Kapitola 8.8: Tvorba HUD překryvu
Domů | << Předchozí: Publikování na Steam Workshop | Tvorba HUD překryvu | Další: Profesionální šablona modu >>
Shrnutí: Tento tutoriál vás provede tvorbou HUD (Heads-Up Display) překryvu pro DayZ. Vytvoříte poloprůhledný informační panel ukotvený v pravém horním rohu obrazovky, který zobrazuje název serveru, počet hráčů a herní čas. Panel se automaticky skrývá při otevřeném inventáři a lze ho přepínat klávesovou zkratkou s plynulou animací prolínání.
Obsah
- Co budeme vytvářet
- Předpoklady
- Struktura modu
- Krok 1: Vytvoření souboru rozvržení
- Krok 2: Vytvoření třídy HUD řadiče
- Krok 3: Napojení na MissionGameplay
- Krok 4: Vyžádání dat ze serveru
- Krok 5: Přidání přepínání klávesovou zkratkou
- Krok 6: Doladění
- Kompletní referenční kód
- Rozšíření HUD
- Časté chyby
- Další kroky
Co budeme vytvářet
Malý, poloprůhledný panel ukotvený v pravém horním rohu obrazovky, který zobrazuje tři řádky informací:
Aurora Survival [Official]
Players: 24 / 60
Time: 14:352
3
Panel sedí pod stavovými indikátory a nad rychlou lištou. Aktualizuje se jednou za sekundu (ne každý snímek), plynule se zobrazí při zobrazení a plynule zmizí při skrytí a automaticky se skryje, když je otevřený inventář nebo pauza menu. Hráč ho může zapínat a vypínat konfigurovatelnou klávesou (výchozí: F7).
Očekávaný výsledek
Po načtení uvidíte tmavý poloprůhledný obdélník v pravé horní oblasti obrazovky. Bílý text zobrazuje název serveru na prvním řádku, aktuální počet hráčů na druhém řádku a herní světový čas na třetím řádku. Stisknutí F7 ho plynule zneviditelní; opětovné stisknutí F7 ho plynule vrátí zpět.
Předpoklady
- Funkční struktura modu (nejprve dokončete Kapitolu 8.1)
- Základní pochopení syntaxe Enforce Script
- Znalost modelu klient-server v DayZ (HUD běží na klientu; počet hráčů přichází ze serveru)
Struktura modu
Vytvořte následující adresářový strom:
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.layout2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Vrstva 3_Game definuje konstanty (naše RPC ID). Vrstva 4_World zpracovává odpověď na straně serveru. Vrstva 5_Mission obsahuje třídu HUD a hook mise. Soubor rozvržení definuje strom widgetů.
Krok 1: Vytvoření souboru rozvržení
Soubory rozvržení (.layout) definují hierarchii widgetů v XML. GUI systém DayZ používá souřadnicový model, kde každý widget má pozici a velikost vyjádřenou jako proporcionální hodnoty (0.0 až 1.0 z rodiče) plus pixelové offsety.
GUI/layouts/ServerInfoHUD.layout
<?xml version="1.0" encoding="UTF-8"?>
<layoutset>
<children>
<!-- Kořenový rámec: pokrývá celou obrazovku, nespotřebovává vstup -->
<Widget name="ServerInfoRoot" type="FrameWidgetClass">
<Attribute name="position" value="0 0" />
<Attribute name="size" value="1 1" />
<Attribute name="halign" value="0" />
<Attribute name="valign" value="0" />
<Attribute name="hexactpos" value="0" />
<Attribute name="vexactpos" value="0" />
<Attribute name="hexactsize" value="0" />
<Attribute name="vexactsize" value="0" />
<children>
<!-- Panel pozadí: pravý horní roh -->
<Widget name="ServerInfoPanel" type="ImageWidgetClass">
<Attribute name="position" value="1 0" />
<Attribute name="size" value="220 70" />
<Attribute name="halign" value="2" />
<Attribute name="valign" value="0" />
<Attribute name="hexactpos" value="0" />
<Attribute name="vexactpos" value="1" />
<Attribute name="hexactsize" value="1" />
<Attribute name="vexactsize" value="1" />
<Attribute name="color" value="0 0 0 0.55" />
<children>
<!-- Text názvu serveru -->
<Widget name="ServerNameText" type="TextWidgetClass">
<Attribute name="position" value="8 6" />
<Attribute name="size" value="204 20" />
<Attribute name="hexactpos" value="1" />
<Attribute name="vexactpos" value="1" />
<Attribute name="hexactsize" value="1" />
<Attribute name="vexactsize" value="1" />
<Attribute name="font" value="gui/fonts/MetronBook" />
<Attribute name="fontsize" value="14" />
<Attribute name="text" value="Server Name" />
<Attribute name="color" value="1 1 1 0.9" />
<Attribute name="halign" value="0" />
<Attribute name="valign" value="0" />
</Widget>
<!-- Text počtu hráčů -->
<Widget name="PlayerCountText" type="TextWidgetClass">
<Attribute name="position" value="8 28" />
<Attribute name="size" value="204 18" />
<Attribute name="hexactpos" value="1" />
<Attribute name="vexactpos" value="1" />
<Attribute name="hexactsize" value="1" />
<Attribute name="vexactsize" value="1" />
<Attribute name="font" value="gui/fonts/MetronBook" />
<Attribute name="fontsize" value="12" />
<Attribute name="text" value="Players: - / -" />
<Attribute name="color" value="0.8 0.8 0.8 0.85" />
<Attribute name="halign" value="0" />
<Attribute name="valign" value="0" />
</Widget>
<!-- Text herního času -->
<Widget name="TimeText" type="TextWidgetClass">
<Attribute name="position" value="8 48" />
<Attribute name="size" value="204 18" />
<Attribute name="hexactpos" value="1" />
<Attribute name="vexactpos" value="1" />
<Attribute name="hexactsize" value="1" />
<Attribute name="vexactsize" value="1" />
<Attribute name="font" value="gui/fonts/MetronBook" />
<Attribute name="fontsize" value="12" />
<Attribute name="text" value="Time: --:--" />
<Attribute name="color" value="0.8 0.8 0.8 0.85" />
<Attribute name="halign" value="0" />
<Attribute name="valign" value="0" />
</Widget>
</children>
</Widget>
</children>
</Widget>
</children>
</layoutset>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
Klíčové koncepty rozvržení
| Atribut | Význam |
|---|---|
halign="2" | Horizontální zarovnání: doprava. Widget se kotví k pravému okraji rodiče. |
valign="0" | Vertikální zarovnání: nahoru. |
hexactpos="0" + vexactpos="1" | Horizontální pozice je proporcionální (1.0 = pravý okraj), vertikální pozice je v pixelech. |
hexactsize="1" + vexactsize="1" | Šířka a výška jsou v pixelech (220 x 70). |
color="0 0 0 0.55" | RGBA jako desetinná čísla. Černá na 55% průhlednosti pro panel pozadí. |
ServerInfoPanel je umístěn na proporcionální X=1.0 (pravý okraj) s halign="2" (zarovnání doprava), takže pravý okraj panelu se dotýká pravé strany obrazovky. Pozice Y je 0 pixelů od vrchu. Tím se náš HUD umístí do pravého horního rohu.
Proč pixelové velikosti pro panel? Proporcionální dimenzování by panel škálovalo s rozlišením, ale pro malé informační widgety chcete pevnou pixelovou stopu, aby text zůstal čitelný při všech rozlišeních.
Krok 2: Vytvoření třídy HUD řadiče
Třída řadiče načte rozvržení, najde widgety podle názvu a zpřístupní metody pro aktualizaci zobrazeného textu. Rozšiřuje ScriptedWidgetEventHandler, aby mohla v případě potřeby přijímat události widgetů.
Scripts/5_Mission/ServerInfoHUD/ServerInfoHUD.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;
// Jak často obnovovat zobrazená data (sekundy)
static const float UPDATE_INTERVAL = 1.0;
void ServerInfoHUD()
{
m_IsVisible = true;
m_UpdateTimer = 0;
}
void ~ServerInfoHUD()
{
Destroy();
}
// Vytvoření a zobrazení HUD
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;
// Vyžádání počátečních dat ze serveru
RequestServerInfo();
}
// Odebrání všech widgetů
void Destroy()
{
if (m_Root)
{
m_Root.Unlink();
m_Root = NULL;
}
}
// Voláno každý snímek z MissionGameplay.OnUpdate
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();
}
}
// Aktualizace zobrazení herního času (na straně klienta, bez potřeby RPC)
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);
}
// Odeslání RPC na server s žádostí o počet hráčů a název serveru
protected void RequestServerInfo()
{
if (!GetGame().IsMultiplayer())
{
// Offline režim: jen zobrazit lokální info
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);
}
// --- Settery volané při příchodu dat ---
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);
}
}
// Přepnutí viditelnosti
void ToggleVisibility()
{
m_IsVisible = !m_IsVisible;
if (m_Root)
m_Root.Show(m_IsVisible);
}
// Skrytí při otevřených menu
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;
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
Důležité detaily
- Cesta
CreateWidgets: Cesta je relativní ke kořenu modu. Jelikož balíme složkuGUI/uvnitř PBO, engine vyřešíServerInfoHUD/GUI/layouts/ServerInfoHUD.layoutpomocí prefixu modu. FindAnyWidget: Prohledává strom widgetů rekurzivně podle názvu. Vždy kontrolujte NULL po přetypování.Widget.Unlink(): Správně odebere widget a všechny jeho potomky ze stromu UI. Vždy volejte při úklidu.- Vzor akumulátoru časovače: Přidáváme
timeslicekaždý snímek a jednáme pouze tehdy, když akumulovaný čas překročíUPDATE_INTERVAL. Tím se zabrání provádění práce každý jednotlivý snímek.
Krok 3: Napojení na MissionGameplay
Třída MissionGameplay je řadič mise na straně klienta. Použijeme modded class pro vložení našeho HUD do jejího životního cyklu bez nahrazení vanilkového souboru.
Scripts/5_Mission/ServerInfoHUD/MissionHook.c
modded class MissionGameplay
{
protected ref ServerInfoHUD m_ServerInfoHUD;
override void OnInit()
{
super.OnInit();
// Vytvoření HUD překryvu
m_ServerInfoHUD = new ServerInfoHUD();
m_ServerInfoHUD.Init();
}
override void OnMissionFinish()
{
// Úklid PŘED voláním super
if (m_ServerInfoHUD)
{
m_ServerInfoHUD.Destroy();
m_ServerInfoHUD = NULL;
}
super.OnMissionFinish();
}
override void OnUpdate(float timeslice)
{
super.OnUpdate(timeslice);
if (!m_ServerInfoHUD)
return;
// Skrytí HUD, když je otevřený inventář nebo jakékoli menu
UIManager uiMgr = GetGame().GetUIManager();
bool menuOpen = false;
if (uiMgr)
{
UIScriptedMenu topMenu = uiMgr.GetMenu();
if (topMenu)
menuOpen = true;
}
m_ServerInfoHUD.SetMenuState(menuOpen);
// Aktualizace dat HUD (interně omezená)
m_ServerInfoHUD.Update(timeslice);
// Kontrola klávesy přepnutí
Input input = GetGame().GetInput();
if (input)
{
if (GetUApi().GetInputByName("UAServerInfoToggle").LocalPress())
{
m_ServerInfoHUD.ToggleVisibility();
}
}
}
// Přístupová metoda, aby RPC handler mohl dosáhnout na HUD
ServerInfoHUD GetServerInfoHUD()
{
return m_ServerInfoHUD;
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
Proč tento vzor funguje
OnInitse spustí jednou, když hráč vstoupí do hry. Zde vytváříme a inicializujeme HUD.OnUpdatese spouští každý snímek. Předávámetimeslicedo HUD, který interně omezuje na jednou za sekundu. Také zde kontrolujeme stisk přepínací klávesy a viditelnost menu.OnMissionFinishse spustí, když se hráč odpojí nebo mise skončí. Zde ničíme naše widgety, abychom zabránili únikům paměti.
Kritické pravidlo: Vždy ukliďte
Pokud zapomenete zničit vaše widgety v OnMissionFinish, kořen widgetu unikne do další relace. Po několika přepnutích serverů hráč skončí se naskládanými duchy widgetů spotřebovávajícími paměť. Vždy párujte Init() s Destroy().
Krok 4: Vyžádání dat ze serveru
Počet hráčů je znám pouze na serveru. Potřebujeme jednoduchý RPC (Remote Procedure Call) obousměrný přenos: klient odešle požadavek, server přečte data a odešle je zpět.
Krok 4a: Definování RPC ID
RPC ID musí být unikátní napříč všemi mody. Definujeme naše ve vrstvě 3_Game, aby na ně mohl odkazovat jak klientský, tak serverový kód.
Scripts/3_Game/ServerInfoHUD/ServerInfoRPC.c
// RPC ID pro Server Info HUD.
// Použití vysokých čísel pro zamezení konfliktů s vanilkou a jinými mody.
const int SIH_RPC_REQUEST_INFO = 72810;
const int SIH_RPC_RESPONSE_INFO = 72811;2
3
4
5
Proč 3_Game? Konstanty a enumy patří do nejnižší vrstvy, ke které mají přístup jak klient, tak server. Vrstva 3_Game se načítá před 4_World a 5_Mission, takže obě strany vidí tyto hodnoty.
Krok 4b: Handler na straně serveru
Server naslouchá SIH_RPC_REQUEST_INFO, shromáždí data a odpoví SIH_RPC_RESPONSE_INFO.
Scripts/4_World/ServerInfoHUD/ServerInfoServer.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;
// Shromáždění informací o serveru
string serverName = "";
GetGame().GetHostName(serverName);
int playerCount = 0;
int maxPlayers = 0;
// Získání seznamu hráčů
ref array<Man> players = new array<Man>();
GetGame().GetPlayers(players);
playerCount = players.Count();
// Maximum hráčů z konfigurace serveru
maxPlayers = GetGame().GetMaxPlayers();
// Odeslání odpovědi zpět žádajícímu klientovi
ScriptRPC rpc = new ScriptRPC();
rpc.Write(serverName);
rpc.Write(playerCount);
rpc.Write(maxPlayers);
rpc.Send(this, SIH_RPC_RESPONSE_INFO, true, sender);
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Krok 4c: Přijímač RPC na straně klienta
Klient přijme odpověď a aktualizuje HUD.
Přidejte toto do stejného souboru ServerInfoHUD.c (na konec, mimo třídu), nebo vytvořte samostatný soubor v 5_Mission/ServerInfoHUD/:
Přidejte následující pod třídu ServerInfoHUD v ServerInfoHUD.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;
// Přístup k HUD přes MissionGameplay
MissionGameplay mission = MissionGameplay.Cast(
GetGame().GetMission()
);
if (!mission)
return;
ServerInfoHUD hud = mission.GetServerInfoHUD();
if (!hud)
return;
hud.SetServerName(serverName);
hud.SetPlayerCount(playerCount, maxPlayers);
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Jak funguje tok RPC
KLIENT SERVER
| |
|--- SIH_RPC_REQUEST_INFO ----->|
| | čte serverName, playerCount, maxPlayers
|<-- SIH_RPC_RESPONSE_INFO ----|
| |
| aktualizuje text HUD |2
3
4
5
6
7
Klient odešle požadavek jednou za sekundu (omezeno aktualizačním časovačem). Server odpoví třemi hodnotami zabalenými do kontextu RPC. Klient je čte ve stejném pořadí, v jakém byly zapsány.
Důležité: rpc.Write() a ctx.Read() musí používat stejné typy ve stejném pořadí. Pokud server zapíše string a poté dvě hodnoty int, klient musí přečíst string a poté dvě hodnoty int.
Krok 5: Přidání přepínání klávesovou zkratkou
Krok 5a: Definování vstupu v inputs.xml
DayZ používá inputs.xml pro registraci vlastních klávesových akcí. Soubor musí být umístěn v Scripts/data/inputs.xml a odkazován z config.cpp.
Scripts/data/inputs.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>2
3
4
5
6
7
8
9
10
11
12
13
| Prvek | Účel |
|---|---|
<actions> | Deklaruje vstupní akci podle názvu. loc je zobrazovaný řetězec uvedený v menu nastavení klávesových zkratek. |
<preset> | Přiřazuje výchozí klávesu. kF7 mapuje na klávesu F7. |
Krok 5b: Odkaz na inputs.xml v config.cpp
Váš config.cpp musí říci enginu, kde najít soubor vstupů. Přidejte záznam inputs dovnitř bloku defs:
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" };
};
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Krok 5c: Čtení stisku klávesy
Toto již zpracováváme v hooku MissionGameplay z kroku 3:
if (GetUApi().GetInputByName("UAServerInfoToggle").LocalPress())
{
m_ServerInfoHUD.ToggleVisibility();
}2
3
4
GetUApi() vrací singleton vstupního API. GetInputByName vyhledá naši registrovanou akci. LocalPress() vrací true přesně pro jeden snímek při stisknutí klávesy.
Referenční seznam názvů kláves
Běžné názvy kláves pro <btn>:
| Název klávesy | Klávesa |
|---|---|
kF1 až kF12 | Funkční klávesy |
kH, kI atd. | Písmenkové klávesy |
kNumpad0 až kNumpad9 | Číselná klávesnice |
kLControl | Levý Control |
kLShift | Levý Shift |
kLAlt | Levý Alt |
Kombinace modifikátorů používají vnořování:
<input name="UAServerInfoToggle">
<btn name="kLControl">
<btn name="kH" />
</btn>
</input>2
3
4
5
To znamená "podržte levý Control a stiskněte H."
Krok 6: Doladění
6a: Animace zobrazení/skrytí
DayZ poskytuje WidgetFadeTimer pro plynulé přechody průhlednosti. Aktualizujte třídu ServerInfoHUD pro jeho použití:
class ServerInfoHUD : ScriptedWidgetEventHandler
{
// ... existující pole ...
protected ref WidgetFadeTimer m_FadeTimer;
void ServerInfoHUD()
{
m_IsVisible = true;
m_UpdateTimer = 0;
m_FadeTimer = new WidgetFadeTimer();
}
// Nahrazení metody ToggleVisibility:
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);
}
}
// ... zbytek třídy ...
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
FadeIn(widget, duration) animuje průhlednost widgetu od 0 do 1 po danou dobu v sekundách. FadeOut jde od 1 do 0 a po dokončení widget skryje.
6b: Panel pozadí s průhledností
Toto jsme již nastavili v rozvržení (color="0 0 0 0.55"), což dává tmavý překryv na 55% průhlednosti. Pokud chcete upravit průhlednost za běhu:
void SetBackgroundAlpha(float alpha)
{
if (m_Panel)
{
int color = ARGB(
(int)(alpha * 255),
0, 0, 0
);
m_Panel.SetColor(color);
}
}2
3
4
5
6
7
8
9
10
11
Funkce ARGB() přijímá celočíselné hodnoty 0-255 pro průhlednost, červenou, zelenou a modrou.
6c: Výběr fontu a barev
DayZ obsahuje několik fontů, na které můžete odkazovat v rozvrženích:
| Cesta fontu | Styl |
|---|---|
gui/fonts/MetronBook | Čistý sans-serif (používaný ve vanilkovém HUD) |
gui/fonts/MetronMedium | Tučnější verze MetronBook |
gui/fonts/Metron | Nejtenčí varianta |
gui/fonts/luxuriousscript | Dekorativní skript (vyhněte se pro HUD) |
Pro změnu barvy textu za běhu:
void SetTextColor(TextWidget widget, int r, int g, int b, int a)
{
if (widget)
widget.SetColor(ARGB(a, r, g, b));
}2
3
4
5
6d: Respektování ostatního UI
Náš MissionHook.c již detekuje otevřené menu a volá SetMenuState(true). Zde je důkladnější přístup, který kontroluje specificky inventář:
// V override OnUpdate moddované MissionGameplay:
bool menuOpen = false;
UIManager uiMgr = GetGame().GetUIManager();
if (uiMgr)
{
UIScriptedMenu topMenu = uiMgr.GetMenu();
if (topMenu)
menuOpen = true;
}
// Také kontrola, zda je otevřený inventář
if (uiMgr && uiMgr.FindMenu(MENU_INVENTORY))
menuOpen = true;
m_ServerInfoHUD.SetMenuState(menuOpen);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Tím zajistíte, že se váš HUD skryje za obrazovkou inventáře, menu pauzy, obrazovkou nastavení a jakýmkoli dalším skriptovaným menu.
Kompletní referenční kód
Níže je každý soubor v modu ve své finální podobě se všemi vylepšeními.
Soubor 1: ServerInfoHUD/mod.cpp
name = "Server Info HUD";
author = "YourName";
version = "1.0";
overview = "Displays server name, player count, and in-game time.";2
3
4
Soubor 2: ServerInfoHUD/Scripts/config.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" };
};
};
};
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
Soubor 3: ServerInfoHUD/Scripts/data/inputs.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>2
3
4
5
6
7
8
9
10
11
12
13
Soubor 4: ServerInfoHUD/Scripts/3_Game/ServerInfoHUD/ServerInfoRPC.c
// RPC ID pro Server Info HUD.
// Použití vysokých čísel pro zamezení kolizí s vanilkovými ERPC a jinými mody.
const int SIH_RPC_REQUEST_INFO = 72810;
const int SIH_RPC_RESPONSE_INFO = 72811;2
3
4
5
Soubor 5: ServerInfoHUD/Scripts/4_World/ServerInfoHUD/ServerInfoServer.c
modded class PlayerBase
{
override void OnRPC(
PlayerIdentity sender,
int rpc_type,
ParamsReadContext ctx
)
{
super.OnRPC(sender, rpc_type, ctx);
// Pouze server zpracovává toto RPC
if (!GetGame().IsServer())
return;
if (rpc_type == SIH_RPC_REQUEST_INFO)
{
HandleServerInfoRequest(sender);
}
}
protected void HandleServerInfoRequest(PlayerIdentity sender)
{
if (!sender)
return;
// Získání názvu serveru
string serverName = "";
GetGame().GetHostName(serverName);
// Spočítání hráčů
ref array<Man> players = new array<Man>();
GetGame().GetPlayers(players);
int playerCount = players.Count();
// Získání maximálního počtu slotů hráčů
int maxPlayers = GetGame().GetMaxPlayers();
// Odeslání dat zpět žádajícímu klientovi
ScriptRPC rpc = new ScriptRPC();
rpc.Write(serverName);
rpc.Write(playerCount);
rpc.Write(maxPlayers);
rpc.Send(this, SIH_RPC_RESPONSE_INFO, true, sender);
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Soubor 6: ServerInfoHUD/Scripts/5_Mission/ServerInfoHUD/ServerInfoHUD.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;
}
};
// -----------------------------------------------
// Přijímač RPC na straně klienta
// -----------------------------------------------
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);
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
Soubor 7: ServerInfoHUD/Scripts/5_Mission/ServerInfoHUD/MissionHook.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;
// Detekce otevřených menu
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);
// Přepínací klávesa
if (GetUApi().GetInputByName(
"UAServerInfoToggle"
).LocalPress())
{
m_ServerInfoHUD.ToggleVisibility();
}
}
ServerInfoHUD GetServerInfoHUD()
{
return m_ServerInfoHUD;
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
Soubor 8: ServerInfoHUD/GUI/layouts/ServerInfoHUD.layout
<?xml version="1.0" encoding="UTF-8"?>
<layoutset>
<children>
<Widget name="ServerInfoRoot" type="FrameWidgetClass">
<Attribute name="position" value="0 0" />
<Attribute name="size" value="1 1" />
<Attribute name="halign" value="0" />
<Attribute name="valign" value="0" />
<Attribute name="hexactpos" value="0" />
<Attribute name="vexactpos" value="0" />
<Attribute name="hexactsize" value="0" />
<Attribute name="vexactsize" value="0" />
<children>
<Widget name="ServerInfoPanel" type="ImageWidgetClass">
<Attribute name="position" value="1 0" />
<Attribute name="size" value="220 70" />
<Attribute name="halign" value="2" />
<Attribute name="valign" value="0" />
<Attribute name="hexactpos" value="0" />
<Attribute name="vexactpos" value="1" />
<Attribute name="hexactsize" value="1" />
<Attribute name="vexactsize" value="1" />
<Attribute name="color" value="0 0 0 0.55" />
<children>
<Widget name="ServerNameText" type="TextWidgetClass">
<Attribute name="position" value="8 6" />
<Attribute name="size" value="204 20" />
<Attribute name="hexactpos" value="1" />
<Attribute name="vexactpos" value="1" />
<Attribute name="hexactsize" value="1" />
<Attribute name="vexactsize" value="1" />
<Attribute name="font" value="gui/fonts/MetronBook" />
<Attribute name="fontsize" value="14" />
<Attribute name="text" value="Server Name" />
<Attribute name="color" value="1 1 1 0.9" />
</Widget>
<Widget name="PlayerCountText" type="TextWidgetClass">
<Attribute name="position" value="8 28" />
<Attribute name="size" value="204 18" />
<Attribute name="hexactpos" value="1" />
<Attribute name="vexactpos" value="1" />
<Attribute name="hexactsize" value="1" />
<Attribute name="vexactsize" value="1" />
<Attribute name="font" value="gui/fonts/MetronBook" />
<Attribute name="fontsize" value="12" />
<Attribute name="text" value="Players: - / -" />
<Attribute name="color" value="0.8 0.8 0.8 0.85" />
</Widget>
<Widget name="TimeText" type="TextWidgetClass">
<Attribute name="position" value="8 48" />
<Attribute name="size" value="204 18" />
<Attribute name="hexactpos" value="1" />
<Attribute name="vexactpos" value="1" />
<Attribute name="hexactsize" value="1" />
<Attribute name="vexactsize" value="1" />
<Attribute name="font" value="gui/fonts/MetronBook" />
<Attribute name="fontsize" value="12" />
<Attribute name="text" value="Time: --:--" />
<Attribute name="color" value="0.8 0.8 0.8 0.85" />
</Widget>
</children>
</Widget>
</children>
</Widget>
</children>
</layoutset>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
Rozšíření HUD
Jakmile máte základní HUD funkční, zde jsou přirozená rozšíření.
Přidání zobrazení FPS
FPS lze číst na straně klienta bez jakéhokoli RPC:
// Přidejte pole TextWidget m_FPSText a najděte ho v Init()
protected void RefreshFPS()
{
if (!m_FPSText)
return;
float fps = 1.0 / GetGame().GetDeltaT();
m_FPSText.SetText("FPS: " + Math.Round(fps).ToString());
}2
3
4
5
6
7
8
9
10
Volejte RefreshFPS() spolu s RefreshTime() v aktualizační metodě. Poznámka: GetDeltaT() vrací čas aktuálního snímku, takže hodnota FPS bude kolísat. Pro hladší zobrazení průměrujte přes několik snímků:
protected float m_FPSAccum;
protected int m_FPSFrames;
protected void RefreshFPS()
{
if (!m_FPSText)
return;
m_FPSAccum += GetGame().GetDeltaT();
m_FPSFrames++;
float avgFPS = m_FPSFrames / m_FPSAccum;
m_FPSText.SetText("FPS: " + Math.Round(avgFPS).ToString());
// Reset každou sekundu (když se spustí hlavní časovač)
m_FPSAccum = 0;
m_FPSFrames = 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Přidání pozice hráče
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);
}2
3
4
5
6
7
8
9
10
11
12
13
14
Více HUD panelů
Pro více panelů (kompas, stav, minimapa) vytvořte nadřazenou třídu manažera, která drží pole HUD prvků:
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++;
}
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Přetahovatelné HUD prvky
Zpřístupnění widgetu pro přetahování vyžaduje zpracování událostí myši přes ScriptedWidgetEventHandler:
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 OnUpdate(Widget w, int x, int y, int oldX, int oldY)
{
if (m_Dragging && m_DragWidget)
{
m_DragWidget.SetPos(x - m_OffsetX, y - m_OffsetY);
return true;
}
return false;
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Poznámka: aby přetahování fungovalo, widget musí mít voláno SetHandler(this), aby event handler přijímal události. Také kurzor musí být viditelný, což omezuje přetahovatelné HUD na situace, kdy je aktivní menu nebo editační režim.
Časté chyby
1. Aktualizace každý snímek místo omezené
Špatně:
override void OnUpdate(float timeslice)
{
super.OnUpdate(timeslice);
m_ServerInfoHUD.RefreshTime(); // Běží 60+ krát za sekundu!
m_ServerInfoHUD.RequestServerInfo(); // Odesílá 60+ RPC za sekundu!
}2
3
4
5
6
Správně: Použijte akumulátor časovače (jak je ukázáno v tutoriálu), aby nákladné operace běžely maximálně jednou za sekundu. HUD text, který se mění každý snímek (jako počítadlo FPS), je v pořádku aktualizovat každý snímek, ale požadavky RPC musí být omezeny.
2. Neúklid v OnMissionFinish
Špatně:
modded class MissionGameplay
{
ref ServerInfoHUD m_HUD;
override void OnInit()
{
super.OnInit();
m_HUD = new ServerInfoHUD();
m_HUD.Init();
// Žádný úklid nikde -- widget uniká při odpojení!
}
};2
3
4
5
6
7
8
9
10
11
12
Správně: Vždy zničte widgety a vynulujte reference v OnMissionFinish(). Destruktor (~ServerInfoHUD) je záchranná síť, ale nespoléhejte se na něj -- OnMissionFinish je správné místo pro explicitní úklid.
3. HUD za ostatními UI prvky
Widgety vytvořené později se vykreslují nad widgety vytvořenými dříve. Pokud se váš HUD zobrazuje za vanilkovým UI, byl vytvořen příliš brzy. Řešení:
- Vytvořte HUD později v inicializační sekvenci (např. při prvním volání
OnUpdatemísto vOnInit). - Použijte
m_Root.SetSort(100)pro vynucení vyššího pořadí řazení, čímž se váš widget posune nad ostatní.
4. Příliš časté vyžadování dat (RPC spam)
Odesílání RPC každý snímek vytváří 60+ síťových paketů za sekundu na připojeného hráče. Na serveru s 60 hráči je to 3 600 paketů za sekundu zbytečného provozu. Vždy omezujte požadavky RPC. Jednou za sekundu je rozumné pro nekritické informace. Pro data, která se zřídka mění (jako název serveru), můžete požádat pouze jednou při inicializaci a uložit do cache.
5. Zapomenutí volání super
// ŠPATNĚ: rozbije vanilkovou funkčnost HUD
override void OnInit()
{
m_HUD = new ServerInfoHUD();
m_HUD.Init();
// Chybí super.OnInit()! Vanilkový HUD se neinicializuje.
}2
3
4
5
6
7
Vždy volejte super.OnInit() (a super.OnUpdate(), super.OnMissionFinish()) jako první. Vynechání volání super rozbije vanilkovou implementaci a každý další mod, který hookuje stejnou metodu.
6. Použití špatné skriptové vrstvy
Pokud se pokusíte odkazovat na MissionGameplay z 4_World, dostanete chybu "Undefined type", protože typy 5_Mission nejsou viditelné pro 4_World. RPC konstanty jdou do 3_Game, handler serveru jde do 4_World (moddování PlayerBase, který tam žije) a třída HUD a hook mise jdou do 5_Mission.
7. Hardkódovaná cesta rozvržení
Cesta rozvržení v CreateWidgets() je relativní k vyhledávacím cestám hry. Pokud prefix vašeho PBO neodpovídá řetězci cesty, rozvržení se nenačte a CreateWidgets vrátí NULL. Vždy kontrolujte NULL po CreateWidgets a logujte chybu, pokud selže.
Další kroky
Nyní, když máte funkční HUD překryv, zvažte tato pokročení:
- Uložení uživatelských předvoleb -- Uložte, zda je HUD viditelný, do lokálního JSON souboru, aby stav přepnutí přetrval mezi relacemi.
- Přidání konfigurace na straně serveru -- Umožněte administrátorům serveru zapnout/vypnout HUD nebo vybrat, která pole zobrazovat přes JSON konfigurační soubor.
- Sestavení administrátorského překryvu -- Rozšiřte HUD o zobrazení informací pouze pro administrátory (výkon serveru, počet entit, časovač restartu) pomocí kontrol oprávnění.
- Vytvoření HUD kompasu -- Použijte
GetGame().GetCurrentCameraDirection()pro výpočet směru a zobrazte lištu kompasu v horní části obrazovky. - Studium existujících modů -- Podívejte se na quest HUD DayZ Expansion a překryvový systém Colorful UI pro produkčně kvalitní implementace HUD.
Doporučené postupy
- Omezte
OnUpdatena minimálně 1-sekundové intervaly. Použijte akumulátor časovače, abyste se vyhnuli spouštění nákladných operací (požadavky RPC, formátování textu) 60+ krát za sekundu. Pouze vizuály každý snímek jako počítadla FPS by se měly aktualizovat každý snímek. - Skryjte HUD, když je otevřený inventář nebo jakékoli menu. Kontrolujte
GetGame().GetUIManager().GetMenu()při každé aktualizaci a potlačte váš překryv. Překrývající se UI prvky matou hráče a blokují interakci. - Vždy ukliďte widgety v
OnMissionFinish. Neuklizenékořeny widgetů přetrvávají mezi přepnutími serverů, hromadí duchové panely, které spotřebovávají paměť a nakonec způsobují vizuální závady. - Použijte
SetSort()pro řízení pořadí vykreslování. Pokud se váš HUD zobrazuje za vanilkovými prvky, volejtem_Root.SetSort(100)pro jeho posunutí nad. Bez explicitního pořadí řazení o vrstvení rozhoduje načasování vytvoření. - Cachujte serverová data, která se zřídka mění. Název serveru se během relace nemění. Vyžádejte ho jednou při inicializaci a uložte lokálně do cache místo opětovného vyžádání každou sekundu.
Teorie vs praxe
| Koncept | Teorie | Realita |
|---|---|---|
OnUpdate(float timeslice) | Voláno jednou za snímek s delta časem snímku | Na klientu se 144 FPS se toto spustí 144krát za sekundu. Odeslání RPC při každém volání vytváří 144 síťových paketů/sekundu na hráče. Vždy akumulujte timeslice a jednejte pouze tehdy, když součet překročí váš interval. |
Cesta rozvržení CreateWidgets() | Načte rozvržení z cesty, kterou poskytnete | Cesta je relativní k PBO prefixu, nikoliv k souborovému systému. Pokud PBO prefix neodpovídá řetězci cesty, CreateWidgets tiše vrátí NULL bez chyby v logu. |
WidgetFadeTimer | Plynule animuje průhlednost widgetu | FadeOut skryje widget po dokončení animace, ale FadeIn NEVOLÁ Show(true) jako první. Musíte widget ručně zobrazit před voláním FadeIn, jinak se nic neobjeví. |
GetUApi().GetInputByName() | Vrací vstupní akci pro vaši vlastní klávesovou zkratku | Pokud inputs.xml není odkázán v config.cpp pod class inputs, název akce je neznámý a GetInputByName vrátí null, což způsobí pád při volání .LocalPress(). |
Co jste se naučili
V tomto tutoriálu jste se naučili:
- Jak vytvořit rozvržení HUD s ukotvenými, poloprůhlednými panely
- Jak sestavit třídu řadiče, která omezuje aktualizace na pevný interval
- Jak se napojit na
MissionGameplaypro správu životního cyklu HUD (inicializace, aktualizace, úklid) - Jak vyžádat serverová data přes RPC a zobrazit je na klientu
- Jak zaregistrovat vlastní klávesovou zkratku přes
inputs.xmla přepínat viditelnost HUD s animacemi prolínání
Předchozí: Kapitola 8.7: Publikování na Steam Workshop
