Wszystko Co Musisz Wiedzieć O Flagach
Wprowadzenie
W tworzeniu gier często musimy reprezentować wiele stanów binarnych jednocześnie. Postać może jednocześnie biec i skakać, AI może być czujne i wrogie, a system może włączać lub wyłączać określone funkcje gameplayowe w trakcie rozgrywki. Choć dane tego typu można reprezentować za pomocą wielu zmiennych logicznych (bool), istnieje bardziej kompaktowe i elastyczniejsze podejście, jakim są flagi.
Flagi są zazwyczaj implementowane przy użyciu liczb binarnych i masek bitowych (bitmasks). Pozwalają one przechowywać wiele stanów logicznych wewnątrz jednej wartości liczbowej. Zarówno Unreal Engine, jak i Unity oferują natywne wsparcie dla tego wzorca, jednak praca z nimi, w obu przypadkach, nieco się różni.
Ten artykuł obejmuje trzy warstwy tego zagadnienia:
- Liczby binarne i sposób, w jaki mapują się na flagi
- Jak flagi bitowe (bitflags) są używane w kodzie
- W jaki sposób Unreal Engine i Unity wspierają ich użycie
Po zapoznaniu się, ze wszystkimi niuansami będziemy rozumieć nie tylko, jak używać flag, ale również, dlaczego działają.
Systemy liczbowe
Aby reprezentować liczby, używamy tzw. systemów liczbowych.
Każdy system jest definiowany przez skończony zbiór cyfr oraz podstawę. Podstawa określa, ile unikalnych cyfr zawiera dany system.
Jeśli system liczbowy używa:
- 3 cyfr (0, 1, 2) → jest to system trójkowy (ternarny, base-3)
- 4 cyfr (0, 1, 2, 3) → jest to system czwórkowy (quaternary, base-4)
- 10 cyfr (0 - 9) → jest to system dziesiętny (decimal, base-10)
Liczba cyfr jest zawsze równa podstawie systemu.
Różne systemy są używane w różnych sytuacjach. My skupimy się na systemie dziesiętnym i binarnym (dwójkowym).
System dziesiętny (base-10)
Najbardziej naturalnym systemem dla człowieka jest system dziesiętny. Używamy go każdego dnia do liczenia, mierzenia i wykonywania obliczeń.
System dziesiętny zawiera dokładnie dziesięć cyfr:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Ograniczona liczba cyfr i zapis pozycyjny
Każdy system liczbowy ma ograniczony zestaw cyfr. Oznacza to, że w pewnym momencie skończą nam się symbole jednocyfrowe. Właśnie wtedy ważny staje się zapis pozycyjny.
Podczas liczenia w systemie dziesiętnym mamy:
… 7, 8, 9
Po 9 nie istnieje już żadna nowa cyfra, a nie możemy po prostu wymyślić nowego symbolu. Zamiast tego stosujemy prostą zasadę:
- Resetujemy bieżącą cyfrę do 0
- Zwiększamy cyfrę po lewej stronie o 1
Czyli: 9 → 10
Nie stworzyliśmy więc nowego symbolu. Stworzyliśmy nową pozycję.
Struktura pozycyjna w systemie dziesiętnym
Każda pozycja w liczbie dziesiętnej reprezentuje potęgę liczby 10.
Od prawej do lewej:
- Pierwsza pozycja -> \(10^0\)
- Druga pozycja -> \(10^1\)
- Trzecia pozycja -> \(10^2\)
i tak dalej.
Te potęgi opisują, jak działa struktura pozycyjna w systemie dziesiętnym.
Gdy obliczymy ich wartości liczbowo:
\[10^0 = 1\] \[10^1 = 10\] \[10^2 = 100\]Innymi słowy:
Reguła pozycyjna wynika z podstawy systemu dziesiętnego.
Wartości liczbowe zapisujemy dziesiętnie, ponieważ jest to dla nas wygodne.
Mechanizm przeniesienia (Carry)
Rozłóżmy pozycję liczby na czynniki:
\[9 = 9 * 10^0\] \[10 = 1 * 10^1 + 0 * 10^0\] \[11 = 1 * 10^1 + 1 * 10^0\]Kiedy przechodzimy z 9 do 10:
- Skrajnie prawa cyfra osiąga maksymalną wartość (9)
- Dodanie 1 powoduje jej reset do 0
- Pozycja po lewej zwiększa się o 1
Ten mechanizm nazywa się przeniesieniem (carry).
Licząc dalej:
8, 9, 10, 11, 12, 13, …, 19, 20
Przy 19 → 20 dzieje się dokładnie to samo:
- 9 resetuje się do 0
- 1 zwiększa się do 2
Mechanizm przeniesienia nie jest unikalny dla systemu dziesiętnego. Występuje w każdym systemie liczbowym, zmienia się jedynie maksymalna wartość cyfry.
System binarny (base-2)
Komputery nie operują przy użyciu dziesięciu różnych symboli. Na poziomie sprzętowym układy scalone mają tylko dwa stabilne stany:
- Sygnał elektryczny obecny
- Sygnał elektryczny nieobecny
Mapujemy te fizyczne stany na liczby:
- Brak sygnału → 0
- Sygnał obecny → 1
Ponieważ istnieją tylko dwie możliwe cyfry (0 i 1), komputery używają systemu binarnego (base-2).
Liczenie binarne
System binarny działa według tych samych zasad co system dziesiętny. Jedyna różnica polega na maksymalnej cyfrze.
W systemie dziesiętnym największą cyfrą jest 9. W systemie binarnym największą cyfrą jest 1.
Policzmy:
0, 1
I właśnie skończyły nam się cyfry.
Stosujemy więc tę samą regułę:
- Resetujemy bieżącą cyfrę do 0
- Przenosimy 1 na lewo
1 → 10
Ponownie, nie stworzyliśmy nowej cyfry. Stworzyliśmy nową pozycję.
Kontynuując liczenie:
0, 1, 10, 11, …
Teraz dodajemy 1 do 11:
- Skrajnie prawy bit (1) resetuje się do 0
- Powoduje to przeniesienie 1 w lewo
- Lewy bit również ma wartość 1, więc także resetuje się do 0
- Powstaje kolejne przeniesienie
Wynik: 11 → 100
Z systemu binarnego do dziesiętnego
Tak jak system dziesiętny opiera się na potęgach liczby 10, system binarny opiera się na potęgach liczby 2. Każda pozycja w liczbie binarnej reprezentuje potęgę liczby 2.
Od prawej do lewej:
- Pierwsza pozycja → \(2^0\)
- Druga pozycja → \(2^1\)
- Trzecia pozycja → \(2^2\)
- Czwarta pozycja → \(2^3\)
- Piąta pozycja → \(2^4\)
i tak dalej.
Gdy obliczymy te potęgi, otrzymamy:
\[2^0 = 1\] \[2^1 = 2\] \[2^2 = 4\] \[2^3 = 8\] \[2^4 = 16\]ponownie zapisujemy wyniki w systemie dziesiętnym dla większej czytelności.
Same liczby nie „należą” do systemu dziesiętnego. Po prostu używamy symboli dziesiętnych, aby je wyrazić.
Na przykład:
- Wartość \(2^2\) to wartość otrzymana przez podwojenie dwa razy
- W systemie dziesiętnym zapisujemy tę wartość jako 4
- W systemie binarnym ta sama wartość byłaby zapisana jako 100
Podstawa systemu określa strukturę pozycyjną (czyli jakie potęgi są używane). System dziesiętny jest po prostu układem odniesienia, którego używamy do zapisywania otrzymanych wartości.
Konwersja z binarnego na dziesiętny
Aby przekonwertować liczbę binarną na dziesiętną:
- Zacznij od skrajnie prawej cyfry.
- Przypisz potęgi liczby 2 do każdej pozycji.
- Pomnóż każdą cyfrę przez odpowiadającą jej potęgę 2.
- Zsumuj wyniki.
Przykład:
\[11 = 1 * 2^1 + 1 * 2^0 = 2 + 1 = 3.\] \[1101 = 1 * 2^3 + 1 * 2^2 + 0 * 2^1 + 1 * 2^0 = 8 + 4 + 0 + 1 = 13\]Kluczowa obserwacja
Potęgi liczby 2 rosną w następujący sposób:
1, 2, 4, 8, 16, 32, 64, 128…
Każdy kolejny krok podwaja poprzednią wartość. Ten wzorzec podwajania jest kluczową ideą stojącą za flagami bitowymi.
Każdy bit reprezentuje niezależną potęgę liczby 2. Ponieważ potęgi liczby 2 nigdy na siebie nie zachodzą, wiele stanów może zostać połączonych w jedną liczbę, która zawsze będzie unikalna.
Czym są flagi i maski bitowe?
Flaga to pojedynczy bit reprezentujący stan włączony/wyłączony.
Maska bitowa (bitmask) to liczba całkowita, której poszczególne bity działają jako niezależne flagi.
Zamiast przechowywać wiele zmiennych logicznych w taki sposób:
1
2
3
bool isRunning;
bool isJumping;
bool isFalling;
możemy przechowywać wszystkie te stany w jednej liczbie całkowitej.
Każdy bit tej liczby reprezentuje jeden możliwy stan.
Mapowanie stanów na bity
Przypiszmy każdy stan do konkretnej pozycji bitowej:
bit0 → isRunning = \(2^0\)
bit1 → isJumping = \(2^1\)
bit2 → isFalling = \(2^2\)
Po obliczeniu:
\[2^0 = 1\] \[2^1 = 2\] \[2^2 = 4\]Każda z tych wartości odpowiada sytuacji, w której "włączony" jest dokładnie jeden bit.
Reprezentacja binarna:
1
2
3
1 = 001
2 = 010
4 = 100
Przykładowy stan
Załóżmy, że aktualny stan to:
1
011
Czytając od prawej do lewej:
- bit0 (isRunning) → 1 → włączony
- bit1 (isJumping) → 1 → włączony
- bit2 (isFalling) → 0 → wyłączony
Ta konfiguracja oznacza więc:
- isRunning = true
- isJumping = true
- isFalling = false
Jak jest to przechowywane w kodzie
Z perspektywy programisty maska stanu jest przechowywana jako liczba całkowita.
Zamiast więc zapisywać:
1
011
przypisujemy jej reprezentację dziesiętną:
1
int stateMask = 3; //011_2 = 3_10
Używamy systemu dziesiętnego w kodzie, ponieważ jest to standardowa reprezentacja liczb w większości języków programowania.
Wewnętrznie jednak wartość ta jest przechowywana binarnie.
Ważne: Szerokość bitowa i ograniczenia
Typ liczby całkowitej ma stały rozmiar w pamięci.
Na przykład w większości współczesnych systemów:
int ma 32 bity (4 bajty)
Oznacza to, że zawiera 32 niezależne bity.
Ponieważ każdy bit może reprezentować jedną flagę, 32-bitowy integer może przechowywać do 32 niezależnych flag.
Liczba flag, które możesz przechować, zależy bezpośrednio od liczby bitów wybranego typu. Jeśli potrzebujesz więcej flag, powinieneś użyć większego typu.
Dlaczego każda flaga musi być potęgą dwójki
Aby ten system działał poprawnie, każda flaga musi odpowiadać unikalnej potędze liczby 2:
\[1, 2, 4, 8, 16, 32, …\]Czyli:
\[2^0, 2^1, 2^2, 2^3, 2^4, …\]Każda potęga dwójki ma dokładnie jeden ustawiony bit w swojej reprezentacji binarnej:
1
2
3
4
1 = 0001
2 = 0010
4 = 0100
8 = 1000
Ponieważ te wartości nigdy nie nachodzą na siebie binarnie, można je bezpiecznie łączyć.
Przykład:
1
2
3
isRunning = 001;
isJumping = 010;
combined = 011;
To, co ma znaczenie strukturalnie, to układ bitów.
Każda pozycja bitowa reprezentuje odrębną potęgę liczby 2.
Ponieważ potęgi dwójki nie współdzielą bitów, ich łączenie nie nadpisuje ani nie niszczy informacji.
Dzięki temu pojedyncza liczba całkowita może bezpiecznie kodować wiele niezależnych stanów logicznych.
Zamiast przechowywać kilka osobnych zmiennych, pakujemy je do jednej. To właśnie jest podstawowa idea stojąca za maskami bitowymi.
Operacje bitowe
Flagi bitowe opierają się na operatorach bitowych (bitwise operators). Operacjach, które działają bezpośrednio na pojedynczych bitach wewnątrz liczby całkowitej.
W przeciwieństwie do operatorów arytmetycznych (+, -, *), operatory bitowe nie traktują liczby jako całości. Zamiast tego operują niezależnie na każdym bicie.
Dla czytelności będziemy przedstawiać maski jako 4-bitowe liczby binarne.
Operator przesunięcia << (przesuwanie bitów w lewo)
Operator przesunięcia w lewo przesuwa bity o daną liczbę pozycji w lewo.
Każde przesunięcie w lewo mnoży wartość przez 2.
1
1 << n
oznacza: przesuń binarne 1 o n pozycji w lewo.
Przykład:
1
2
3
4
1 = 0001
1 << 1 = 0010
1 << 2 = 0100
1 << 3 = 1000
Każde przesunięcie podwaja wartość.
Dlatego, dla wygody, możemy definiować flagi w następujący sposób:
1
2
isRunning = 1 << 1; // 0010
isJumping = 1 << 2; // 0100
Gwarantuje to, że każda flaga ma ustawiony dokładnie jeden bit.
OR | (ustawianie bitu)
Operator OR porównuje bity pozycja po pozycji.
Reguła:
1
2
3
4
0 | 0 = 0
1 | 0 = 1
0 | 1 = 1
1 | 1 = 1
Jeśli którykolwiek z bitów ma wartość 1, wynik to 1.
Przykład:
1
2
3
4
0010 (isRunning)
| 0100 (isJumping)
-------------------
0110
Oba bity pozostają ustawione, dlatego OR używamy do włączania flag.
1
state = state | isRunning;
AND & (zachowaj wspólne bity)
Reguła:
1
2
3
4
0 & 0 = 0
1 & 0 = 0
0 & 1 = 0
1 & 1 = 1
Tylko jeśli oba bity mają wartość 1, wynik również wynosi 1.
Przykład:
1
2
3
4
0110 (state)
& 0010 (isRunning)
-------------------
0010
Tylko gdy obie ze stron działania są ustawiona na 1, to wynik również da 1. Dlatego AND służy do sprawdzania, czy konkretny bit jest ustawiony.
1
bool isSet = (state & isRunning) != 0;
NOT ~ (odwracanie bitów)
Operator NOT odwraca każdy bit.
Reguła:
1
2
~0 = 1
~1 = 0
Przykład:
1
~0010 = 1101
W prawdziwym kodzie liczby całkowite mają szerokość 32 lub 64 bitów, więc
~odwraca wszystkie bity.
AND NOT &~ (czyszczenie bitów)
Aby usunąć flagę:
- Odwracamy bity
- Wykonujemy AND z aktualnym stanem
Przykład:
1
2
3
4
5
6
7
8
9
// Odwróć flagę, którą chcesz usunąć
isRunning: 0010
~isRunning: 1101
// AND z aktualnym stanem maski bitowej
0110 (state) -> isJumping, isRunning -> włączone
& 1101 (~isRunning)
-------------------
0100
isRunning zostało usunięte.
1
state = state & ~isRunning;
XOR ^ (przełączanie bitów)
XOR porównuje bity i ustawia 1 tylko wtedy, gdy są różne.
Reguła:
1
2
3
4
0 ^ 0 = 0
1 ^ 0 = 1
0 ^ 1 = 1
1 ^ 1 = 0
Operator odwraca bity tam, gdzie maska ma 1.
Przykład:
1
2
3
4
5
6
7
8
9
10
11
// przełącz running
0100 (state)
^ 0010 (isRunning)
-------------------
0110
// przełącz ponownie
0110 (state)
^ 0010 (isRunning)
-------------------
0100
XOR służy do przełączania flagi.
1
state = state ^ isRunning;
Dlaczego te operatory są ważne
Operacje bitowe:
- Działają bezpośrednio na bitach
- Są bardzo szybkie (instrukcje na poziomie CPU)
- Pozwalają przechowywać wiele stanów logicznych w jednej liczbie całkowitej
- Eliminują potrzebę przechowywania wielu osobnych zmiennych logicznych
A ponieważ każda flaga odpowiada unikalnej potędze liczby 2, operacje te nigdy nie uszkadzają pozostałych flag.
Flagi jako enumy
Zwykłe liczby całkowite
Choć można zaimplementować flagi przy użyciu zwykłych liczb całkowitych, poleganie na surowych wartościach liczbowych nie jest dobrą praktyką w rzeczywistym procesie produkcyjnym. Same liczby nie niosą za sobą zbyt wiele informacji.
Na przykład:
1
int stateMask = 6;
Co oznacza 6?
Bez kontekstu nie da się stwierdzić, czy:
6oznaczaRunning+Jumping6oznaczaDebug+Invincible6oznacza coś zupełnie niezwiązanego
Sama liczba nie zawiera żadnej informacji semantycznej.
Problem zwykłych liczb całkowitych
Można próbować poprawić czytelność, wprowadzając stałe:
1
2
3
int Running = 1;
int Jumping = 2;
int Falling = 4;
Na pierwszy rzut oka wygląda to akceptowalnie. Wartości mają nazwy i są czytelniejsze niż surowe liczby.
Jednak to podejście nadal ma kilka problemów:
- Stałe nie są zgrupowane w sensownej strukturze
- Nie ma wymuszonej relacji między nimi
- Mogą zostać przypadkowo użyte w niepowiązanym kontekście
- Nie istnieje type safety
Kompilator nie jest w stanie zapobiec mieszaniu niepowiązanych flag.
„Wrzućmy je do klasy”
Możemy spróbować zamknąć te zmienne w nazwanym kontekście:
1
2
3
4
5
6
7
class CharacterState
{
public:
static const int Running = 1;
static const int Jumping = 2;
static const int Falling = 4;
};
Choć poprawia to organizację kodu, nie rozwiązuje głównego problemu:
- Nadal przekazujemy zwykłe liczby całkowite
- Typ
Runningto nadalint - Typ
Jumpingto nadalint
- Typ
To oznacza, że nadal możemy napisać:
1
2
int health = 100;
stateMask = health | CharacterState::Running; // logicznie błędne, ale skompiluje się
Kompilator nie potrafi wykryć, że taka operacja nie ma semantycznego sensu.
Główny problem: brak kontekstu
int nie zawiera informacji o:
- Dziedzinie, do której należy
- Jakie wartości są poprawne
- Czy dwie wartości są kompatybilne
Nawet jeśli opakujemy stałe w klasy lub namespace’y, nadal pozostają one nieotypowanymi liczbami całkowitymi.
Polegamy więc na dyscyplinie programisty zamiast na gwarancjach kompilatora.
W dużych projektach staje się to kruche i podatne na błędy.
Enumy zapewniają strukturę
Lepszym rozwiązaniem jest użycie enumeracji (enum).
Enum definiuje nazwany zbiór powiązanych wartości stałych wewnątrz konkretnego typu.
Przykład:
```cpp id enum CharacterState { Running = 1 « 0, Jumping = 1 « 1, Falling = 1 « 2 };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Teraz:
* Flagi należą do jasno określonego kontekstu (`CharacterState`)
* Każda wartość ma opisową nazwę
* Intencja kodu jest jednoznaczna
* Wartości są z definicji stałe
To poprawia czytelność i utrzymywalność kodu.
### Dlaczego enumy dobrze współpracują z flagami
Enumy szczególnie dobrze nadają się do flag, ponieważ:
* Wewnętrznie są reprezentowane jako liczby całkowite
* Można je łączyć operatorami bitowymi
* Zachowują znaczenie semantyczne w kodzie
* W razie potrzeby można rzutować je na liczby całkowite
Przykład:
```cpp
int stateMask = CharacterState::Running | CharacterState::Jumping;
Wewnętrznie nadal daje to wartość 3, ale kod staje się samodokumentujący.
Kontekst typów i bezpieczeństwo
Enumy wprowadzają ścisły kontekst. Zamiast przekazywać dowolne liczby całkowite, przekazujemy wartości należące do dobrze zdefiniowanej dziedziny.
W silnie typowanych wariantach kompilator może zapobiec przypadkowemu mieszaniu niepowiązanych wartości — chyba że jawnie wykonamy rzutowanie.
Zmniejsza to liczbę błędów i poprawia poprawność programu.
Flagi w Unity
W Unity flagi są implementowane przy użyciu enumów C# wraz z atrybutem [System.Flags].
Atrybut [Flags] informuje silnik (oraz runtime), że wartości enuma mogą być łączone przy użyciu operacji bitowych.
Bez tego atrybutu enum zachowuje się jak zwykła enumeracja, co oznacza, że jednocześnie można wybrać tylko jedną wartość w edytorze.
Definiowanie enum z flagami
1
2
3
4
5
6
7
8
9
[System.Flags]
public enum CharacterState
{
None = 0,
Running = 1 << 0,
Jumping = 1 << 1,
Falling = 1 << 2,
Everything = Running | Jumping | Falling
}
Ważne szczegóły:
None = 0jest zalecane jako domyślny stan „brak ustawionych flag”.- Każda wartość jest definiowana za pomocą
1 << n, aby zagwarantować unikalny bit. - Enum wewnętrznie nadal jest reprezentowany jako liczba całkowita.
- Możemy także zdefiniować
Everything, aby reprezentować stan „wszystkie flagi ustawione”.
Zachowanie w Inspectorze
Kiedy pole tego typu enum zostaje zserializowane, Unity renderuje je w Inspectorze jako dropdown z wielokrotnym wyborem, zamiast zwykłego dropdownu jednokrotnego wyboru.
Oznacza to, że:
- Możesz wybrać wiele wartości jednocześnie
- Inspector wyświetla checkboxy
- Unity automatycznie łączy wybrane flagi przy użyciu bitowego OR
Przykładowe pole:
1
public CharacterState state;
W Inspectorze pojawi się dropdown, w którym można zaznaczyć wiele pozycji naraz.
Wewnętrznie, jeśli zaznaczone są Running i Jumping:
1
2
3
4
Running = 0010
Jumping = 0100
--------------
state = 0110
W reprezentacji dziesiętnej:
1
state = 6
Ale to układ bitów ma rzeczywiste znaczenie.
Praca z flagami w kodzie
Do operowania na flagach w kodzie używamy poznanych wcześniej operatorów bitowych:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Łączenie flag
state = CharacterState.Running | CharacterState.Jumping;
// Sprawdzanie flagi
if ((state & CharacterState.Running) != 0)
{
// Running jest aktywne
}
// Usuwanie flagi
state = state & ~CharacterState.Running;
// Przełączanie flagi
state = state ^ CharacterState.Running;
Ważne uwagi dotyczące Unity
[Flags]nie zmienia wewnętrznego działania enuma — wpływa głównie na sposób jego wyświetlania i interpretacji.- Unity serializuje enum jako liczbę całkowitą.
- Maksymalna liczba flag zależy od typu bazowego (domyślnie
int, czyli 32 bity). - W razie potrzeby można określić inny typ bazowy:
1
public enum CharacterState : byte
Flagi w Unreal Engine
W Unreal Engine flagi są implementowane przy użyciu tzw. enum class w połączeniu z systemem refleksji silnika.
Aby enum mógł być używany jako maska bitowa w edytorze, wymagane są dwie rzeczy:
- Oznaczenie enuma za pomocą
UENUM(meta = (Bitflags)) - Użycie makra
ENUM_CLASS_FLAGS(), aby włączyć operatory bitowe dla typu enum
Bez tej konfiguracji enum zachowuje się jak zwykła enumeracja, co oznacza, że jednocześnie można wybrać tylko jedną wartość w edytorze.
Definiowanie enum z flagami
1
2
3
4
5
6
7
8
9
10
11
UENUM(meta = (Bitflags))
enum class ECharacterState : uint8
{
None = 0,
Running = 1 << 0,
Jumping = 1 << 1,
Falling = 1 << 2,
Everything = Running | Jumping | Falling
};
ENUM_CLASS_FLAGS(ECharacterState);
Ważne szczegóły:
None = 0jest zalecane jako domyślny stan „brak ustawionych flag”.- Każda wartość jest definiowana przy użyciu
1 << n, aby zagwarantować unikalny bit. Everythingjest opcjonalne, ale przydatne jako stan „wszystkie flagi aktywne”.ENUM_CLASS_FLAGSwłącza operatory|,&,^,~dla tego typu enum.- Enum jest oparty o typ całkowity (tutaj
uint8, ale może to być takżeuint32itd.).
Zachowanie w Inspectorze
W przeciwieństwie do Unity, Unreal Engine nie przechowuje maski bezpośrednio jako typu enum podczas ekspozycji w edytorze.
Zamiast tego tworzymy zmienną typu integer i łączymy ją z enumem.
1
2
UPROPERTY(EditAnywhere, meta = (Bitmask, BitmaskEnum = "ECharacterState"))
int32 StateMask;
To mówi Unrealowi:
- Traktuj
StateMaskjako maskę bitową - Użyj
ECharacterStatedo zdefiniowania checkboxów
Edytor wyrenderuje to jako listę checkboxów zamiast zwykłego pola integer.
Te same metadane można zastosować do parametrów funkcji:
1
void SomeFunction(UPARAM(meta = (Bitmask, BitmaskEnum = "ECharacterState")) int32 InState)
Praca z flagami w kodzie
Jeśli nie wystawiamy wartości do edytora, lepiej przechowywać ją bezpośrednio jako typ enum. Jeśli używamy int32 jako maski eksponowanej do edytora, musimy rzutować wynik operacji bitowych na int32.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int32 StateMask;
ECharacterState StateMaskEnum;
// Łączenie flag
StateMask = StaticCast<int32>(ECharacterState::Running | ECharacterState::Jumping);
StateMaskEnum = ECharacterState::Running | ECharacterState::Jumping;
// Sprawdzanie flagi
if ((StateMask & StaticCast<int32>(ECharacterState::Running)) != 0)
{
// Running jest aktywne
}
if ((StateMaskEnum & ECharacterState::Running) != ECharacterState::None)
{
// Running jest aktywne
}
// Usuwanie flagi
StateMask = StateMask & StaticCast<int32>(~ECharacterState::Running);
StateMaskEnum = StateMaskEnum & ECharacterState::Jumping;
// Przełączanie flagi
StateMask = StateMask ^ StaticCast<int32>(ECharacterState::Running);
StateMaskEnum = StateMaskEnum ^ ECharacterState::Running;
Unreal udostępnia także funkcje pomocnicze do pracy z enumami-flagami:
EnumHasAnyFlagsEnumHasAllFlagsEnumAddFlagsEnumRemoveFlags
Działają one bezpośrednio na typach enum (nie na surowym int32).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int32 StateMask;
ECharacterState StateMaskEnum;
// Łączenie flag
EnumAddFlags(StaticCast<ECharacterState>(StateMask), ECharacterState::Running | ECharacterState::Jumping);
EnumAddFlags(StateMaskEnum, ECharacterState::Running | ECharacterState::Jumping);
// Sprawdzanie flagi
if (EnumHasAnyFlags(StaticCast<ECharacterState>(StateMask), ECharacterState::Running))
{
// Running jest aktywne
}
if (EnumHasAnyFlags(StateMaskEnum, ECharacterState::Running))
{
// Running jest aktywne
}
// Usuwanie flagi
EnumRemoveFlags(StaticCast<ECharacterState>(StateMask), ECharacterState::Running);
EnumRemoveFlags(StateMaskEnum, ECharacterState::Running);
// Dodawanie flagi
EnumAddFlags(StaticCast<ECharacterState>(StateMask), ECharacterState::Running);
EnumAddFlags(StateMaskEnum, ECharacterState::Running);
Te "helpery" poprawiają czytelność i zmniejszają ryzyko błędów związanych z rzutowaniem.
Ważne uwagi dotyczące Unreal Engine
- Wymaga jawnego powiązania enuma przez metadane
- Jest bardziej verbose niż Unity
- Zapewnia silną integrację z systemem refleksji i Blueprintami
Po poprawnej konfiguracji flagi integrują się płynnie z:
- C++
- Blueprintami
- UI edytora
- Property System
Praktyczne zastosowania
Flagi są szczególnie przydatne wtedy, gdy wiele niezależnych stanów musi współistnieć w efektywny sposób.
Typowe użycia obejmują:
- Stany ruchu postaci
- Tryby zachowania AI
- Stany umiejętności lub skilli
- Przełączniki debugowe i developerskie
- Określanie właściciela obiektu w multiplayerze
Flagi są właściwym rozwiązaniem, gdy:
- Wiele stanów może być aktywnych jednocześnie
- Stany są logicznie niezależne
- Wymagane są szybkie sprawdzenia
- Efektywność pamięci ma znaczenie
- Chcesz uzyskać zwartą, komponowalną reprezentację stanu
Jeśli jednocześnie aktywny może być tylko jeden stan, zwykły enum (bez flag) jest zazwyczaj lepszym wyborem.
Najczęstsze pułapki
Niepoprawne wartości enumów
Używanie wartości takich jak:
1
1, 2, 3, 4
psuje cały system.
Każda flaga musi być potęgą liczby 2:
1
1, 2, 4, 8, 16, 32, ...
W przeciwnym razie bity zaczną się nakładać, a kombinacje staną się niejednoznaczne.
Nakładające się bity
Dwie flagi używające tej samej pozycji bitowej prowadzą do konfliktów:
cpp id="4x2kza" Running = 1 << 1; Jumping = 1 << 1; // błąd
Obie wartości reprezentowałyby ten sam bit.
Przekroczenie szerokości bitowej
Liczba całkowita ma stałą liczbę bitów:
- Typ 32-bitowy → maksymalnie 32 flagi
- Typ 64-bitowy → maksymalnie 64 flagi
Przekroczenie tego limitu prowadzi do overflow i nieokreślonego lub niezamierzonego zachowania.
Słabe nazewnictwo
Flagi powinny opisywać stan, a nie akcję.
Dobre nazwy:
IsRunningCanJumpHasAuthority
Złe nazwy:
StartRunningJumpNow
Flagi reprezentują warunki lub właściwości, nie polecenia.
Podsumowanie
Flagi są niskopoziomową, ale niezwykle potężną techniką opartą na binarnej reprezentacji liczb.
Pozwalają wielu niezależnym stanom współistnieć w jednej wartości, pozostając jednocześnie wydajnymi i łatwymi do sprawdzania.
Ponieważ każda flaga odpowiada unikalnej potędze liczby 2, ich kombinacje pozostają jednoznaczne i bezpieczne.
Oba najpopularniejsze silniki wspierają ten wzorzec:
- Unreal Engine integruje flagi głęboko ze swoim systemem refleksji oraz narzędziami edytora.
- Unity wykorzystuje enumy C# wraz z atrybutem
[Flags], oferując eleganckie wsparcie na wysokim poziomie języka.
Zrozumienie, w jaki sposób liczby binarne mapują się na flagi bitowe, pozwala na:
- Lepsze rozumienie reprezentacji stanu
- Czystsze projektowanie systemów
- Efektywniejsze wykorzystanie pamięci
- Szybsze sprawdzanie warunków
- Bardziej ekspresyjną logikę gameplayową
Flagi bitowe są proste na poziomie sprzętowym, ale stają się niezwykle potężnym narzędziem, gdy są świadomie wykorzystywane w game developmencie.



Comments powered by Disqus.