Skip to content

8.8. fejezet: HUD overlay készítése ​


Összefoglaló: Ez az útmutató végigvezet egy egyéni HUD overlay készítésén, amely szerver információkat jelenít meg a képernyő jobb felső sarkában. Létre fogsz hozni egy layout fájlt, írni egy vezérlő osztályt, becsatlakozni a misszió életciklusba, adatokat kérni a szerverről RPC-n keresztül, hozzáadni egy átkapcsoló billentyűkötést, és csiszolni az eredményt halványítási animációkkal és intelligens láthatósággal. A végére egy nem tolakodó Szerver Infó HUD-od lesz, amely a szerver nevét, a játékosszámot és az aktuális játékon belüli időt mutatja -- plusz alapos megértésed arról, hogyan működnek a HUD overlayek a DayZ-ben.


Tartalomjegyzék ​


Mit építünk ​

Egy kicsi, félig átlátszó panel, amely a képernyő jobb felső sarkához van rögzítve, és három sor információt jelenít meg:

  Aurora Survival [Official]
  Players: 24 / 60
  Time: 14:35

A panel az állapotjelzők alatt és a gyorselérési sáv felett helyezkedik el. Másodpercenként egyszer frissül (nem minden képkockánál), megjelenik behalványodva és eltűnik kihalványodva, és automatikusan elrejtőzik, amikor a felszerelés vagy a szüneteltetés menü nyitva van. A játékos ki- és bekapcsolhatja egy konfigurálható billentyűvel (alapértelmezett: F7).

Várt eredmény ​

Betöltéskor egy sötét, félig átlátszó téglalapot fogsz látni a képernyő jobb felső területén. Fehér szöveg mutatja a szerver nevét az első sorban, az aktuális játékosszámot a második sorban, és a játékon belüli világidőt a harmadik sorban. Az F7 megnyomása simán kihalványítja; az F7 újbóli megnyomása visszahalványítja.


Előfeltételek ​

  • Működő mod struktúra (először végezd el a 8.1. fejezetet)
  • Alapvető Enforce Script szintaxis ismerete
  • A DayZ kliens-szerver modelljének ismerete (a HUD a kliensen fut; a játékosszám a szerverről jön)

Mod struktúra ​

Hozd létre a következő könyvtárfát:

ServerInfoHUD/
    mod.cpp
    Scripts/
        config.cpp
        data/
            inputs.xml
        3_Game/
            ServerInfoHUD/
                ServerInfoRPC.c
        4_World/
            ServerInfoHUD/
                ServerInfoServer.c
        5_Mission/
            ServerInfoHUD/
                ServerInfoHUD.c
                MissionHook.c
    GUI/
        layouts/
            ServerInfoHUD.layout

A 3_Game réteg definiálja a konstansokat (az RPC ID-nket). A 4_World réteg kezeli a szerver oldali választ. Az 5_Mission réteg tartalmazza a HUD osztályt és a misszió hookot. A layout fájl definiálja a widget fát.


1. lépés: A layout fájl létrehozása ​

A layout fájlok (.layout) egy kapcsos zárójel alapú formátumban definiálják a widget hierarchiát. Minden widget WidgetClass WidgetName { ... } formában van deklarálva, az attribútumok csupasz name value sorok (csak akkor idézőjelesek, ha a név szóközt tartalmaz), a gyerek widgetek pedig egy belső { } blokkba vannak ágyazva. A DayZ GUI rendszere egy koordináta modellt használ, ahol minden widgetnek van pozíciója és mérete, arányos értékekkel (0.0 - 1.0 a szülő arányában) plusz pixel eltolásokkal kifejezve.

GUI/layouts/ServerInfoHUD.layout ​

FrameWidgetClass ServerInfoRoot {
 position 0 0
 size 1 1
 halign left
 valign top
 hexactpos 0
 vexactpos 0
 hexactsize 0
 vexactsize 0
 {
  // Háttér panel: jobb felső sarok
  ImageWidgetClass ServerInfoPanel {
   position 1 0
   size 220 70
   halign right
   valign top
   hexactpos 0
   vexactpos 1
   hexactsize 1
   vexactsize 1
   color 0 0 0 0.55
   {
    // Szerver név szöveg
    TextWidgetClass ServerNameText {
     position 8 6
     size 204 20
     hexactpos 1
     vexactpos 1
     hexactsize 1
     vexactsize 1
     font "gui/fonts/MetronBook"
     fontsize 14
     text "Server Name"
     color 1 1 1 0.9
     "text halign" left
     "text valign" top
    }
    // Játékosszám szöveg
    TextWidgetClass PlayerCountText {
     position 8 28
     size 204 18
     hexactpos 1
     vexactpos 1
     hexactsize 1
     vexactsize 1
     font "gui/fonts/MetronBook"
     fontsize 12
     text "Players: - / -"
     color 0.8 0.8 0.8 0.85
     "text halign" left
     "text valign" top
    }
    // Játékon belüli idő szöveg
    TextWidgetClass TimeText {
     position 8 48
     size 204 18
     hexactpos 1
     vexactpos 1
     hexactsize 1
     vexactsize 1
     font "gui/fonts/MetronBook"
     fontsize 12
     text "Time: --:--"
     color 0.8 0.8 0.8 0.85
     "text halign" left
     "text valign" top
    }
   }
  }
 }
}

Főbb layout fogalmak ​

AttribútumJelentés
halign rightVízszintes igazítás: jobbra. A widget a szülője jobb széléhez rögzül.
valign topFüggőleges igazítás: felülre.
hexactpos 0 + vexactpos 1A vízszintes pozíció arányos (1.0 = jobb szél), a függőleges pozíció pixelben van.
hexactsize 1 + vexactsize 1A szélesség és magasság pixelben van (220 x 70).
color 0 0 0 0.55RGBA lebegőpontos számokként. Fekete 55%-os átlátszatlansággal a háttér panelhez.

A ServerInfoPanel az arányos X=1.0 pozícióra (jobb szél) van helyezve halign right (jobbra igazított) beállítással, így a panel jobb széle érinti a képernyő jobb oldalát. Az Y pozíció 0 pixel a tetejétől. Ez a jobb felső sarokba helyezi a HUD-unkat.

Miért pixel méret a panelhez? Az arányos méretezés a panelt a felbontással együtt méretezné, de kis infó widgeteknél rögzített pixel lábnyomot szeretnél, hogy a szöveg minden felbontáson olvasható maradjon.


2. lépés: A HUD vezérlő osztály létrehozása ​

A vezérlő osztály betölti a layoutot, név alapján megtalálja a widgeteket, és metódusokat biztosít a megjelenített szöveg frissítéséhez. A ScriptedWidgetEventHandler-t terjeszti ki, hogy később widget eseményeket fogadhasson, ha szükséges.

Scripts/5_Mission/ServerInfoHUD/ServerInfoHUD.c ​

c
class ServerInfoHUD : ScriptedWidgetEventHandler
{
    protected Widget m_Root;
    protected Widget m_Panel;
    protected TextWidget m_ServerNameText;
    protected TextWidget m_PlayerCountText;
    protected TextWidget m_TimeText;

    protected bool m_IsVisible;
    protected float m_UpdateTimer;

    // Milyen gyakran frissítse a megjelenített adatokat (másodperc)
    static const float UPDATE_INTERVAL = 1.0;

    void ServerInfoHUD()
    {
        m_IsVisible = true;
        m_UpdateTimer = 0;
    }

    void ~ServerInfoHUD()
    {
        Destroy();
    }

    // HUD létrehozása és megjelenítése
    void Init()
    {
        if (m_Root)
            return;

        m_Root = GetGame().GetWorkspace().CreateWidgets(
            "ServerInfoHUD/GUI/layouts/ServerInfoHUD.layout"
        );

        if (!m_Root)
        {
            Print("[ServerInfoHUD] ERROR: Failed to load layout file.");
            return;
        }

        m_Panel = m_Root.FindAnyWidget("ServerInfoPanel");
        m_ServerNameText = TextWidget.Cast(
            m_Root.FindAnyWidget("ServerNameText")
        );
        m_PlayerCountText = TextWidget.Cast(
            m_Root.FindAnyWidget("PlayerCountText")
        );
        m_TimeText = TextWidget.Cast(
            m_Root.FindAnyWidget("TimeText")
        );

        m_Root.Show(true);
        m_IsVisible = true;

        // Kezdeti adatok kérése a szerverről
        RequestServerInfo();
    }

    // Összes widget eltávolítása
    void Destroy()
    {
        if (m_Root)
        {
            m_Root.Unlink();
            m_Root = NULL;
        }
    }

    // Minden képkockánál meghívódik a MissionGameplay.OnUpdate-ből
    void Update(float timeslice)
    {
        if (!m_Root)
            return;

        if (!m_IsVisible)
            return;

        m_UpdateTimer += timeslice;

        if (m_UpdateTimer >= UPDATE_INTERVAL)
        {
            m_UpdateTimer = 0;
            RefreshTime();
            RequestServerInfo();
        }
    }

    // A játékon belüli idő megjelenítés frissítése (kliens oldali, nincs szükség RPC-re)
    protected void RefreshTime()
    {
        if (!m_TimeText)
            return;

        int year, month, day, hour, minute;
        GetGame().GetWorld().GetDate(year, month, day, hour, minute);

        string hourStr = hour.ToString();
        string minStr = minute.ToString();

        if (hour < 10)
            hourStr = "0" + hourStr;

        if (minute < 10)
            minStr = "0" + minStr;

        m_TimeText.SetText("Time: " + hourStr + ":" + minStr);
    }

    // RPC küldése a szervernek a játékosszám és szerver név kéréséhez
    protected void RequestServerInfo()
    {
        if (!GetGame().IsMultiplayer())
        {
            // Offline mód: csak helyi infó megjelenítése
            SetServerName("Offline Mode");
            SetPlayerCount(1, 1);
            return;
        }

        Man player = GetGame().GetPlayer();
        if (!player)
            return;

        ScriptRPC rpc = new ScriptRPC();
        rpc.Send(player, SIH_RPC_REQUEST_INFO, true, NULL);
    }

    // --- Beállítók, amelyek az adatok megérkezésekor hívódnak ---

    void SetServerName(string name)
    {
        if (m_ServerNameText)
            m_ServerNameText.SetText(name);
    }

    void SetPlayerCount(int current, int max)
    {
        if (m_PlayerCountText)
        {
            string text = "Players: " + current.ToString()
                + " / " + max.ToString();
            m_PlayerCountText.SetText(text);
        }
    }

    // Láthatóság átkapcsolása
    void ToggleVisibility()
    {
        m_IsVisible = !m_IsVisible;

        if (m_Root)
            m_Root.Show(m_IsVisible);
    }

    // Elrejtés, amikor menük nyitva vannak
    void SetMenuState(bool menuOpen)
    {
        if (!m_Root)
            return;

        if (menuOpen)
        {
            m_Root.Show(false);
        }
        else if (m_IsVisible)
        {
            m_Root.Show(true);
        }
    }

    bool IsVisible()
    {
        return m_IsVisible;
    }

    Widget GetRoot()
    {
        return m_Root;
    }
};

Fontos részletek ​

  1. CreateWidgets útvonal: Az útvonal a mod gyökérhez képest relatív. Mivel a GUI/ mappát a PBO-n belül csomagoljuk, a motor feloldja a ServerInfoHUD/GUI/layouts/ServerInfoHUD.layout útvonalat a mod prefix segítségével.
  2. FindAnyWidget: Rekurzívan keres a widget fában név alapján. Castolás után mindig ellenőrizd NULL-ra.
  3. Widget.Unlink(): Megfelelően eltávolítja a widgetet és annak összes gyerekét az UI fából. Takarításnál mindig hívd meg.
  4. Időzítő akkumulátor minta: Minden képkockánál hozzáadjuk a timeslice-ot, és csak akkor cselekszünk, amikor az összegyűjtött idő meghaladja az UPDATE_INTERVAL-t. Ez megakadályozza, hogy minden egyes képkockánál dolgozzunk.

3. lépés: Becsatlakozás a MissionGameplay-be ​

A MissionGameplay osztály a misszió vezérlő a kliens oldalon. A modded class-t használjuk, hogy a HUD-unkat beilleszthessük az életciklusába anélkül, hogy lecserélnénk a vanilla fájlt.

Scripts/5_Mission/ServerInfoHUD/MissionHook.c ​

c
modded class MissionGameplay
{
    protected ref ServerInfoHUD m_ServerInfoHUD;

    override void OnInit()
    {
        super.OnInit();

        // A HUD overlay létrehozása
        m_ServerInfoHUD = new ServerInfoHUD();
        m_ServerInfoHUD.Init();
    }

    override void OnMissionFinish()
    {
        // Takarítás a super hívása ELŐTT
        if (m_ServerInfoHUD)
        {
            m_ServerInfoHUD.Destroy();
            m_ServerInfoHUD = NULL;
        }

        super.OnMissionFinish();
    }

    override void OnUpdate(float timeslice)
    {
        super.OnUpdate(timeslice);

        if (!m_ServerInfoHUD)
            return;

        // HUD elrejtése, amikor a felszerelés vagy bármilyen menü nyitva van
        UIManager uiMgr = GetGame().GetUIManager();
        bool menuOpen = false;

        if (uiMgr)
        {
            UIScriptedMenu topMenu = uiMgr.GetMenu();
            if (topMenu)
                menuOpen = true;
        }

        m_ServerInfoHUD.SetMenuState(menuOpen);

        // HUD adatok frissítése (belsőleg korlátozva)
        m_ServerInfoHUD.Update(timeslice);

        // Átkapcsoló billentyű ellenőrzése
        Input input = GetGame().GetInput();
        if (input)
        {
            if (GetUApi().GetInputByName("UAServerInfoToggle").LocalPress())
            {
                m_ServerInfoHUD.ToggleVisibility();
            }
        }
    }

    // Elérő, hogy az RPC kezelő elérhesse a HUD-ot
    ServerInfoHUD GetServerInfoHUD()
    {
        return m_ServerInfoHUD;
    }
};

Miért működik ez a minta ​

  • Az OnInit egyszer fut le, amikor a játékos belép a játékmenetbe. Itt hozzuk létre és inicializáljuk a HUD-ot.
  • Az OnUpdate minden képkockánál fut. Átadjuk a timeslice-ot a HUD-nak, amely belsőleg másodpercenként egyszer korlátozza. Itt ellenőrizzük az átkapcsoló billentyű lenyomását és a menü láthatóságot is.
  • Az OnMissionFinish akkor fut, amikor a játékos lekapcsolódik vagy a misszió véget ér. Itt semmisítjük meg a widgetjeinket a memóriaszivárgás megelőzéséhez.

Kritikus szabály: Mindig takaríts ​

Ha elfelejted megsemmisíteni a widgetjeidet az OnMissionFinish-ben, a widget gyökér átszivárog a következő munkamenetbe. Néhány szerverváltás után a játékosnál egymásra halmozódott szellem widgetek maradnak, amelyek memóriát fogyasztanak. Mindig párosítsd az Init()-et a Destroy()-jal.


4. lépés: Adatok kérése a szerverről ​

A játékosszámot csak a szerver ismeri. Szükségünk van egy egyszerű RPC (Remote Procedure Call) oda-visszaútra: a kliens küld egy kérést, a szerver beolvassa az adatokat és visszaküldi.

4a lépés: Az RPC ID definiálása ​

Az RPC ID-knek egyedinek kell lenniük az összes mod között. A mieinket a 3_Game rétegben definiáljuk, hogy mind a kliens, mind a szerver kód hivatkozhasson rájuk.

Scripts/3_Game/ServerInfoHUD/ServerInfoRPC.c ​

c
// RPC ID-k a Szerver Infó HUD-hoz.
// Magas számok használata az ütközések elkerüléséhez a vanillával és más modokkal.

const int SIH_RPC_REQUEST_INFO = 72810;
const int SIH_RPC_RESPONSE_INFO = 72811;

// Maximum játékos helyek. Nincs GetGame().GetMaxPlayers() natív, ezért
// állítsd ezt úgy, hogy egyezzen a szerver konfigoddal (az indítási paramétereidben lévő érték).
const int SIH_MAX_PLAYERS = 60;

Miért 3_Game? A konstansok és enum-ok a legalsó rétegbe tartoznak, amelyet mind a kliens, mind a szerver elérhet. A 3_Game réteg a 4_World és 5_Mission előtt töltődik be, így mindkét oldal láthatja ezeket az értékeket.

4b lépés: Szerver oldali kezelő ​

A szerver figyeli az SIH_RPC_REQUEST_INFO-t, összegyűjti az adatokat, és válaszol az SIH_RPC_RESPONSE_INFO-val.

Scripts/4_World/ServerInfoHUD/ServerInfoServer.c ​

c
modded class PlayerBase
{
    override void OnRPC(
        PlayerIdentity sender,
        int rpc_type,
        ParamsReadContext ctx
    )
    {
        super.OnRPC(sender, rpc_type, ctx);

        if (!GetGame().IsServer())
            return;

        if (rpc_type == SIH_RPC_REQUEST_INFO)
        {
            HandleServerInfoRequest(sender);
        }
    }

    protected void HandleServerInfoRequest(PlayerIdentity sender)
    {
        if (!sender)
            return;

        // Szerver infó összegyűjtése
        string serverName = "";
        GetGame().GetHostName(serverName);

        int playerCount = 0;
        int maxPlayers = 0;

        // Játékos lista lekérése
        ref array<Man> players = new array<Man>();
        GetGame().GetPlayers(players);
        playerCount = players.Count();

        // Nincs GetGame().GetMaxPlayers(). A beállított hely-
        // szám a szerver konfigban él, nem egy scriptből elérhető
        // CGame natívban, ezért itt egy ismert konstanst használunk.
        maxPlayers = SIH_MAX_PLAYERS;

        // Válasz küldése vissza a kérő kliensnek
        ScriptRPC rpc = new ScriptRPC();
        rpc.Write(serverName);
        rpc.Write(playerCount);
        rpc.Write(maxPlayers);
        rpc.Send(this, SIH_RPC_RESPONSE_INFO, true, sender);
    }
};

4c lépés: Kliens oldali RPC fogadó ​

A kliens fogadja a választ és frissíti a HUD-ot.

Add hozzá a ServerInfoHUD osztály alá a ServerInfoHUD.c fájlban:

c
modded class PlayerBase
{
    override void OnRPC(
        PlayerIdentity sender,
        int rpc_type,
        ParamsReadContext ctx
    )
    {
        super.OnRPC(sender, rpc_type, ctx);

        if (GetGame().IsServer())
            return;

        if (rpc_type == SIH_RPC_RESPONSE_INFO)
        {
            HandleServerInfoResponse(ctx);
        }
    }

    protected void HandleServerInfoResponse(ParamsReadContext ctx)
    {
        string serverName;
        int playerCount;
        int maxPlayers;

        if (!ctx.Read(serverName))
            return;
        if (!ctx.Read(playerCount))
            return;
        if (!ctx.Read(maxPlayers))
            return;

        // A HUD elérése a MissionGameplay-en keresztül
        MissionGameplay mission = MissionGameplay.Cast(
            GetGame().GetMission()
        );

        if (!mission)
            return;

        ServerInfoHUD hud = mission.GetServerInfoHUD();
        if (!hud)
            return;

        hud.SetServerName(serverName);
        hud.SetPlayerCount(playerCount, maxPlayers);
    }
};

Hogyan működik az RPC folyamat ​

KLIENS                           SZERVER
  |                                |
  |--- SIH_RPC_REQUEST_INFO ----->|
  |                                | beolvassa: serverName, playerCount, maxPlayers
  |<-- SIH_RPC_RESPONSE_INFO ----|
  |                                |
  | frissíti a HUD szöveget       |

A kliens másodpercenként egyszer küldi a kérést (a frissítési időzítő korlátozza). A szerver három értékkel válaszol, amelyeket az RPC kontextusba csomagol. A kliens ugyanabban a sorrendben olvassa ki őket, ahogyan írva lettek.

Fontos: Az rpc.Write() és a ctx.Read() ugyanazokat a típusokat kell használja ugyanabban a sorrendben. Ha a szerver egy string-et, majd két int értéket ír, a kliensnek egy string-et, majd két int értéket kell olvasnia.


5. lépés: Átkapcsolás billentyűkötéssel ​

5a lépés: A bemenet definiálása az inputs.xml-ben ​

A DayZ az inputs.xml-t használja egyéni billentyű akciók regisztrálásához. A fájlt a Scripts/data/inputs.xml helyre kell tenni, és hivatkozni rá a config.cpp-ből.

Scripts/data/inputs.xml ​

xml
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<modded_inputs>
    <inputs>
        <actions>
            <input name="UAServerInfoToggle" loc="Toggle Server Info HUD" />
        </actions>
    </inputs>
    <preset>
        <input name="UAServerInfoToggle">
            <btn name="kF7" />
        </input>
    </preset>
</modded_inputs>
ElemCél
<actions>Deklarálja a beviteli akciót név alapján. A loc a billentyűkötési beállítások menüben megjelenített string.
<preset>Az alapértelmezett billentyűt rendeli hozzá. A kF7 az F7 billentyűhöz térképeződik.

5b lépés: Az inputs.xml hivatkozása a config.cpp-ben ​

A config.cpp-nek meg kell mondania a motornak, hol találja a bemeneti fájlt. Adj hozzá egy inputs bejegyzést a defs blokkon belül:

cpp
class defs
{
    class gameScriptModule
    {
        value = "";
        files[] = { "ServerInfoHUD/Scripts/3_Game" };
    };

    class worldScriptModule
    {
        value = "";
        files[] = { "ServerInfoHUD/Scripts/4_World" };
    };

    class missionScriptModule
    {
        value = "";
        files[] = { "ServerInfoHUD/Scripts/5_Mission" };
    };

    class inputs
    {
        value = "";
        files[] = { "ServerInfoHUD/Scripts/data" };
    };
};

5c lépés: A billentyű lenyomás olvasása ​

Ezt már kezeljük a MissionGameplay hookban a 3. lépésből:

c
if (GetUApi().GetInputByName("UAServerInfoToggle").LocalPress())
{
    m_ServerInfoHUD.ToggleVisibility();
}

A GetUApi() a beviteli API singletont adja vissza. A GetInputByName kikeresi a regisztrált akciónkat. A LocalPress() pontosan egy képkockára adja vissza a true-t, amikor a billentyű lenyomódik.

Billentyű név referencia ​

Gyakori billentyű nevek a <btn>-hez:

Billentyű névBillentyű
kF1 - kF12Funkcióbillentyűk
kH, kI, stb.Betűbillentyűk
kNumpad0 - kNumpad9Számbillentyűzet
kLControlBal Control
kLShiftBal Shift
kLAltBal Alt

Módosító kombinációk beágyazást használnak:

xml
<input name="UAServerInfoToggle">
    <btn name="kLControl">
        <btn name="kH" />
    </btn>
</input>

Ez azt jelenti: "tartsd lenyomva a bal Control-t és nyomd meg a H-t."


6. lépés: Csiszolás ​

6a: Behalványodás/kihalványodás animáció ​

A DayZ WidgetFadeTimer-t biztosít sima alfa átmenetekhez. Frissítsd a ServerInfoHUD osztályt, hogy használja:

c
class ServerInfoHUD : ScriptedWidgetEventHandler
{
    // ... meglévő mezők ...

    protected ref WidgetFadeTimer m_FadeTimer;

    void ServerInfoHUD()
    {
        m_IsVisible = true;
        m_UpdateTimer = 0;
        m_FadeTimer = new WidgetFadeTimer();
    }

    // Cseréld le a ToggleVisibility metódust:
    void ToggleVisibility()
    {
        m_IsVisible = !m_IsVisible;

        if (!m_Root)
            return;

        if (m_IsVisible)
        {
            m_Root.Show(true);
            m_FadeTimer.FadeIn(m_Root, 0.3);
        }
        else
        {
            m_FadeTimer.FadeOut(m_Root, 0.3);
        }
    }

    // ... az osztály többi része ...
};

A FadeIn(widget, duration) a widget alfáját 0-ról 1-re animálja a megadott időtartam alatt másodpercben. A FadeOut 1-ről 0-ra megy és elrejti a widgetet, amikor kész.

6b: Háttér panel alfával ​

Ezt már beállítottuk a layoutban (color="0 0 0 0.55"), ami egy sötét overlayt ad 55%-os átlátszatlansággal. Ha futásidőben akarod módosítani az alfát:

c
void SetBackgroundAlpha(float alpha)
{
    if (m_Panel)
    {
        int color = ARGB(
            (int)(alpha * 255),
            0, 0, 0
        );
        m_Panel.SetColor(color);
    }
}

Az ARGB() függvény 0-255 közötti egész értékeket fogad az alfa, piros, zöld és kék csatornákhoz.

6c: Betűtípus és szín választások ​

A DayZ több betűtípust szállít, amelyekre hivatkozhatsz a layoutokban:

Betűtípus útvonalStílus
gui/fonts/MetronBookTiszta sans-serif (a vanilla HUD-ban használt)
gui/fonts/MetronMediumVastagabb változat a MetronBook-ból
gui/fonts/MetronLegvékonyabb változat
gui/fonts/luxuriousscriptDekoratív script (kerüld a HUD-nál)

Szöveg szín megváltoztatása futásidőben:

c
void SetTextColor(TextWidget widget, int r, int g, int b, int a)
{
    if (widget)
        widget.SetColor(ARGB(a, r, g, b));
}

6d: Más felületek tiszteletben tartása ​

A MissionHook.c fájlunk már észleli, amikor egy menü nyitva van, és meghívja a SetMenuState(true)-t. Íme egy alaposabb megközelítés, amely kifejezetten a felszerelést is ellenőrzi:

c
// A modded MissionGameplay OnUpdate felülírásában:
bool menuOpen = false;

UIManager uiMgr = GetGame().GetUIManager();
if (uiMgr)
{
    UIScriptedMenu topMenu = uiMgr.GetMenu();
    if (topMenu)
        menuOpen = true;
}

// A felszerelés nyitva van-e, szintén ellenőrizzük
if (uiMgr && uiMgr.FindMenu(MENU_INVENTORY))
    menuOpen = true;

m_ServerInfoHUD.SetMenuState(menuOpen);

Ez biztosítja, hogy a HUD-od elrejtőzik a felszerelés képernyő, a szüneteltetés menü, a beállítások képernyő és bármely más scriptelt menü mögött.


Teljes kód referencia ​

Az alábbiakban a mod minden fájlja, végleges formájában az összes csiszolással.

1. fájl: ServerInfoHUD/mod.cpp ​

cpp
name = "Server Info HUD";
author = "YourName";
version = "1.0";
overview = "Displays server name, player count, and in-game time.";

2. fájl: ServerInfoHUD/Scripts/config.cpp ​

cpp
class CfgPatches
{
    class ServerInfoHUD_Scripts
    {
        units[] = {};
        weapons[] = {};
        requiredVersion = 0.1;
        requiredAddons[] =
        {
            "DZ_Data",
            "DZ_Scripts"
        };
    };
};

class CfgMods
{
    class ServerInfoHUD
    {
        dir = "ServerInfoHUD";
        name = "Server Info HUD";
        author = "YourName";
        type = "mod";

        dependencies[] = { "Game", "World", "Mission" };

        class defs
        {
            class gameScriptModule
            {
                value = "";
                files[] = { "ServerInfoHUD/Scripts/3_Game" };
            };

            class worldScriptModule
            {
                value = "";
                files[] = { "ServerInfoHUD/Scripts/4_World" };
            };

            class missionScriptModule
            {
                value = "";
                files[] = { "ServerInfoHUD/Scripts/5_Mission" };
            };

            class inputs
            {
                value = "";
                files[] = { "ServerInfoHUD/Scripts/data" };
            };
        };
    };
};

3. fájl: ServerInfoHUD/Scripts/data/inputs.xml ​

xml
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<modded_inputs>
    <inputs>
        <actions>
            <input name="UAServerInfoToggle" loc="Toggle Server Info HUD" />
        </actions>
    </inputs>
    <preset>
        <input name="UAServerInfoToggle">
            <btn name="kF7" />
        </input>
    </preset>
</modded_inputs>

4. fájl: ServerInfoHUD/Scripts/3_Game/ServerInfoHUD/ServerInfoRPC.c ​

c
// RPC ID-k a Szerver Infó HUD-hoz.
// Magas számok használata az ütközések elkerüléséhez a vanilla ERPC-kkel és más modokkal.

const int SIH_RPC_REQUEST_INFO = 72810;
const int SIH_RPC_RESPONSE_INFO = 72811;

// Maximum játékos helyek. Nincs GetGame().GetMaxPlayers() natív, ezért
// állítsd ezt úgy, hogy egyezzen a szerver konfigoddal (az indítási paramétereidben lévő érték).
const int SIH_MAX_PLAYERS = 60;

5. fájl: ServerInfoHUD/Scripts/4_World/ServerInfoHUD/ServerInfoServer.c ​

c
modded class PlayerBase
{
    override void OnRPC(
        PlayerIdentity sender,
        int rpc_type,
        ParamsReadContext ctx
    )
    {
        super.OnRPC(sender, rpc_type, ctx);

        // Csak a szerver kezeli ezt az RPC-t
        if (!GetGame().IsServer())
            return;

        if (rpc_type == SIH_RPC_REQUEST_INFO)
        {
            HandleServerInfoRequest(sender);
        }
    }

    protected void HandleServerInfoRequest(PlayerIdentity sender)
    {
        if (!sender)
            return;

        // Szerver név lekérése
        string serverName = "";
        GetGame().GetHostName(serverName);

        // Játékosok számlálása
        ref array<Man> players = new array<Man>();
        GetGame().GetPlayers(players);
        int playerCount = players.Count();

        // Nincs GetGame().GetMaxPlayers(). A beállított helyszám
        // a szerver konfigban él, nem egy scriptből elérhető CGame natívban,
        // ezért itt egy ismert konstanst használunk.
        int maxPlayers = SIH_MAX_PLAYERS;

        // Az adatok visszaküldése a kérő kliensnek
        ScriptRPC rpc = new ScriptRPC();
        rpc.Write(serverName);
        rpc.Write(playerCount);
        rpc.Write(maxPlayers);
        rpc.Send(this, SIH_RPC_RESPONSE_INFO, true, sender);
    }
};

6. fájl: ServerInfoHUD/Scripts/5_Mission/ServerInfoHUD/ServerInfoHUD.c ​

c
class ServerInfoHUD : ScriptedWidgetEventHandler
{
    protected Widget m_Root;
    protected Widget m_Panel;
    protected TextWidget m_ServerNameText;
    protected TextWidget m_PlayerCountText;
    protected TextWidget m_TimeText;

    protected bool m_IsVisible;
    protected float m_UpdateTimer;
    protected ref WidgetFadeTimer m_FadeTimer;

    static const float UPDATE_INTERVAL = 1.0;

    void ServerInfoHUD()
    {
        m_IsVisible = true;
        m_UpdateTimer = 0;
        m_FadeTimer = new WidgetFadeTimer();
    }

    void ~ServerInfoHUD()
    {
        Destroy();
    }

    void Init()
    {
        if (m_Root)
            return;

        m_Root = GetGame().GetWorkspace().CreateWidgets(
            "ServerInfoHUD/GUI/layouts/ServerInfoHUD.layout"
        );

        if (!m_Root)
        {
            Print("[ServerInfoHUD] ERROR: Failed to load layout.");
            return;
        }

        m_Panel = m_Root.FindAnyWidget("ServerInfoPanel");
        m_ServerNameText = TextWidget.Cast(
            m_Root.FindAnyWidget("ServerNameText")
        );
        m_PlayerCountText = TextWidget.Cast(
            m_Root.FindAnyWidget("PlayerCountText")
        );
        m_TimeText = TextWidget.Cast(
            m_Root.FindAnyWidget("TimeText")
        );

        m_Root.Show(true);
        m_IsVisible = true;

        RequestServerInfo();
    }

    void Destroy()
    {
        if (m_Root)
        {
            m_Root.Unlink();
            m_Root = NULL;
        }
    }

    void Update(float timeslice)
    {
        if (!m_Root || !m_IsVisible)
            return;

        m_UpdateTimer += timeslice;

        if (m_UpdateTimer >= UPDATE_INTERVAL)
        {
            m_UpdateTimer = 0;
            RefreshTime();
            RequestServerInfo();
        }
    }

    protected void RefreshTime()
    {
        if (!m_TimeText)
            return;

        int year, month, day, hour, minute;
        GetGame().GetWorld().GetDate(year, month, day, hour, minute);

        string hourStr = hour.ToString();
        string minStr = minute.ToString();

        if (hour < 10)
            hourStr = "0" + hourStr;

        if (minute < 10)
            minStr = "0" + minStr;

        m_TimeText.SetText("Time: " + hourStr + ":" + minStr);
    }

    protected void RequestServerInfo()
    {
        if (!GetGame().IsMultiplayer())
        {
            SetServerName("Offline Mode");
            SetPlayerCount(1, 1);
            return;
        }

        Man player = GetGame().GetPlayer();
        if (!player)
            return;

        ScriptRPC rpc = new ScriptRPC();
        rpc.Send(player, SIH_RPC_REQUEST_INFO, true, NULL);
    }

    void SetServerName(string name)
    {
        if (m_ServerNameText)
            m_ServerNameText.SetText(name);
    }

    void SetPlayerCount(int current, int max)
    {
        if (m_PlayerCountText)
        {
            string text = "Players: " + current.ToString()
                + " / " + max.ToString();
            m_PlayerCountText.SetText(text);
        }
    }

    void ToggleVisibility()
    {
        m_IsVisible = !m_IsVisible;

        if (!m_Root)
            return;

        if (m_IsVisible)
        {
            m_Root.Show(true);
            m_FadeTimer.FadeIn(m_Root, 0.3);
        }
        else
        {
            m_FadeTimer.FadeOut(m_Root, 0.3);
        }
    }

    void SetMenuState(bool menuOpen)
    {
        if (!m_Root)
            return;

        if (menuOpen)
        {
            m_Root.Show(false);
        }
        else if (m_IsVisible)
        {
            m_Root.Show(true);
        }
    }

    bool IsVisible()
    {
        return m_IsVisible;
    }

    Widget GetRoot()
    {
        return m_Root;
    }
};

// -----------------------------------------------
// Kliens oldali RPC fogadó
// -----------------------------------------------
modded class PlayerBase
{
    override void OnRPC(
        PlayerIdentity sender,
        int rpc_type,
        ParamsReadContext ctx
    )
    {
        super.OnRPC(sender, rpc_type, ctx);

        if (GetGame().IsServer())
            return;

        if (rpc_type == SIH_RPC_RESPONSE_INFO)
        {
            HandleServerInfoResponse(ctx);
        }
    }

    protected void HandleServerInfoResponse(ParamsReadContext ctx)
    {
        string serverName;
        int playerCount;
        int maxPlayers;

        if (!ctx.Read(serverName))
            return;
        if (!ctx.Read(playerCount))
            return;
        if (!ctx.Read(maxPlayers))
            return;

        MissionGameplay mission = MissionGameplay.Cast(
            GetGame().GetMission()
        );
        if (!mission)
            return;

        ServerInfoHUD hud = mission.GetServerInfoHUD();
        if (!hud)
            return;

        hud.SetServerName(serverName);
        hud.SetPlayerCount(playerCount, maxPlayers);
    }
};

7. fájl: ServerInfoHUD/Scripts/5_Mission/ServerInfoHUD/MissionHook.c ​

c
modded class MissionGameplay
{
    protected ref ServerInfoHUD m_ServerInfoHUD;

    override void OnInit()
    {
        super.OnInit();

        m_ServerInfoHUD = new ServerInfoHUD();
        m_ServerInfoHUD.Init();
    }

    override void OnMissionFinish()
    {
        if (m_ServerInfoHUD)
        {
            m_ServerInfoHUD.Destroy();
            m_ServerInfoHUD = NULL;
        }

        super.OnMissionFinish();
    }

    override void OnUpdate(float timeslice)
    {
        super.OnUpdate(timeslice);

        if (!m_ServerInfoHUD)
            return;

        // Nyitott menük észlelése
        bool menuOpen = false;
        UIManager uiMgr = GetGame().GetUIManager();
        if (uiMgr)
        {
            UIScriptedMenu topMenu = uiMgr.GetMenu();
            if (topMenu)
                menuOpen = true;
        }

        m_ServerInfoHUD.SetMenuState(menuOpen);
        m_ServerInfoHUD.Update(timeslice);

        // Átkapcsoló billentyű
        if (GetUApi().GetInputByName(
            "UAServerInfoToggle"
        ).LocalPress())
        {
            m_ServerInfoHUD.ToggleVisibility();
        }
    }

    ServerInfoHUD GetServerInfoHUD()
    {
        return m_ServerInfoHUD;
    }
};

8. fájl: ServerInfoHUD/GUI/layouts/ServerInfoHUD.layout ​

FrameWidgetClass ServerInfoRoot {
 position 0 0
 size 1 1
 halign left
 valign top
 hexactpos 0
 vexactpos 0
 hexactsize 0
 vexactsize 0
 {
  ImageWidgetClass ServerInfoPanel {
   position 1 0
   size 220 70
   halign right
   valign top
   hexactpos 0
   vexactpos 1
   hexactsize 1
   vexactsize 1
   color 0 0 0 0.55
   {
    TextWidgetClass ServerNameText {
     position 8 6
     size 204 20
     hexactpos 1
     vexactpos 1
     hexactsize 1
     vexactsize 1
     font "gui/fonts/MetronBook"
     fontsize 14
     text "Server Name"
     color 1 1 1 0.9
    }
    TextWidgetClass PlayerCountText {
     position 8 28
     size 204 18
     hexactpos 1
     vexactpos 1
     hexactsize 1
     vexactsize 1
     font "gui/fonts/MetronBook"
     fontsize 12
     text "Players: - / -"
     color 0.8 0.8 0.8 0.85
    }
    TextWidgetClass TimeText {
     position 8 48
     size 204 18
     hexactpos 1
     vexactpos 1
     hexactsize 1
     vexactsize 1
     font "gui/fonts/MetronBook"
     fontsize 12
     text "Time: --:--"
     color 0.8 0.8 0.8 0.85
    }
   }
  }
 }
}

A HUD bővítése ​

Ha az alap HUD működik, íme természetes bővítések.

FPS kijelzés hozzáadása ​

Az FPS kliens oldalon olvasható bármilyen RPC nélkül:

c
// Adj hozzá egy TextWidget m_FPSText mezőt és keresd meg az Init()-ben

protected void RefreshFPS()
{
    if (!m_FPSText)
        return;

    float fps = GetGame().GetLastFPS();
    m_FPSText.SetText("FPS: " + Math.Round(fps).ToString());
}

Hívd meg a RefreshFPS()-t a RefreshTime() mellett a frissítés metódusban. Vedd figyelembe, hogy a GetLastFPS() az utolsó képkocka képkockasebességét adja vissza, tehát az érték ingadozni fog. Simább megjelenítéshez használd a motor beépített gördülő átlagát:

c
protected void RefreshFPS()
{
    if (!m_FPSText)
        return;

    // A GetAvgFPS a megadott számú legutóbbi képkockán átlagol.
    float avgFPS = GetGame().GetAvgFPS(64);
    m_FPSText.SetText("FPS: " + Math.Round(avgFPS).ToString());
}

Játékos pozíció hozzáadása ​

c
protected void RefreshPosition()
{
    if (!m_PositionText)
        return;

    Man player = GetGame().GetPlayer();
    if (!player)
        return;

    vector pos = player.GetPosition();
    string text = "Pos: " + Math.Round(pos[0]).ToString()
        + " / " + Math.Round(pos[2]).ToString();
    m_PositionText.SetText(text);
}

Több HUD panel ​

Több panelhez (iránytű, állapot, minitérkép) hozz létre egy szülő menedzser osztályt, amely HUD elemek tömbjét tartja:

c
class HUDManager
{
    protected ref array<ref ServerInfoHUD> m_Panels;

    void HUDManager()
    {
        m_Panels = new array<ref ServerInfoHUD>();
    }

    void AddPanel(ServerInfoHUD panel)
    {
        m_Panels.Insert(panel);
    }

    void UpdateAll(float timeslice)
    {
        int count = m_Panels.Count();
        int i = 0;
        while (i < count)
        {
            m_Panels.Get(i).Update(timeslice);
            i++;
        }
    }
};

Húzható HUD elemek ​

Egy widget húzhatóvá tétele egér események kezelését igényli a ScriptedWidgetEventHandler-en keresztül:

c
class DraggableHUD : ScriptedWidgetEventHandler
{
    protected bool m_Dragging;
    protected float m_OffsetX;
    protected float m_OffsetY;
    protected Widget m_DragWidget;

    override bool OnMouseButtonDown(Widget w, int x, int y, int button)
    {
        if (w == m_DragWidget && button == 0)
        {
            m_Dragging = true;
            float wx, wy;
            m_DragWidget.GetScreenPos(wx, wy);
            m_OffsetX = x - wx;
            m_OffsetY = y - wy;
            return true;
        }
        return false;
    }

    override bool OnMouseButtonUp(Widget w, int x, int y, int button)
    {
        if (button == 0)
            m_Dragging = false;
        return false;
    }

    override bool OnDrag(Widget w, int x, int y)
    {
        if (m_Dragging && m_DragWidget)
        {
            m_DragWidget.SetPos(x - m_OffsetX, y - m_OffsetY);
            return true;
        }
        return false;
    }
};

Megjegyzés: a húzás működéséhez a widgeten meg kell hívni a SetHandler(this) metódust, hogy az eseménykezelő megkapja az eseményeket. Ezen kívül a kurzornak láthatónak kell lennie, ami korlátozza a húzható HUD-okat azokra a helyzetekre, amikor egy menü vagy szerkesztési mód aktív.


Gyakori hibák ​

1. Frissítés minden képkockánál korlátozás helyett ​

Helytelen:

c
override void OnUpdate(float timeslice)
{
    super.OnUpdate(timeslice);
    m_ServerInfoHUD.RefreshTime();      // Másodpercenként 60+ alkalommal fut!
    m_ServerInfoHUD.RequestServerInfo(); // Másodpercenként 60+ RPC-t küld!
}

Helyes: Használj időzítő akkumulátort (ahogyan az útmutatóban megmutatva), hogy a drága műveletek legfeljebb másodpercenként egyszer fussanak. A HUD szöveg, ami minden képkockánál változik (mint az FPS számláló) frissülhet képkockánként, de az RPC kéréseket korlátozni kell.

2. Takarítás hiánya az OnMissionFinish-ben ​

Helytelen:

c
modded class MissionGameplay
{
    ref ServerInfoHUD m_HUD;

    override void OnInit()
    {
        super.OnInit();
        m_HUD = new ServerInfoHUD();
        m_HUD.Init();
        // Sehol nincs takarítás -- widget szivárog lekapcsolódáskor!
    }
};

Helyes: Mindig semmisítsd meg a widgeteket és nullázd a referenciákat az OnMissionFinish()-ben. A destruktor (~ServerInfoHUD) egy biztonsági háló, de ne hagyatkozz rá -- az OnMissionFinish a helyes hely az explicit takarításra.

3. HUD más UI elemek mögött ​

A később létrehozott widgetek a korábban létrehozottak tetejére renderelődnek. Ha a HUD-od a vanilla UI mögött jelenik meg, túl korán hozták létre. Megoldások:

  • Hozd létre a HUD-ot később az inicializálási sorrendben (pl. az első OnUpdate híváskor az OnInit helyett).
  • Használd az m_Root.SetSort(100) metódust magasabb rendezési sorrend kényszerítéséhez, ami a widgetedet mások fölé tolja.

4. Túl gyakori adatkérés (RPC spam) ​

RPC küldése minden képkockánál csatlakozott játékosonként másodpercenként 60+ hálózati csomagot hoz létre. Egy 60 játékosos szerveren ez másodpercenként 3600 csomag felesleges forgalom. Mindig korlátozd az RPC kéréseket. Másodpercenként egyszer ésszerű nem kritikus információhoz. Ritkán változó adatokhoz (mint a szerver neve) kérheted csak egyszer az init-kor és gyorsítótárazhatod.

5. A super hívás elfelejtése ​

c
// HELYTELEN: megtöri a vanilla HUD funkcionalitást
override void OnInit()
{
    m_HUD = new ServerInfoHUD();
    m_HUD.Init();
    // Hiányzik a super.OnInit()! A vanilla HUD nem fog inicializálódni.
}

Mindig hívd meg először a super.OnInit()-et (és super.OnUpdate()-et, super.OnMissionFinish()-t). A super hívás kihagyása megtöri a vanilla implementációt és minden más modot, amely ugyanazt a metódust hookolja.

6. Rossz script réteg használata ​

Ha megpróbálod hivatkozni a MissionGameplay-t a 4_World-ből, "Undefined type" hibát kapsz, mert az 5_Mission típusok nem láthatók a 4_World számára. Az RPC konstansok a 3_Game-be tartoznak, a szerver kezelő a 4_World-be (a PlayerBase modolása, ami ott él), a HUD osztály és a misszió hook pedig az 5_Mission-be.

7. Beégetett layout útvonal ​

A layout útvonal a CreateWidgets()-ben a játék keresési útvonalaihoz képest relatív. Ha a PBO prefixed nem egyezik az útvonal stringgel, a layout nem fog betöltődni és a CreateWidgets NULL-t ad vissza. Mindig ellenőrizd NULL-ra a CreateWidgets után és logolj hibát, ha sikertelen.


Következő lépések ​

Most, hogy van egy működő HUD overlayd, fontold meg ezeket a továbblépéseket:

  1. Felhasználói preferenciák mentése -- Tárold, hogy a HUD látható-e egy helyi JSON fájlban, így az átkapcsolási állapot megmarad a munkamenetek között.
  2. Szerver oldali konfiguráció hozzáadása -- Engedd, hogy a szerver adminok engedélyezzék/letiltsák a HUD-ot, vagy válasszák ki, mely mezők jelenjenek meg egy JSON konfigurációs fájlon keresztül.
  3. Admin overlay készítése -- Bővítsd a HUD-ot, hogy csak admin információkat mutasson (szerver teljesítmény, entitásszám, újraindítási időzítő) jogosultsági ellenőrzésekkel.
  4. Iránytű HUD készítése -- Használd a GetGame().GetCurrentCameraDirection() metódust az irány kiszámításához és jeleníts meg egy iránytű sávot a képernyő tetején.
  5. Meglévő modok tanulmányozása -- Nézd meg a DayZ Expansion quest HUD-ját és a Colorful UI overlay rendszerét produkciós minőségű HUD implementációkért.

Legjobb gyakorlatok ​

  • Korlátozd az OnUpdate-et minimum 1 másodperces intervallumokra. Használj időzítő akkumulátort, hogy elkerüld a drága műveletek (RPC kérések, szöveg formázás) másodpercenként 60+ alkalommal futtatását. Csak a képkockánkénti vizuális elemek, mint az FPS számláló frissüljenek minden képkockánál.
  • Rejtsd el a HUD-ot, amikor a felszerelés vagy bármilyen menü nyitva van. Ellenőrizd a GetGame().GetUIManager().GetMenu() metódust minden frissítéskor és rejtsd el az overlayt. Az átfedő UI elemek összezavarják a játékosokat és blokkolják az interakciót.
  • Mindig takaríts el widgeteket az OnMissionFinish-ben. A kiszivárgott widget gyökerek megmaradnak a szerverváltások között, egymásra halmozódott szellem paneleket halmozva, amelyek memóriát fogyasztanak és végül vizuális hibákat okoznak.
  • Használd a SetSort() metódust a renderelési sorrend vezérléséhez. Ha a HUD-od vanilla elemek mögött jelenik meg, hívd meg az m_Root.SetSort(100) metódust, hogy fölé told. Explicit rendezési sorrend nélkül a létrehozási időzítés határozza meg a rétegezést.
  • Gyorsítótárazd a ritkán változó szerver adatokat. A szerver neve nem változik egy munkamenet során. Kérd egyszer az init-kor és tárold helyileg ahelyett, hogy másodpercenként újra kérnéd.

Elmélet vs gyakorlat ​

FogalomElméletValóság
OnUpdate(float timeslice)Képkockánként egyszer hívódik a képkocka delta idővelEgy 144 FPS-es kliensen ez másodpercenként 144-szer aktiválódik. RPC küldése minden híváskor játékosonként másodpercenként 144 hálózati csomagot hoz létre. Mindig gyűjtsd a timeslice-ot és csak akkor cselekedj, ha az összeg meghaladja az intervallumodat.
CreateWidgets() layout útvonalBetölti a layoutot a megadott útvonalrólAz útvonal a PBO prefix-hez képest relatív, nem a fájlrendszerhez. Ha a PBO prefixed nem egyezik az útvonal stringgel, a CreateWidgets csendben NULL-t ad vissza hiba nélkül a logban.
WidgetFadeTimerSimán animálja a widget átlátszóságátA FadeOut elrejti a widgetet, amint az alfa közel nullára ér, a FadeIn pedig meghívja a Show(true) metódust magán a widgeten, mielőtt az alfát 0-ról 1-re animálná. A FadeIn előtti extra m_Root.Show(true) tehát felesleges (ártalmatlan, nem szükséges).
GetUApi().GetInputByName()Visszaadja a beviteli akciót az egyéni billentyűkötésedhezHa az inputs.xml nincs hivatkozva a config.cpp-ben a class inputs alatt, az akció név ismeretlen és a GetInputByName null-t ad vissza, ami összeomlást okoz a .LocalPress() hívásnál.

Amit tanultál ​

Ebben az útmutatóban megtanultad:

  • Hogyan hozz létre HUD layoutot rögzített, félig átlátszó panelekkel
  • Hogyan építs vezérlő osztályt, amely rögzített intervallumra korlátozza a frissítéseket
  • Hogyan csatlakozz a MissionGameplay-be a HUD életciklus kezeléséhez (inicializálás, frissítés, takarítás)
  • Hogyan kérj szerver adatokat RPC-n keresztül és jelenítsd meg a kliensen
  • Hogyan regisztrálj egyéni billentyűkötést az inputs.xml-en keresztül és kapcsold a HUD láthatóságot halványítási animációkkal

Előző: 8.7. fejezet: Publikálás a Steam Workshopra

Released under CC BY-SA 4.0 | Code examples under MIT License