Press "Enter" to skip to content

Flags aneb kolekce možných hodnot

Zechy 0

Občas je potřeba mít k dispozici v jedné proměnné uloženo vícero možných kombinací. Tyto hodnoty pak můžeme přidávat nebo číst pomocí bitových/bitwise operátorů.

Co to jsou flagy

Flagy nám umožňují do jedné proměnné zkombinovat vícero hodnot. V C# k tomu velmi jednoduše můžeme využít tzv. Enum, který dozdobíme atributem [Flags], umožnujíc nám využít nad takovými proměnnými bitové operace.

[Flags]
public enum Colors {
	None = 0,
  	Red = 1,
  	Green = 2,
  	Blue = 4
}

Každá další hodnota musí být násobkem dvou té předchozí, protože společně jim pak vzniká možná sada kombinací, které zaplňují chybějící mezery. Pokud bychom tedy míchali klasické RGB barvy pomocí tohoto Enumu, máme následující tabulku:

KombinaceHodnotaPoznámka
None0Žádná/černá barva.
Red1První základní barva.
Green2Druhá základní barva.
Red | Green3Červená + Zelená =Žlutá (2 + 1 = 3)
Blue4Třetí základní barva.
Blue | Red5Modrá + Červená = Purpurová (4 + 1 = 5)
Blue | Green6Modrá + Zelená = Azurová (4 + 2 = 6)
Blue | Green | Red7Modrá + Zelená + Červená = Bílá (1 + 2 + 4 = 7)

A pokud bychom vymysleli novou barvu do našeho systému, měla by hodnotu 8.

Tučně máme zvýrazněné základní hodnoty z našeho Enum, zatím co kurzívou jsou jejich kombinace. Nyní jsme tak schopni složit všechny ostatní RGB barvy. Pokud to převedeme do kódu:

Colors yellow = Colors.Red | Colors.Green;
Colors blue = Colors.Blue;
Colors white = Colors.Blue | Colors.Green | Colors.Red;

Pomocí operátoru | řekneme, že se hodnota skládá z vícero různých. Následně si pak snadno můžeme ověřit jaké hodnoty proměnná obsahuje. Výsledkem nám vždy bude ověřovaná hodnota nebo None.

Console.WriteLine(yellow & Colors.Red); // Vrátí Red, jelikož ji obsahuje.
Console.WriteLine(white & Colors.Blue); // Vrátí Blue, jelikož ji obsahuje.

Console.WriteLine(blue & Colors.Red); // Vrátí None, protože modrá žádnou jinou neobsahuje.
Console.WriteLine(yellow & Colors.Blue); // Vrátí None, protože modrou neobsahuje.

Udělejme si to jednodušší

Občas je ovšem potřeba si věci trochu zjednodušit a neztrácet čas psaním, k tomu nám může pomoci třída FlagsHelper, kterou lze k tomuto tématu najít na StackOverflow.

public static class FlagsHelper
{
    public static bool IsSet<T>(T flags, T flag) where T : struct
    {
        var flagsValue = (int) (object) flags;
        var flagValue = (int) (object) flag;

        return (flagsValue & flagValue) != 0;
    }

    public static void Set<T>(ref T flags, T flag) where T : struct
    {
        var flagsValue = (int) (object) flags;
        var flagValue = (int) (object) flag;

        flags = (T) (object) (flagsValue | flagValue);
    }

    public static void Unset<T>(ref T flags, T flag) where T : struct
    {
        var flagsValue = (int) (object) flags;
        var flagValue = (int) (object) flag;

        flags = (T) (object) (flagsValue & ~flagValue);
    }
}

U všech metod si můžeme povšimnout, že používáme tzv. generiku, tedy obecný typ, není tak nutné definovat přesně o jaký typ proměnné se má jednat, ale dále máme podmínku where, pomocí které říkáme, že předávaný typ by měl být v základu struct, tedy právě i Enum.

public static bool IsSet<T>(T flags, T flag)

Tato metoda nám ověří, zda se v naší proměnné flags nachází flag námi požadovaný. Hodnoty se tedy postupně převedou na jejich číselné vyjádření.

A následně pomocí bitového operátoru porovnáme naše dvě hodnoty. Jelikož jsme vše převedli na int, výsledkem neexistujícího flagu bude 0. Pokud tedy nezískáme nulu, máme flag v proměnné definovaný.

public static void Set<T>(ref T flags, T flag)

Pomocí této funkce můžeme zase naopak do proměnné flags libovolný flag přidat. K tomu nám navíc slouží klíčové slovíčko ref, kdy si předáváme pouze odkaz na proměnnou a tak veškeré provedené změny v této funkci budou převedeny i do naší proměnné.

Jak už jsme si ukázali, libovolný flag přidáme pomocí operátoru |.

public static void Unset<T>(ref T flags, T flag)

Podobně jako flagy přídáme, můžeme je i odebrat. K tomu budiž kombinace & ~flag. Která odstraní náš zvolený flag.

Do praxe!

Jak to využít v praxi? Můj poslední využitý příklad je z herního vývoje, kdy máme herní předměty. Ty obsahující spoustu různých definicí, jako například název či popisek pro hráče, zbraně dále mohou obsahovat údaje o jejich útoku… A můžeme tak mít hodnoty, u kterých je výhodné, pokud budou obsahovat kombinace různých možností.

Například jak hráč může s předměty interagovat, protože s jedním předmětem může být možné vykonat vícero různých akcí.

[Flags]
public enum ItemFlag {
	None = 0,
    Collect = 1,
    Activate = 2,
    Search = 4,
    Combine = 8,
    ThrowAway = 16
}

Pomocí těchto hodnot můžu definovat jednotlivým předmětům libovolnou kombinaci možných akcí, které lze s nimi provést. Hráč může předmět pouze sbírat (Collect), nebo sbírat i zahazovat (Collect | ThrowAway) a tak dále, než vyčerpáme všechny možné kombinace.

Unity: Zobraz mi to v inspektoru!

Flagy je ovšem možné zobrazit i v unity inspektorech a tak pro jednotlivé herní objekty jejich vlastnosti rovnou klikat. K tomu nám vystačí atribut [Serializable].

[Flags]
[Serializable]
public enum ItemFlag {
	// Naše hodnoty
}

Použijeme-li tento enum v našem Mono skriptu, Unity nám zobrazí dropdown nabídku, kde si můžeme vybírat jednotlivé hodnoty. Případně si vybrat mezi žádné a vše.

Flags Enum v Unity Inspektoru

Napsat komentář