Capitolo 8.8: Costruire un Overlay HUD
Home | << Precedente: Pubblicazione sullo Steam Workshop | Costruire un Overlay HUD | Successivo: Template Professionale per Mod >>
Riepilogo: Questo tutorial ti guida nella costruzione di un overlay HUD personalizzato che mostra informazioni sul server nell'angolo in alto a destra dello schermo. Creerai un file layout, scriverai una classe controller, ti aggancerai al ciclo di vita della missione, richiederai dati dal server tramite RPC, aggiungerai un tasto di attivazione, e rifinrai il risultato con animazioni di dissolvenza e visibilità intelligente. Alla fine, avrai un HUD Informazioni Server discreto che mostra il nome del server, il conteggio giocatori e l'orario di gioco corrente -- oltre a una solida comprensione di come funzionano gli overlay HUD in DayZ.
Indice dei Contenuti
- Cosa Stiamo Costruendo
- Prerequisiti
- Struttura del Mod
- Step 1: Creare il File Layout
- Step 2: Creare la Classe Controller HUD
- Step 3: Agganciarsi a MissionGameplay
- Step 4: Richiedere Dati dal Server
- Step 5: Aggiungere il Toggle con Tasto
- Step 6: Rifinitura
- Riferimento Codice Completo
- Estendere l'HUD
- Errori Comuni
- Prossimi Passi
Cosa Stiamo Costruendo
Un piccolo pannello semi-trasparente ancorato all'angolo in alto a destra dello schermo che mostra tre righe di informazioni:
Aurora Survival [Official]
Players: 24 / 60
Time: 14:352
3
Il pannello si trova sotto gli indicatori di stato e sopra la quickbar. Si aggiorna una volta al secondo (non ad ogni frame), appare con dissolvenza quando viene mostrato e scompare con dissolvenza quando viene nascosto, e si nasconde automaticamente quando l'inventario o il menù di pausa sono aperti. Il giocatore può attivarlo e disattivarlo con un tasto configurabile (predefinito: F7).
Risultato Atteso
Quando caricato, vedrai un rettangolo scuro semi-trasparente nell'area in alto a destra dello schermo. Il testo bianco mostra il nome del server sulla prima riga, il conteggio giocatori corrente sulla seconda riga e l'orario di gioco corrente sulla terza riga. Premendo F7 si dissolve dolcemente; premendo F7 di nuovo riappare con dissolvenza.
Prerequisiti
- Una struttura mod funzionante (completa prima il Capitolo 8.1)
- Comprensione di base della sintassi Enforce Script
- Familiarità con il modello client-server di DayZ (l'HUD gira sul client; il conteggio giocatori viene dal server)
Struttura del Mod
Crea la seguente struttura di directory:
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
Il layer 3_Game definisce le costanti (il nostro ID RPC). Il layer 4_World gestisce la risposta lato server. Il layer 5_Mission contiene la classe HUD e l'hook alla missione. Il file layout definisce l'albero dei widget.
Step 1: Creare il File Layout
I file layout (.layout) definiscono la gerarchia dei widget in XML. Il sistema GUI di DayZ utilizza un modello a coordinate dove ogni widget ha una posizione e una dimensione espresse come valori proporzionali (da 0.0 a 1.0 del genitore) più offset in pixel.
GUI/layouts/ServerInfoHUD.layout
<?xml version="1.0" encoding="UTF-8"?>
<layoutset>
<children>
<!-- Frame radice: copre l'intero schermo, non consuma input -->
<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>
<!-- Pannello di sfondo: angolo in alto a destra -->
<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>
<!-- Testo nome server -->
<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>
<!-- Testo conteggio giocatori -->
<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>
<!-- Testo orario di gioco -->
<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
Concetti Chiave del Layout
| Attributo | Significato |
|---|---|
halign="2" | Allineamento orizzontale: destra. Il widget si ancora al bordo destro del genitore. |
valign="0" | Allineamento verticale: alto. |
hexactpos="0" + vexactpos="1" | La posizione orizzontale è proporzionale (1.0 = bordo destro), la posizione verticale è in pixel. |
hexactsize="1" + vexactsize="1" | Larghezza e altezza sono in pixel (220 x 70). |
color="0 0 0 0.55" | RGBA come float. Nero al 55% di opacità per il pannello di sfondo. |
Il ServerInfoPanel è posizionato a X proporzionale=1.0 (bordo destro) con halign="2" (allineato a destra), quindi il bordo destro del pannello tocca il lato destro dello schermo. La posizione Y è 0 pixel dall'alto. Questo posiziona il nostro HUD nell'angolo in alto a destra.
Perché dimensioni in pixel per il pannello? Il dimensionamento proporzionale farebbe scalare il pannello con la risoluzione, ma per piccoli widget informativi si vuole un'impronta fissa in pixel così il testo resta leggibile a tutte le risoluzioni.
Step 2: Creare la Classe Controller HUD
La classe controller carica il layout, trova i widget per nome ed espone metodi per aggiornare il testo visualizzato. Estende ScriptedWidgetEventHandler così può ricevere eventi dei widget se necessario in futuro.
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;
// Quanto spesso aggiornare i dati visualizzati (secondi)
static const float UPDATE_INTERVAL = 1.0;
void ServerInfoHUD()
{
m_IsVisible = true;
m_UpdateTimer = 0;
}
void ~ServerInfoHUD()
{
Destroy();
}
// Crea e mostra l'HUD
void Init()
{
if (m_Root)
return;
m_Root = GetGame().GetWorkspace().CreateWidgets(
"ServerInfoHUD/GUI/layouts/ServerInfoHUD.layout"
);
if (!m_Root)
{
Print("[ServerInfoHUD] ERRORE: Impossibile caricare il file 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;
// Richiedi i dati iniziali dal server
RequestServerInfo();
}
// Rimuovi tutti i widget
void Destroy()
{
if (m_Root)
{
m_Root.Unlink();
m_Root = NULL;
}
}
// Chiamato ogni frame da 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();
}
}
// Aggiorna la visualizzazione dell'orario di gioco (lato client, nessun RPC necessario)
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);
}
// Invia RPC al server chiedendo conteggio giocatori e nome server
protected void RequestServerInfo()
{
if (!GetGame().IsMultiplayer())
{
// Modalità offline: mostra solo info locali
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);
}
// --- Setter chiamati quando arrivano i dati ---
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);
}
}
// Alterna visibilità
void ToggleVisibility()
{
m_IsVisible = !m_IsVisible;
if (m_Root)
m_Root.Show(m_IsVisible);
}
// Nascondi quando i menù sono aperti
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
Dettagli Importanti
- Percorso
CreateWidgets: Il percorso è relativo alla radice del mod. Poiché impacchettiamo la cartellaGUI/dentro il PBO, il motore risolveServerInfoHUD/GUI/layouts/ServerInfoHUD.layoutusando il prefisso del mod. FindAnyWidget: Cerca nell'albero dei widget ricorsivamente per nome. Controlla sempre per NULL dopo il casting.Widget.Unlink(): Rimuove correttamente il widget e tutti i suoi figli dall'albero UI. Chiamalo sempre durante la pulizia.- Pattern dell'accumulatore di tempo: Aggiungiamo
timeslicead ogni frame e agiamo solo quando il tempo accumulato superaUPDATE_INTERVAL. Questo evita di fare lavoro ad ogni singolo frame.
Step 3: Agganciarsi a MissionGameplay
La classe MissionGameplay è il controller della missione lato client. Usiamo modded class per iniettare il nostro HUD nel suo ciclo di vita senza sostituire il file vanilla.
Scripts/5_Mission/ServerInfoHUD/MissionHook.c
modded class MissionGameplay
{
protected ref ServerInfoHUD m_ServerInfoHUD;
override void OnInit()
{
super.OnInit();
// Crea l'overlay HUD
m_ServerInfoHUD = new ServerInfoHUD();
m_ServerInfoHUD.Init();
}
override void OnMissionFinish()
{
// Pulisci PRIMA di chiamare super
if (m_ServerInfoHUD)
{
m_ServerInfoHUD.Destroy();
m_ServerInfoHUD = NULL;
}
super.OnMissionFinish();
}
override void OnUpdate(float timeslice)
{
super.OnUpdate(timeslice);
if (!m_ServerInfoHUD)
return;
// Nascondi l'HUD quando l'inventario o qualsiasi menù è aperto
UIManager uiMgr = GetGame().GetUIManager();
bool menuOpen = false;
if (uiMgr)
{
UIScriptedMenu topMenu = uiMgr.GetMenu();
if (topMenu)
menuOpen = true;
}
m_ServerInfoHUD.SetMenuState(menuOpen);
// Aggiorna i dati dell'HUD (limitato internamente)
m_ServerInfoHUD.Update(timeslice);
// Controlla il tasto di toggle
Input input = GetGame().GetInput();
if (input)
{
if (GetUApi().GetInputByName("UAServerInfoToggle").LocalPress())
{
m_ServerInfoHUD.ToggleVisibility();
}
}
}
// Accessor così il gestore RPC può raggiungere l'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
Perché Questo Pattern Funziona
OnInitviene eseguito una volta quando il giocatore entra nel gameplay. Creiamo e inizializziamo l'HUD qui.OnUpdateviene eseguito ogni frame. Passiamotimesliceall'HUD, che internamente limita a una volta al secondo. Controlliamo anche la pressione del tasto di toggle e la visibilità dei menù qui.OnMissionFinishviene eseguito quando il giocatore si disconnette o la missione finisce. Distruggiamo i nostri widget qui per prevenire perdite di memoria.
Regola Critica: Pulisci Sempre
Se dimentichi di distruggere i tuoi widget in OnMissionFinish, il root del widget rimarrà nella sessione successiva. Dopo qualche cambio di server, il giocatore si ritroverà con widget fantasma impilati che consumano memoria. Associa sempre Init() con Destroy().
Step 4: Richiedere Dati dal Server
Il conteggio giocatori è noto solo al server. Abbiamo bisogno di un semplice RPC (Remote Procedure Call) andata e ritorno: il client invia una richiesta, il server legge i dati e li rimanda indietro.
Step 4a: Definire l'ID RPC
Gli ID RPC devono essere unici tra tutti i mod. Definiamo il nostro nel layer 3_Game così sia il codice client che server possono riferirlo.
Scripts/3_Game/ServerInfoHUD/ServerInfoRPC.c
// ID RPC per il Server Info HUD.
// Uso di numeri alti per evitare conflitti con vanilla e altri mod.
const int SIH_RPC_REQUEST_INFO = 72810;
const int SIH_RPC_RESPONSE_INFO = 72811;2
3
4
5
Perché 3_Game? Le costanti e gli enum appartengono al layer più basso a cui sia il client che il server possono accedere. Il layer 3_Game si carica prima di 4_World e 5_Mission, quindi entrambi i lati possono vedere questi valori.
Step 4b: Gestore Lato Server
Il server ascolta SIH_RPC_REQUEST_INFO, raccoglie i dati e risponde con 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;
// Raccogliere informazioni del server
string serverName = "";
GetGame().GetHostName(serverName);
int playerCount = 0;
int maxPlayers = 0;
// Ottenere la lista giocatori
ref array<Man> players = new array<Man>();
GetGame().GetPlayers(players);
playerCount = players.Count();
// Massimo giocatori dalla configurazione del server
maxPlayers = GetGame().GetMaxPlayers();
// Inviare la risposta al client richiedente
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
Step 4c: Ricevitore RPC Lato Client
Il client riceve la risposta e aggiorna l'HUD.
Aggiungi questo allo stesso file ServerInfoHUD.c (in fondo, fuori dalla classe), oppure crea un file separato in 5_Mission/ServerInfoHUD/:
Aggiungi quanto segue sotto la classe ServerInfoHUD in 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;
// Accedere all'HUD tramite 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
Come Funziona il Flusso RPC
CLIENT SERVER
| |
|--- SIH_RPC_REQUEST_INFO ----->|
| | legge serverName, playerCount, maxPlayers
|<-- SIH_RPC_RESPONSE_INFO ----|
| |
| aggiorna il testo dell'HUD |2
3
4
5
6
7
Il client invia la richiesta una volta al secondo (limitata dal timer di aggiornamento). Il server risponde con tre valori impacchettati nel contesto RPC. Il client li legge nello stesso ordine in cui sono stati scritti.
Importante: rpc.Write() e ctx.Read() devono usare gli stessi tipi nello stesso ordine. Se il server scrive una string e poi due valori int, il client deve leggere una string e poi due valori int.
Step 5: Aggiungere il Toggle con Tasto
Step 5a: Definire l'Input in inputs.xml
DayZ usa inputs.xml per registrare azioni personalizzate dei tasti. Il file deve essere posizionato in Scripts/data/inputs.xml e referenziato da 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
| Elemento | Scopo |
|---|---|
<actions> | Dichiara l'azione di input per nome. loc è la stringa di visualizzazione mostrata nel menù delle opzioni di binding dei tasti. |
<preset> | Assegna il tasto predefinito. kF7 corrisponde al tasto F7. |
Step 5b: Referenziare inputs.xml in config.cpp
Il tuo config.cpp deve dire al motore dove trovare il file degli input. Aggiungi una voce inputs dentro il blocco 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
Step 5c: Leggere la Pressione del Tasto
Gestiamo già questo nell'hook MissionGameplay dallo Step 3:
if (GetUApi().GetInputByName("UAServerInfoToggle").LocalPress())
{
m_ServerInfoHUD.ToggleVisibility();
}2
3
4
GetUApi() restituisce il singleton dell'API di input. GetInputByName cerca la nostra azione registrata. LocalPress() restituisce true per esattamente un frame quando il tasto viene premuto.
Riferimento Nomi Tasti
Nomi dei tasti comuni per <btn>:
| Nome Tasto | Tasto |
|---|---|
kF1 fino a kF12 | Tasti funzione |
kH, kI, ecc. | Tasti lettera |
kNumpad0 fino a kNumpad9 | Tastierino numerico |
kLControl | Control sinistro |
kLShift | Shift sinistro |
kLAlt | Alt sinistro |
Le combinazioni con modificatori usano l'annidamento:
<input name="UAServerInfoToggle">
<btn name="kLControl">
<btn name="kH" />
</btn>
</input>2
3
4
5
Questo significa "tieni premuto Control sinistro e premi H."
Step 6: Rifinitura
6a: Animazione di Dissolvenza In/Out
DayZ fornisce WidgetFadeTimer per transizioni fluide dell'alfa. Aggiorna la classe ServerInfoHUD per usarlo:
class ServerInfoHUD : ScriptedWidgetEventHandler
{
// ... campi esistenti ...
protected ref WidgetFadeTimer m_FadeTimer;
void ServerInfoHUD()
{
m_IsVisible = true;
m_UpdateTimer = 0;
m_FadeTimer = new WidgetFadeTimer();
}
// Sostituisci il metodo 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);
}
}
// ... resto della classe ...
};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) anima l'alfa del widget da 0 a 1 per la durata specificata in secondi. FadeOut va da 1 a 0 e nasconde il widget quando termina.
6b: Pannello di Sfondo con Alfa
Lo abbiamo già impostato nel layout (color="0 0 0 0.55"), dando un overlay scuro al 55% di opacità. Se vuoi regolare l'alfa a runtime:
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
La funzione ARGB() accetta valori interi da 0 a 255 per alfa, rosso, verde e blu.
6c: Scelte di Font e Colore
DayZ include diversi font che puoi referenziare nei layout:
| Percorso Font | Stile |
|---|---|
gui/fonts/MetronBook | Sans-serif pulito (usato nell'HUD vanilla) |
gui/fonts/MetronMedium | Versione più grassetta di MetronBook |
gui/fonts/Metron | Variante più sottile |
gui/fonts/luxuriousscript | Script decorativo (evitare per l'HUD) |
Per cambiare il colore del testo a runtime:
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: Rispettare le Altre UI
Il nostro MissionHook.c già rileva quando un menù è aperto e chiama SetMenuState(true). Ecco un approccio più accurato che controlla specificatamente l'inventario:
// Nell'override OnUpdate della modded MissionGameplay:
bool menuOpen = false;
UIManager uiMgr = GetGame().GetUIManager();
if (uiMgr)
{
UIScriptedMenu topMenu = uiMgr.GetMenu();
if (topMenu)
menuOpen = true;
}
// Controllare anche se l'inventario è aperto
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
Questo assicura che il tuo HUD si nasconda dietro la schermata dell'inventario, il menù di pausa, la schermata delle opzioni e qualsiasi altro menù scriptato.
Riferimento Codice Completo
Di seguito ogni file nel mod, nella sua forma finale con tutte le rifiniture applicate.
File 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
File 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
File 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
File 4: ServerInfoHUD/Scripts/3_Game/ServerInfoHUD/ServerInfoRPC.c
// ID RPC per il Server Info HUD.
// Usa numeri alti per evitare conflitti con gli ERPC vanilla e altri mod.
const int SIH_RPC_REQUEST_INFO = 72810;
const int SIH_RPC_RESPONSE_INFO = 72811;2
3
4
5
File 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);
// Solo il server gestisce questo RPC
if (!GetGame().IsServer())
return;
if (rpc_type == SIH_RPC_REQUEST_INFO)
{
HandleServerInfoRequest(sender);
}
}
protected void HandleServerInfoRequest(PlayerIdentity sender)
{
if (!sender)
return;
// Ottenere il nome del server
string serverName = "";
GetGame().GetHostName(serverName);
// Contare i giocatori
ref array<Man> players = new array<Man>();
GetGame().GetPlayers(players);
int playerCount = players.Count();
// Ottenere gli slot giocatori massimi
int maxPlayers = GetGame().GetMaxPlayers();
// Inviare i dati al client richiedente
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
File 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] ERRORE: Impossibile caricare il 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;
}
};
// -----------------------------------------------
// Ricevitore RPC lato client
// -----------------------------------------------
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
File 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;
// Rilevare menù aperti
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);
// Tasto di toggle
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
File 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
Estendere l'HUD
Una volta che l'HUD di base funziona, ecco le estensioni naturali.
Aggiungere la Visualizzazione degli FPS
Gli FPS possono essere letti lato client senza alcun RPC:
// Aggiungi un campo TextWidget m_FPSText e trovalo in 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
Chiama RefreshFPS() insieme a RefreshTime() nel metodo di aggiornamento. Nota che GetDeltaT() restituisce il tempo del frame corrente, quindi il valore degli FPS oscillerà. Per una visualizzazione più fluida, calcola la media su diversi frame:
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());
// Resetta ogni secondo (quando il timer principale scatta)
m_FPSAccum = 0;
m_FPSFrames = 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Aggiungere la Posizione del Giocatore
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
Pannelli HUD Multipli
Per pannelli multipli (bussola, stato, minimappa), crea una classe manager genitore che contiene un array di elementi HUD:
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
Elementi HUD Trascinabili
Rendere un widget trascinabile richiede la gestione degli eventi del mouse tramite 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
Nota: affinché il trascinamento funzioni, il widget deve avere SetHandler(this) chiamato su di esso così l'event handler riceve gli eventi. Inoltre, il cursore deve essere visibile, il che limita gli HUD trascinabili a situazioni in cui un menù o una modalità di modifica è attiva.
Errori Comuni
1. Aggiornamento ad Ogni Frame Invece che Limitato
Sbagliato:
override void OnUpdate(float timeslice)
{
super.OnUpdate(timeslice);
m_ServerInfoHUD.RefreshTime(); // Eseguito 60+ volte al secondo!
m_ServerInfoHUD.RequestServerInfo(); // Invia 60+ RPC al secondo!
}2
3
4
5
6
Corretto: Usa un accumulatore di tempo (come mostrato nel tutorial) così le operazioni costose vengono eseguite al massimo una volta al secondo. Il testo dell'HUD che cambia ogni frame (come un contatore FPS) va bene aggiornare per-frame, ma le richieste RPC devono essere limitate.
2. Non Pulire in OnMissionFinish
Sbagliato:
modded class MissionGameplay
{
ref ServerInfoHUD m_HUD;
override void OnInit()
{
super.OnInit();
m_HUD = new ServerInfoHUD();
m_HUD.Init();
// Nessuna pulizia da nessuna parte -- il widget perde memoria alla disconnessione!
}
};2
3
4
5
6
7
8
9
10
11
12
Corretto: Distruggi sempre i widget e annulla i riferimenti in OnMissionFinish(). Il distruttore (~ServerInfoHUD) è una rete di sicurezza, ma non fare affidamento su di esso -- OnMissionFinish è il posto corretto per la pulizia esplicita.
3. HUD Dietro Altri Elementi UI
I widget creati successivamente vengono renderizzati sopra i widget creati prima. Se il tuo HUD appare dietro la UI vanilla, è stato creato troppo presto. Soluzioni:
- Crea l'HUD più tardi nella sequenza di inizializzazione (ad esempio, alla prima chiamata
OnUpdatepiuttosto che inOnInit). - Usa
m_Root.SetSort(100)per forzare un ordine di ordinamento più alto, spingendo il tuo widget sopra gli altri.
4. Richiedere Dati Troppo Frequentemente (Spam RPC)
Inviare un RPC ogni frame crea 60+ pacchetti di rete al secondo per giocatore connesso. Su un server da 60 giocatori, sono 3.600 pacchetti al secondo di traffico non necessario. Limita sempre le richieste RPC. Una volta al secondo è ragionevole per informazioni non critiche. Per dati che cambiano raramente (come il nome del server), potresti richiederli solo una volta all'init e salvarli in cache.
5. Dimenticare la Chiamata super
// SBAGLIATO: rompe la funzionalità dell'HUD vanilla
override void OnInit()
{
m_HUD = new ServerInfoHUD();
m_HUD.Init();
// Manca super.OnInit()! L'HUD vanilla non si inizializzerà.
}2
3
4
5
6
7
Chiama sempre super.OnInit() (e super.OnUpdate(), super.OnMissionFinish()) per primo. Omettere la chiamata super rompe l'implementazione vanilla e ogni altro mod che aggancia lo stesso metodo.
6. Usare il Layer di Script Sbagliato
Se provi a referenziare MissionGameplay da 4_World, otterrai un errore "Undefined type" perché i tipi di 5_Mission non sono visibili a 4_World. Le costanti RPC vanno in 3_Game, il gestore server va in 4_World (moddando PlayerBase che risiede lì), e la classe HUD e l'hook alla missione vanno in 5_Mission.
7. Percorso Layout Hardcoded
Il percorso del layout in CreateWidgets() è relativo ai percorsi di ricerca del gioco. Se il prefisso del PBO non corrisponde alla stringa del percorso, il layout non verrà caricato e CreateWidgets restituirà NULL. Controlla sempre per NULL dopo CreateWidgets e registra un errore se fallisce.
Prossimi Passi
Ora che hai un overlay HUD funzionante, considera queste progressioni:
- Salvare le preferenze utente -- Memorizza se l'HUD è visibile in un file JSON locale così lo stato del toggle persiste tra le sessioni.
- Aggiungere configurazione lato server -- Permetti agli admin del server di abilitare/disabilitare l'HUD o scegliere quali campi mostrare tramite un file di configurazione JSON.
- Costruire un overlay admin -- Espandi l'HUD per mostrare informazioni riservate agli admin (prestazioni del server, conteggio entità, timer di riavvio) usando controlli di permessi.
- Creare un HUD bussola -- Usa
GetGame().GetCurrentCameraDirection()per calcolare la direzione e mostrare una barra bussola nella parte superiore dello schermo. - Studiare i mod esistenti -- Guarda l'HUD missioni di DayZ Expansion e il sistema overlay di Colorful UI per implementazioni HUD di qualità produzione.
Buone Pratiche
- Limita
OnUpdatea intervalli minimi di 1 secondo. Usa un accumulatore di tempo per evitare di eseguire operazioni costose (richieste RPC, formattazione testo) 60+ volte al secondo. Solo gli elementi visivi per-frame come i contatori FPS dovrebbero aggiornarsi ogni frame. - Nascondi l'HUD quando l'inventario o qualsiasi menù è aperto. Controlla
GetGame().GetUIManager().GetMenu()ad ogni aggiornamento e sopprimi il tuo overlay. Elementi UI sovrapposti confondono i giocatori e bloccano l'interazione. - Pulisci sempre i widget in
OnMissionFinish. I root dei widget non distrutti persistono tra i cambi di server, accumulando pannelli fantasma che consumano memoria e alla fine causano glitch visivi. - Usa
SetSort()per controllare l'ordine di rendering. Se il tuo HUD appare dietro gli elementi vanilla, chiamam_Root.SetSort(100)per spingerlo sopra. Senza un ordine di ordinamento esplicito, la tempistica di creazione determina la stratificazione. - Salva in cache i dati del server che cambiano raramente. Il nome del server non cambia durante una sessione. Richiedilo una volta all'init e salvalo localmente in cache invece di ri-richiederlo ogni secondo.
Teoria vs Pratica
| Concetto | Teoria | Realtà |
|---|---|---|
OnUpdate(float timeslice) | Chiamato una volta per frame con il delta time del frame | Su un client a 144 FPS, questo scatta 144 volte al secondo. Inviare un RPC ad ogni chiamata crea 144 pacchetti di rete/secondo per giocatore. Accumula sempre timeslice e agisci solo quando la somma supera il tuo intervallo. |
Percorso layout di CreateWidgets() | Carica il layout dal percorso fornito | Il percorso è relativo al prefisso del PBO, non al file system. Se il prefisso del PBO non corrisponde alla stringa del percorso, CreateWidgets restituisce silenziosamente NULL senza errori nel log. |
WidgetFadeTimer | Anima fluidamente l'opacità del widget | FadeOut nasconde il widget dopo il completamento dell'animazione, ma FadeIn NON chiama Show(true) prima. Devi mostrare manualmente il widget prima di chiamare FadeIn, o non appare nulla. |
GetUApi().GetInputByName() | Restituisce l'azione di input per il tuo keybind personalizzato | Se inputs.xml non è referenziato in config.cpp sotto class inputs, il nome dell'azione è sconosciuto e GetInputByName restituisce null, causando un crash su .LocalPress(). |
Cosa Hai Imparato
In questo tutorial hai imparato:
- Come creare un layout HUD con pannelli ancorati e semi-trasparenti
- Come costruire una classe controller che limita gli aggiornamenti a un intervallo fisso
- Come agganciarsi a
MissionGameplayper la gestione del ciclo di vita dell'HUD (init, aggiornamento, pulizia) - Come richiedere dati dal server tramite RPC e visualizzarli sul client
- Come registrare un keybind personalizzato tramite
inputs.xmle alternare la visibilità dell'HUD con animazioni di dissolvenza
Precedente: Capitolo 8.7: Pubblicazione sullo Steam Workshop
