English summary: This article explains lessons learned when implementing custom NetworkVariable types in Unity Netcode for GameObjects. It highlights common pitfalls around serialization, update order, and choosing between NGO interfaces for real multiplayer data models.

Need help with similar technical challenges? See Services.

Jakmile se pustíte do multiplayeru s pomocí NGO brzy přijdete na to, že vám defaultní struktury NetworkVariable a NetworkList nebudou stačit. Budete si muset napsat vlastní.

Jak pracovat s vlastní síťovou proměnnou?

V prvé řadě je třeba zmínit několik důležitých informací.

NetworkVariable používejte uvnitř třídy NetworkBehaviour, jinak nebude fungovat. Párkrát jsem se nachytal, že jsem si nevšiml a měl proměnou uvnitř MonoBehaviour nebo Lifetimescope (VContainer) a hledal jsem chybu půl dne.

Tady pozor na to, že každá proměnná se po sítí aktualizuje jinak, v jiný čas a taky pořadí! To není garantované. Píšou to tady, ale já to samozřejmě pořádně nečetl.

Netcode nabízí několik interface, jejichž význam nemusí být hned jasný, takže lehce dovysvětlím:

INetworkSerializeByMemcpy

  • struct
  • musí být unmanaged (i nested)
  • (de)serializace je automatická

Takový typ pak můžete použít rovnou v NetworkVariable a lze posílat přes RPC.

INetworkSerializable

  • struct
  • managed i unmanaged typy
  • (de)serializaci si musíte napsat

Takový typ pak můžete použít rovnou v NetworkVariable a lze posílat přes RPC.

NetworkVariableBase

  • class!

Tady si můžete implementovat co chcete včetně (de)serializace. Instanci si tvoříte již sami.

Příklad použití vlastní proměnné

Představte si, že chcete s hráči spárovat nějaká data.

Typicky uděláte v první verzi zřejmě použijete pro vztah hráč a data Dictionary, kde klíč bude Guid nebo jiný identifikátor hráče (klidně i string) a hodnota bude nějaká struktura s informacemi o hráči.

Například:

public class NetworkPlayersInfo : NetworkBehaviour
{
    private readonly PlayerInfoNetworkVariable m_PlayersInfoNetworkVariable = new();
}

[Serializable]
public class PlayerInfoNetworkVariable : NetworkVariableBase
{
    private Dictionary<Guid, PlayerInfo> m_PlayersInfo = new();
    // povinné metody ReadDelta, ReadField, WriteDelta, WriteField
}

Vypadá to hezky, ale má to pár háčků. Pojďme si ukázat upravenou verzi a posléze ji okomentovat.

public class NetworkPlayersInfo : NetworkBehaviour
{
    private readonly PlayerInfoNetworkVariable m_PlayersInfoNetworkVariable = new();
    
    private void Awake()
    {
        m_PlayersInfoNetworkVariable.Initialize();
    }
}

[Serializable]
public class PlayerInfoNetworkVariable : NetworkVariableBase
{
    private NativeHashMap<byte, PlayerInfo> m_PlayerInfoMap;
    
    public void Initialize()
    {
        m_PlayerInfoMap = new NativeHashMap<byte, PlayerInfo>(10, Allocator.Persistent);
    }
    
    public override void Dispose()
    {
        m_PlayerInfoMap.Dispose();
    }

    // povinné metody ReadDelta, ReadField, WriteDelta, WriteField
}

public struct PlayerInfo : INetworkSerializeByMemcpy {
    // jen unmanaged typy
    public byte PlayerId;
    public ushort TotalMoney;
    // atd.
}

Za prvé, snažte se po sítí přenášet, co nejmenší množství dat. Tzn. ideálně jen unmanaged typy - byte, ushort, enumy atd.

Pokud to jde, doporučuji použít Unity Native Collection. Mělo by to být lepší než System.Collections.

Problém je, že s Native Collection nemůžete vytvořit instanci přímo v deklaraci fieldu ani v konstruktoru kvůli memory leaku. To je třeba jeden z důvodů, proč nelze vytvořit instanci NetworkList přímo v deklaraci, ale musíte ji vytvořit v Awake()

Je to opět uvedeno tady v příkladu:

void Awake()
{
    // NetworkList can't be initialized at declaration time like NetworkVariable. It must be initialized in Awake instead.
    // If you do initialize at declaration, you will run into Memmory leak errors.
    TeamAreaWeaponBoosters = new NetworkList<AreaWeaponBooster>();
}

Závěr

Několik doporučení:

  • pro ladění je super ParrelSync
  • ještě lepší je Multiplayer Play Mode, ale funguje bohužel až od verze 2023.1 a dost často padá
  • hodně používejte NetworkLog a Debug
  • pamatujte, že čas ani pořadí proměných není garantované - musíte vymyslet vlastní synchronizaci (o tom jindy)
  • pro testovaní nepište zbytečně WriteDelta a ReadDelta - je to opruz a je to důležité až pro produkční verzi
  • používejte Asserty, hodně to usnadní ladění
  • klidně rozsekejte logiku jedna proměnná = jeden NetworkVariable objekt
  • budete téměř vždy potřebovat callbacky, že se data změnily
  • navrhněte si naming a ten dodržujte
  • počítejte s tím, že nad síťovými proměnnými budete stavět další vrstvu, protože network variables jsou psány pro optimalizaci pásma a vy budete potřebovat získané data nějak prezentovat (například si přes síť pošlete u karetní hry jen CardId:byte a na klientovi si z registru karet vytáhnete textury apod.)

P.S.: NetworkList má aktuálně nepříjemnou chybu, že neposílá připojeným klientům oznámení o změně, viz nahlášený bug.

Neváhejte komentovat, sdílet svůj názor, nápad, zkušenost. Uvítám každou dobrou radu, kritiku i pomoc. Rovněž uvítám kolegy programátory, grafiky, zvukaře, animátory, vfx specialisty a mnoho dalších, kteří se na vývoji her podílí.