Artykuł

Wszystko Co Musisz Wiedzieć O Flagach



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ą:

  1. Zacznij od skrajnie prawej cyfry.
  2. Przypisz potęgi liczby 2 do każdej pozycji.
  3. Pomnóż każdą cyfrę przez odpowiadającą jej potęgę 2.
  4. 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:

  • 6 oznacza Running + Jumping
  • 6 oznacza Debug + Invincible
  • 6 oznacza 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 Running to nadal int
    • Typ Jumping to nadal int

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 = 0 jest 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.

UnityInspectorFlag

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 = 0 jest zalecane jako domyślny stan „brak ustawionych flag”.
  • Każda wartość jest definiowana przy użyciu 1 << n, aby zagwarantować unikalny bit.
  • Everything jest opcjonalne, ale przydatne jako stan „wszystkie flagi aktywne”.
  • ENUM_CLASS_FLAGS włącza operatory |, &, ^, ~ dla tego typu enum.
  • Enum jest oparty o typ całkowity (tutaj uint8, ale może to być także uint32 itd.).

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 StateMask jako maskę bitową
  • Użyj ECharacterState do zdefiniowania checkboxów

Edytor wyrenderuje to jako listę checkboxów zamiast zwykłego pola integer.

UnrealInspectorFlag

Te same metadane można zastosować do parametrów funkcji:

1
void SomeFunction(UPARAM(meta = (Bitmask, BitmaskEnum = "ECharacterState")) int32 InState)

UnrealBlueprintFlag

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:

  • EnumHasAnyFlags
  • EnumHasAllFlags
  • EnumAddFlags
  • EnumRemoveFlags

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:

  • IsRunning
  • CanJump
  • HasAuthority

Złe nazwy:

  • StartRunning
  • JumpNow

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.

Autor

Konrad Słoń

Programming Wizard

To dzieło jest licencjonowane na warunkach CC BY 4.0 .

Comments powered by Disqus.

Popularne tagi