Jump to content
  • Chmurka
  • Boróweczka
  • Jabłuszko
  • Limonka
  • Czekoladka
  • Węgielek
Sign in to follow this  
MAGNET

[Poradnik] Operacje na bitach oraz jak używać flag?

Recommended Posts

KOMPEDIUM WIEDZY O BITACH, OPERACJACH BITOWYCH I ZASTOSOWANIU W PRAKTYCE

 

Jeśli masz już jakieś doświadczenie z programowaniem, niezależnie czy jest to pawn, C czy inny język, na pewno na trafiłeś na takie pojęcie, jak "flaga" w pluginie/skrypcie etc. Masz też zapewne świadomość do czego taka flaga służy

 

Cytat

Flaga służy do określania poszczególnych atrybutów/ustawień w funkcjach.

 

 

Przykładowo, w poradniku Vasto o timerach
Hej! Skorzystałeś z linku lub pobrałeś załącznik? Uhonoruj naszą pracę poprzez rejestrację na forum i rośnij razem z nami! mamy do czynienia z flagami timera, np.

UchwytNaszegoTimeru = CreateTimer(99999.9, FunkcjaKtoraSieWywola, INVALID_HANDLE, TIMER_REPEAT);

czwarty argument funkcji CreateTimer
Hej! Skorzystałeś z linku lub pobrałeś załącznik? Uhonoruj naszą pracę poprzez rejestrację na forum i rośnij razem z nami! mówi nam, że timer będzie cyklicznie wywoływał podaną funkcję dopóki go nie 'zabijemy' (KillTimer
Hej! Skorzystałeś z linku lub pobrałeś załącznik? Uhonoruj naszą pracę poprzez rejestrację na forum i rośnij razem z nami!). Idea wydaje się być prosta - funkcja działa tak, jak jej rozkażemy.

 

 

Nieco wyżej pojawia się jednak takie wywołanie:

CreateTimer(1.0, FunkcjaKtoraSieWywola, GetClientUserId(client), TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE|TIMER_DATA_HNDL_CLOSE);

Czy potrafisz wyjaśnić, co tutaj się dzieje? Na pewno domyślasz się, że timer wykona się wraz ze wszystkimi trzema flagami, jednak rozłożenie tego na czynniki pierwsze wydaje się być dosyć enigmatyczne. 

Okazuje się jednak, że nie jest to wcale takie straszne. Wymaga to dobrego zrozumienia, jednak na pewno zapunktuje w przyszłości. Postaram się wyjaśnić to tak dobrze, na ile tylko będę w stanie. Przygotuj sobie jakieś jedzenie i do roboty!

 

Część I - jak zapisujemy liczby?

Podstawą tutaj jest dwubitowa architektura komputerów. Oznacza to, że dane w pamięci reprezentowane są w postaci logicznej prawdy (1 - jedynka logiczna), lub fałszu (0 - zero logiczne). Kiedyś nieodzownym było operowanie właśnie na pamięci w takiej formie, jednak z czasem zaczęły powstawać języki programowania, które pozwalały na bardziej abstrakcyjne podejście do tematu kodowania. Jednak pomimo tego, programowanie cały czas sprowadza się do tych samych operacji, co wiele lat temu, jednak w ładniejszym wydaniu (co niekonieczne oznacza, że lepszym). Pod tym, co widzi współczesny programista kodując w C++, w dalszym ciągu kryją się skomplikowane operacje bitowe.

I tutaj mamy całe clue sprawy - flagi są reprezentowane bitowo!

 

Od razu nasuwa się pytanie - dlaczego? Po co ktoś miałby używać takiego podejścia? Odpowiedź nasunie się sama w dalszej części poradnika

 

W komputerach najmniejszą adresowalną jednostką jest bajt, który składa się z ośmiu bitów, gdzie każdy bit może zawierać 1 lub 0 (jak wspominałem wyżej). My nie posługujemy się "bajtami" - my działamy ma zmiennych, jak int, float, czy char.

Otóż każda zmienna składa się ze stałej ilości bajtów (char 1 bajt, int i float 4 bajty w pawnie). Współczesne języki są jednak na tyle elastyczne, że możemy bez problemu posługiwać się znanymi nam dobrze operacjami dodawania, odejmowania, mnożenia, potęgowania itd bez potrzeby działania na bitach

Nie oznacza to jednak, że jesteśmy od tych operacji odcięci ? programiści bardzo dobrze wykorzystują je do optymalizacji działania programów, w tym znanych nam już flag. Dzisiaj również nauczymy się, jak pisać takie flagi samemu.

 

Skoro wiemy już, że pod takim "prostym" integerem(int) kryje się zlepek bitów, zajrzyjmy sobie do niego:

bajt.thumb.png.66390469fb562d46af3ce3479cf7831a.png

Jak widzimy w powyższej jakże profesjonalnie wykonanej, tabeli pokazana jest tylko połowa integera - 2 bajty. Tyle jednak w zupełności wystarczy, żeby pokazać o co tutaj chodzi...

Dowolną liczbę zapisaną w jednym systemie można zapisać w innym (np. z dziesiętnego na szesnastkowy). W tym przypadku interesuje nas zamiana liczby dziesiętnej na binarną(ciąg zer i jedynek). Ktoś kiedyś słusznie zauważył,  że: 

Cytat

każdy bit w liczbie dwójkowej to kolejna potęga dwójki, zaczynając od zera 

i tak 1 to 2^0, 2 to 2^1 i tak dalej....

można zauważyć tutaj jeszcze jedną ciekawą zależność - wartość dziesiętna każdego bitu jest równa wartości wszystkich poprzednich bitów plus jeden, np.

2^6 = 64 = 32+16+8+4+2+1+1

Płynący z tego wniosek jest taki, że każda liczba dziesiętna ma swoją unikalną reprezentację w systemie dwójkowym.

 

Jak zapisujemy liczby dwójkowo? Oto przykłady:

15 = 1111 (2^0 + 2^1 + 2^2 + 2^3)  |  20 = 10100 (2^4 + 2^2)  |  1024 = 10000000000 (2^10)

Działanie odwrotne (liczba binarna na dziesiętną) prezentuje się następująco:

101 = 2^0*1 + 2^1*0 + 2^2*1 = 5  |  1000 = 2^0*0 + 2^1*0 + 2^2*0 + 2^3*1 = 8

Po więcej informacji odnośnie przeliczeń zapraszam tutaj
Hej! Skorzystałeś z linku lub pobrałeś załącznik? Uhonoruj naszą pracę poprzez rejestrację na forum i rośnij razem z nami!.

 

 

Skoro więc wiemy, że pod każdą zmienną kryją się bity, oraz że jest ich skończona ilość dla danej zmiennej, możemy również znaleźć największą wartość, jaką dany typ zmiennej może przechowywać. Dla uproszczenia nie będę się zagłębiał w liczby zmiennoprzecinkowe - dziś skupimy się tylko na liczbach całkowitym i na integerze.

Przydatnym narzędziem systemowym jest kalkulator, który oferuje tryb programisty. Po wprowadzeniu do niego 32 jedynek binarnie (bo tyle bitów posiada integer), otrzymałem nastepujący wynik:

calc.png.47da13a410b3306588639f471c13091f.png

Sporo, ale...nie jest to faktyczna wartość maksymalna integera. Musimy także pamiętać o tym, że liczba może być także ujemna! Jak określić tutaj ujemność liczby? W tej sytuacji poświęca się ostatni bit, którego wartośc określa, czy liczba jest dodatnia, czy ujemna.

Skoro straciliśmy jeden bit, nasza liczba "skurczyła się" do 31 miejsc, a więc teraz jest to... 2 147 483 647, a konkretniej przedział < -2 147 483 647 ; 2 147 483 647 >

 

Na tym etapie rozumiemy jak zapisywane są liczby w komputerze, co to bit, bajt i jak przechodzimy z systemu dziesiętnego do binarnego i odwrotnie. Teraz możemy zająć się operacjami bitowymi.

 

 

Część II - operacje bitowe

W systemie dziesiętnym wykonujemy operacje dodawania, odejmowania, mnożenia, dzielenia, potęgowania, pierwiastkowania i tak dalej. System binarny charakteryzuje się swoim własnym zestawem operacji. Każdą operację wykonujemy parami - pierwszy bit liczby numer 1 z pierwszym bitem numer 2 - czynność powtarzamy na wszystkich bitach.

Omówię teraz po kolei wszystkie operacje.

 

1. AND (mnożenie - koniunkcja)  ->  Znak &

Wynik operacji AND daje 1, jeśli obydwie liczby są jedynką

AND.png.929ce4191137e9c51b42b91e8351b6c6.png

 

 Przykład:   1010 & 1001 = 1000   |   1100 & 0101 = 0100

figure2-and_mask.png.51abdbde8c40b74da0a2ca50f70bc4ba.png

 

2. OR (dodawanie - alternatywa) -> Znak |

Wynik operacji daje 1, jeśli przynajmniej jedna z liczb jest jedynką

OR.png.4212bec9d031be1697b630aab31f1d49.png

 

Przykład:  1010 | 1001 = 1011   |   1100 | 0101 = 1101

bitwise-or.png.5d2e65ef551c0297596ce50ffc39392f.png

 

3. XOR (alternatywa wykluczająca) | Znak ^

Wynik operacji daje 1, jeśli dwie liczby różnią się, a 0, jeśli są takie same

XOR.png.cc47c78371ce069f0075629eb9531b85.png

 

Przykład: 1010 ^ 1001 = 0011   |   1100 ^ 0101 = 1001

 

4. NOT (negacja) | Znak ~ (tylda)

Operacja odwraca wartość bitu

NOT.png.bdb121983e4e08234030b6ff190b6027.png

 

Przykład: ~1010 = 0101   |   ~1100 = 0011

 

Są to cztery podstawowe operacje  logiczne. Do omówienia pozostały jeszcze przesunięcia bitowe:

 

5. Przesunięcie bitowe w lewo | Znak <<

 

Operacja powoduje, że wszystkie bity w liczbie zostają przesunięte w lewo. Wszystkie bity, które na skutek przesunięcia 'wypadły' poza liczbę zostają utracone. Bity, które 'pojawiają się z prawej strony są zapisywane zawsze jako zera.

Po lewej stronie operandu zapisujemy liczbę, natomiast po prawej o ile miejsc przesuwamy.

 

Przykład: 1010 << 1 = 0100   |   1100 << 1 = 1000

 

5. Przesunięcie bitowe w prawo | Znak >>

 

Operacja powoduje, że wszystkie bity w liczbie zostają przesunięte w prawo. Wszystkie bity, które na skutek przesunięcia 'wypadły' poza liczbę zostają utracone. Bity, które 'pojawiają się z lewej strony są zapisywane zawsze jako zera.

Po lewej stronie operandu zapisujemy liczbę, natomiast po prawej o ile miejsc przesuwamy.

 

Przykład: 1010 >> 1 = 0101   |   1100 >> 1 = 0110

 

Wszystko wydaje się być dziwne i bezsensowne, jednak o tym jak potężne jest to narzędzie przekonasz się w części trzeciej ?

Omówiliśmy już wszystkie operacje. Teraz, kiedy jesteśmy już gruntowo przygotowani pod względem teoretycznym, możemy wreszcie przejść do praktyki.

 

 

Część III - flagi

Rozpatrzmy teoretyczną sytuację, w której chcemy monitorować, czy gracz (dla uproszczenia jeden) posiada w nicku frazę "[GO-Code]", aby nagradzać go dodatkową ilością złota co rundę. Aby tego dokonać, musimy wykonać szereg operacji: znaleźć id gracza, na jego podstawie pobrać nick i umieścić w tablicy, sprawdzić, czy nazwa posiada szukaną frazę za pomocą StrContains
Hej! Skorzystałeś z linku lub pobrałeś załącznik? Uhonoruj naszą pracę poprzez rejestrację na forum i rośnij razem z nami!, a następnie zwrócić rezultat. Mimo, że wszystkie te operacje trwają dość szybko (dla człowieka), to komputer nie ma świadomości tego, że już raz sprawdziliśmy nick gracza - dlatego w każdej kolejnej rundzie musi mieć jasną informację, czy ma przydzielać złoto z tytułu posiadanej frazy...

Co byś w tej sytuacji zrobił? Najsensowniejszym rozwiązaniem wydaje się być zastosowanie zwykłej zmiennej, która domyślnie przyjmuje wartość 0 i zmienia się na 1, jeśli powyższe warunki zostaną spełnione. Teraz wystarczy, że serwer będzie na początku rundy sprawdzał zawartość zmiennej, zamiast każdorazowo wykonywać szereg kosztownych operacji - a nie zapominajmy, że przeważnie jest dużo więcej graczy do sprawdzenia ?

Załóżmy teraz, że poza sprawdzeniem frazy [GO-Code] chcielibyśmy, by serwer dodatkowo sprawdzał, czy SteamID gracza to STEAM_0:1:1234567, gdyż jest to streamer, które gra na naszym serwerze i chcemy zapewnić mu dodatkowe przywileje. Aby to sprawdzać zapewne ponownie zastosujemy dodatkową zmienną, jednak musimy być świadomi, że teraz mamy już dwie zmienne, zamiast jednej.

A co, jeżeli nasze dwie zmienne są lokalne i chcielibyśmy je przekazać do innej funkcji? Sprawa wygląda w miarę prosto, gdy mamy dwie zmienne, ale co w sytuacji, gdzie potrzebujemy takich zmiennych np. dziesięć?

PrzekazStatystyki(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10)

Pomyślmy, jak wyglądałaby deklaracja flag regexa
Hej! Skorzystałeś z linku lub pobrałeś załącznik? Uhonoruj naszą pracę poprzez rejestrację na forum i rośnij razem z nami!? Co w sytuacji, gdybyśmy chcieli skorzystać tylko z jednej opcji? Mimo wszystko musielibyśmy wysyłać wszystkie zmienne (bez wyraźnej potrzeby)

 

Dopiero teraz na wierzch wychodzi to, jak użyteczne są operacje bitowe

Pamiętamy, że każdy int to zestaw 32 bitów. Każdy bit przyjmuje wartość 1 lub 0.... czyli właśnie to, czego potrzebujemy!

1 i 0 to właśnie nasza prawda i fałsz!

Cytat

W jednym integerze możemy zapisać równowartość 32 wartości typu prawda-fałsz

daje nam to niesamowicie szeroki wachlarz odogodnień. Zamiast tworzyć dziesięć zmiennych:

int x1, x2, x3, x4, x5, x6, x7, x8, x9;

tworzymy jedną:

int flags;

 

Zamiast przekazywać do funkcji dziesięć zmiennych:

PrzekazStatystyki(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10)

przekazujemy jedną:

PrzekazStatystyki(flags)

 

Teraz pozostaje nam nauczenie się, jak z tego korzystać, gdyż wszystkie operacje będą przeprowadzane na bitach.

Powróćmy do naszego przykładu dwóch flag gracza. Gdybyśmy realizowali to standardową metodą, robilibyśmy to w taki sposób:

Spoiler

public void CzyMaFraze(int client)
{
	int fraza = 0;
	int sid = 0;
	// nie chodzi tutaj o pokazanie, jak przeprowadzac sprawdzanie nicku czy SID, wiec uproszcze tutaj sobie sprawe...
	if(maFraze(client))	fraza = 1;
	
	if(maSID(client))	sid = 1;
  
	PrzekazInformacje(client, fraza, sid);
}

Oraz funkcja przydzielająca atrybuty graczowi:

Spoiler

public void PrzekazInformacje(int client, int fraza, int sid)
{
	if(fraza)
	{
		// tutaj dajemy zloto...
	}

	if(sid)
	{
		// a tutaj jakies przywileje...
	}
}

Teraz napiszmy to samo z wykorzystaniem bitów 

 

W naszym przykładzie posiadanie frazy w nicku będzie równoznaczne z ustawieniem pierwszego bitu (od prawej) w zmiennej na 1, natomiast streamer - drugiego bitu. Zauważmy tutaj ciekawą rzecz - pierwszy bit oznacza jedynkę (2^0), natomiast drugi bit dwójkę (2^1). Jeśli więc streamer będzie miał w nicku frazę "GO-Code" (obydwa przypadki spełnione), nasza flaga przyjmie wartość...3! Dlaczego? Oto wyjaśnienie (przypominam, że int ma 32 bity):

int flags = 0000 0000 0000 0000 0000 0000 0000 0011 + 2^0 + 2^1 = 3

Prawda, że intuicyjne? Podobnie, gdybyśmy mieli dziesięć flag i chcielibyśmy sprawdzić, czy WSZYSTKIE są ustawione na true(1) wystarczyłoby porównanie wartości zmiennej flags do 1023 (ponieważ każdy bit to suma poprzedników plus jeden, a pod jedenastym bitem siedzi 2^10, czyli 1024. Oznacza to, że wartość 1024 to 00...100 0000 0000, a 1023 to 00...011 1111 1111)

Dodatkowo, jeśli wykonujemy operacje arytmetyczne na liczbie dziesiętnej, i tak można ją przedstawić w postaci binarnej. Jeśli więc do zmiennej, która ma wartość 0, dodamy 2 (zmienna += 2) drugi bit tej zmiennej stanie się jedynką, a pozostałe będą zerami.

My jednak nie będziemy wykonywać operacji arytmetycznych, gdyż operacja na np. dziesięciu zmiennych mogłaby przyprawić o zawrót głowy nawet największego wymiatacza 

 

Aby dodać flagę do zmiennej (zmienić wartość bitu), będziemy posługiwać się logiczną operacją OR. Wiemy z części drugiej, że OR zwraca 1, jeśli przynajmniej jedna ze zmiennych jest prawdziwa. Jeśli więc wykonamy taką operację:

flags |= 1 ( to samo, co flags = flags|1 )

spowodujemy, że pierwszy bit naszej zmiennej stanie się jedynką, natomiast pozostałe pozostaną bez zmian:

...101010 | ...000001(jedynka binarnie) = ...101011

prawda, że fajne? jedynym problemem jest dobre zrozumienie co tutaj się dzieje 

 

podobnie, ustawienie drugiej flagi wiąże się z operacją OR, jednak tutaj posłużymy się już dwójką:

...010101 | ...000010(dwójka binarnie) = ...010111

 

Ale to jeszcze nie wszystko! Aby nie musieć poruszać się cały czas po potęgach dwójki, możemy również użyć przesunięć bitowych. Jak to działa? Zauważmy, że przesunięcie bitowe w lewo jest równoznaczne z pomnożeniem naszej liczby przez 2. Tak więc 2<<1 = 4. Widzimy też, że działa to dokładnie tak, jak nasza identyfikacja numeru bitu. Jeśli więc zrobimy coś takiego:

flags |= (1<<1)

efekt będzie taki sam, jak:

flags |= 2

 

Sytuacja robi się jeszcze przyjemniejsza, gdy zdefiniujemy sobie takie makra:

#define FRAZA (1<<0)
#define STREAMER (1<<1)

a później już tylko sama bajka:

flags |= STREAMER;
flags |= FRAZA;
lub
flags |= (STREAMER|FRAZA);

I WŁAŚNIE TAKICH MAKR UŻYWA SIĘ WSZĘDZIE! Nie ważne, czy będzie to ADMIN_IMMUNITY z flag admina, czy  PCRE_UTF8 z regexa - wszędzie odbywa się to w dokładnie ten sam sposób!

 

Ostatnią rzeczą jest sprawdzenie, czy dana flaga jest aktywna, Tutaj posługujemy się logiczną operacją AND. Daje ona jedynkę tylko w sytuacji, gdy obydwie zmienne są prawdziwe. Możemy to obrócić na naszą korzyść. Aby sprawdzić, czy dany bit (tj nasza flaga) jest "włączona", robimy coś takiego:

if(flags & FRAZA)

jak wiemy, if "przepuszcza dalej", jeśli wartość wyrażenia jest różna od zera. A więc jeśli if otrzymał przykładowo:

...010101 & ...000001 = ...000001 = 1 (binarnie)

...musiał on przepuścić warunek 

sprawdzenia możemy łączyć:

if(flags & FRAZA && flags & STREAMER)
{
	// uprawnienia i kasiora prosze
}

pamiętajmy też o kolejności wykonywania działań! Z tego właśnie powodu stosuję nawiasy zarówno w warunku jak i przy deklaracji makr #define

 

Tak oto przerobiliśmy cały materiał. Po zastosowaniu zdobytej wiedzy w praktyce, nasz poprzedni kod możemy napisać w następujący sposób:

Spoiler

#define FRAZA (1<<0)
#define STREAMER (1<<1)

public void CzyMaGraze(int client)
{
	int flags=0;

	if(maFraze)	flags |= FRAZA;
	if(maSID)	flags |= STREAMER;
  
	PrzekazInformacje(client, flags);
}
public void PrzekazInformacje(int client, int flags)
{
	if(flags & FRAZA)
    {
        //tutaj dajemy  zloto...
    }
  	if(flags & STREAMER)
    {
        //tutaj dajemy jakies przywileje...
    }
}

Oczywiście nie należy popadać w paranoję i stosować tych flag wszędzie, gdzie się tylko da xD. W małych pluginach możemy spokojnie korzystać ze zwykłych zmiennych, gdyż narzut pamięciowy jest i tak znikomy. Wierzę, że będziesz w stanie znaleźć do tego wiele zastosowań.

 

Wooah.. ale poleciałem. Poradnik wyszedł strasznie długi, ale chciałem to zrobić w sposób rzetelny i w takiej formie, jaką sam bym chciał otrzymać lata temu, ponieważ nikt nie wytłumaczył tego w należyty sposób. Niech Wam dobrze służy 

Jeśli macie jeszcze jakieś pytania, czy wątpliwości, śmiało zadawajcie pytania, a jeśli okaże się to potrzebne, zaktualizuję poradnik.

 

Oczywiście zachęcam do przeglądania innych poradników i ciągłego samodoskonalenia.

 

Dzięki za uwagę i powodzenia w lekturze 

  • Lubię to! 3
  • Kocham to! 4

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Sign in to follow this  

  • Recently Browsing   0 members

    No registered users viewing this page.

Nasza historia

Na początku byliśmy małą grupą internetowych znajomych, którzy stwierdzili, że potrzebne jest solidne forum, na którym znajdą się ludzie z dużą wiedzą programistyczną ukierunkowaną na CS:GO. Pomysł powstał na początku 2018 roku, a parę miesięcy później, 19 kwietnia, powstała ta strona internetowa. Jako alternatywna odpowiedź na inne tego typu miejsca, poważnie podeszliśmy do tematu, najpierw tłumacząc angielską dokumentację SourceMod'a na język polski, a potem pisząc rozległe poradniki i wypełniając forum najpotrzebniejszymi rzeczami dla właścicieli serwerów i programistów. Cała nasza Ekipa jest dumna z pracy jaką w to włożyliśmy i cieszymy się że zbierają się wokół nas zarówno ludzie znający tematy sourcepawn'a i konfiguracji, jak i również nowe twarze w tym "biznesie", którym z chęcią niesiemy wiedzę oraz pomoc w rozwiązywaniu problemów.

Największe modyfikacje serwerowe

×
×
  • Create New...