Chapitre 8.8 : Construire une surcouche HUD
Accueil | << Précédent : Publier sur le Steam Workshop | Construire une surcouche HUD | Suivant : Modèle de mod professionnel >>
Résumé : Ce tutoriel vous guide à travers la construction d'une surcouche HUD personnalisée qui affiche les informations du serveur dans le coin supérieur droit de l'écran. Vous créerez un fichier de disposition, écrirez une classe contrôleur, vous connecterez au cycle de vie de la mission, demanderez des données au serveur via RPC, ajouterez une touche de bascule, et peaufinerez le résultat avec des animations de fondu et une visibilité intelligente. À la fin, vous aurez un HUD d'informations serveur non intrusif affichant le nom du serveur, le nombre de joueurs et l'heure en jeu -- plus une solide compréhension du fonctionnement des surcouches HUD dans DayZ.
Table des matières
- Ce que nous construisons
- Prérequis
- Structure du mod
- Étape 1 : Créer le fichier de disposition
- Étape 2 : Créer la classe contrôleur du HUD
- Étape 3 : Se connecter à MissionGameplay
- Étape 4 : Demander des données au serveur
- Étape 5 : Ajouter une bascule avec raccourci clavier
- Étape 6 : Finitions
- Référence complète du code
- Étendre le HUD
- Erreurs courantes
- Prochaines étapes
Ce que nous construisons
Un petit panneau semi-transparent ancré dans le coin supérieur droit de l'écran qui affiche trois lignes d'informations :
Aurora Survival [Official]
Players: 24 / 60
Time: 14:352
3
Le panneau se situe sous les indicateurs de statut et au-dessus de la barre rapide. Il se met à jour une fois par seconde (pas à chaque frame), apparaît en fondu quand il est affiché et disparaît en fondu quand il est masqué, et se cache automatiquement quand l'inventaire ou le menu pause est ouvert. Le joueur peut le basculer avec une touche configurable (par défaut : F7).
Résultat attendu
Une fois chargé, vous verrez un rectangle semi-transparent foncé dans la zone supérieure droite de l'écran. Un texte blanc affiche le nom du serveur sur la première ligne, le nombre actuel de joueurs sur la deuxième ligne, et l'heure du monde en jeu sur la troisième ligne. Appuyer sur F7 le fait disparaître en fondu ; appuyer à nouveau sur F7 le fait réapparaître en fondu.
Prérequis
- Une structure de mod fonctionnelle (complétez d'abord le Chapitre 8.1)
- Compréhension de base de la syntaxe Enforce Script
- Familiarité avec le modèle client-serveur de DayZ (le HUD s'exécute sur le client ; le nombre de joueurs vient du serveur)
Structure du mod
Créez l'arborescence de répertoires suivante :
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
La couche 3_Game définit les constantes (notre identifiant RPC). La couche 4_World gère la réponse côté serveur. La couche 5_Mission contient la classe HUD et le hook de mission. Le fichier de disposition définit l'arbre de widgets.
Étape 1 : Créer le fichier de disposition
Les fichiers de disposition (.layout) définissent la hiérarchie des widgets en XML. Le système GUI de DayZ utilise un modèle de coordonnées où chaque widget a une position et une taille exprimées en valeurs proportionnelles (0.0 à 1.0 du parent) plus des décalages en pixels.
GUI/layouts/ServerInfoHUD.layout
<?xml version="1.0" encoding="UTF-8"?>
<layoutset>
<children>
<!-- Cadre racine : couvre tout l'écran, ne consomme pas les entrées -->
<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>
<!-- Panneau de fond : coin supérieur droit -->
<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>
<!-- Texte du nom du serveur -->
<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>
<!-- Texte du nombre de joueurs -->
<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>
<!-- Texte de l'heure en jeu -->
<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
Concepts clés de la disposition
| Attribut | Signification |
|---|---|
halign="2" | Alignement horizontal : droite. Le widget s'ancre au bord droit de son parent. |
valign="0" | Alignement vertical : haut. |
hexactpos="0" + vexactpos="1" | La position horizontale est proportionnelle (1.0 = bord droit), la position verticale est en pixels. |
hexactsize="1" + vexactsize="1" | La largeur et la hauteur sont en pixels (220 x 70). |
color="0 0 0 0.55" | RGBA en flottants. Noir à 55% d'opacité pour le panneau de fond. |
Le ServerInfoPanel est positionné à X proportionnel=1.0 (bord droit) avec halign="2" (aligné à droite), donc le bord droit du panneau touche le côté droit de l'écran. La position Y est à 0 pixels du haut. Cela place notre HUD dans le coin supérieur droit.
Pourquoi des tailles en pixels pour le panneau ? Le dimensionnement proportionnel ferait que le panneau s'adapte avec la résolution, mais pour les petits widgets d'information vous voulez une empreinte fixe en pixels pour que le texte reste lisible à toutes les résolutions.
Étape 2 : Créer la classe contrôleur du HUD
La classe contrôleur charge la disposition, trouve les widgets par nom et expose des méthodes pour mettre à jour le texte affiché. Elle étend ScriptedWidgetEventHandler pour pouvoir recevoir des événements de widgets si nécessaire ultérieurement.
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;
// Fréquence de rafraîchissement des données affichées (secondes)
static const float UPDATE_INTERVAL = 1.0;
void ServerInfoHUD()
{
m_IsVisible = true;
m_UpdateTimer = 0;
}
void ~ServerInfoHUD()
{
Destroy();
}
// Créer et afficher le 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;
// Demander les données initiales au serveur
RequestServerInfo();
}
// Supprimer tous les widgets
void Destroy()
{
if (m_Root)
{
m_Root.Unlink();
m_Root = NULL;
}
}
// Appelé chaque frame depuis 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();
}
}
// Mettre à jour l'affichage de l'heure en jeu (côté client, pas de RPC nécessaire)
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);
}
// Envoyer un RPC au serveur demandant le nombre de joueurs et le nom du serveur
protected void RequestServerInfo()
{
if (!GetGame().IsMultiplayer())
{
// Mode hors ligne : afficher simplement les infos locales
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);
}
// --- Setters appelés quand les données arrivent ---
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);
}
}
// Basculer la visibilité
void ToggleVisibility()
{
m_IsVisible = !m_IsVisible;
if (m_Root)
m_Root.Show(m_IsVisible);
}
// Masquer quand les menus sont ouverts
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étails importants
- Chemin de
CreateWidgets: Le chemin est relatif à la racine du mod. Puisque nous empaquetons le dossierGUI/dans le PBO, le moteur résoutServerInfoHUD/GUI/layouts/ServerInfoHUD.layouten utilisant le préfixe du mod. FindAnyWidget: Recherche récursivement dans l'arbre de widgets par nom. Vérifiez toujours NULL après le cast.Widget.Unlink(): Supprime proprement le widget et tous ses enfants de l'arbre UI. Appelez toujours ceci lors du nettoyage.- Patron d'accumulateur de temps : Nous ajoutons
timesliceà chaque frame et n'agissons que quand le temps accumulé dépasseUPDATE_INTERVAL. Cela évite de faire du travail à chaque frame.
Étape 3 : Se connecter à MissionGameplay
La classe MissionGameplay est le contrôleur de mission côté client. Nous utilisons modded class pour injecter notre HUD dans son cycle de vie sans remplacer le fichier vanilla.
Scripts/5_Mission/ServerInfoHUD/MissionHook.c
modded class MissionGameplay
{
protected ref ServerInfoHUD m_ServerInfoHUD;
override void OnInit()
{
super.OnInit();
// Créer la surcouche HUD
m_ServerInfoHUD = new ServerInfoHUD();
m_ServerInfoHUD.Init();
}
override void OnMissionFinish()
{
// Nettoyer AVANT d'appeler super
if (m_ServerInfoHUD)
{
m_ServerInfoHUD.Destroy();
m_ServerInfoHUD = NULL;
}
super.OnMissionFinish();
}
override void OnUpdate(float timeslice)
{
super.OnUpdate(timeslice);
if (!m_ServerInfoHUD)
return;
// Masquer le HUD quand l'inventaire ou un menu est ouvert
UIManager uiMgr = GetGame().GetUIManager();
bool menuOpen = false;
if (uiMgr)
{
UIScriptedMenu topMenu = uiMgr.GetMenu();
if (topMenu)
menuOpen = true;
}
m_ServerInfoHUD.SetMenuState(menuOpen);
// Mettre à jour les données du HUD (limité en interne)
m_ServerInfoHUD.Update(timeslice);
// Vérifier la touche de bascule
Input input = GetGame().GetInput();
if (input)
{
if (GetUApi().GetInputByName("UAServerInfoToggle").LocalPress())
{
m_ServerInfoHUD.ToggleVisibility();
}
}
}
// Accesseur pour que le gestionnaire RPC puisse atteindre le 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
Pourquoi ce patron fonctionne
OnInits'exécute une fois quand le joueur entre en gameplay. Nous créons et initialisons le HUD ici.OnUpdates'exécute à chaque frame. Nous passonstimesliceau HUD, qui limite en interne à une fois par seconde. Nous vérifions aussi l'appui sur la touche de bascule et la visibilité des menus ici.OnMissionFinishs'exécute quand le joueur se déconnecte ou que la mission se termine. Nous détruisons nos widgets ici pour éviter les fuites de mémoire.
Règle critique : Toujours nettoyer
Si vous oubliez de détruire vos widgets dans OnMissionFinish, la racine du widget fuira dans la session suivante. Après quelques changements de serveur, le joueur se retrouve avec des widgets fantômes empilés consommant de la mémoire. Associez toujours Init() avec Destroy().
Étape 4 : Demander des données au serveur
Le nombre de joueurs n'est connu que sur le serveur. Nous avons besoin d'un simple aller-retour RPC (Remote Procedure Call) : le client envoie une requête, le serveur lit les données et les renvoie.
Étape 4a : Définir l'identifiant RPC
Les identifiants RPC doivent être uniques parmi tous les mods. Nous définissons le nôtre dans la couche 3_Game pour que le code client et serveur puissent le référencer.
Scripts/3_Game/ServerInfoHUD/ServerInfoRPC.c
// Identifiants RPC pour le Server Info HUD.
// Utiliser des numéros élevés pour éviter les conflits avec vanilla et les autres mods.
const int SIH_RPC_REQUEST_INFO = 72810;
const int SIH_RPC_RESPONSE_INFO = 72811;2
3
4
5
Pourquoi 3_Game ? Les constantes et énumérations appartiennent à la couche la plus basse accessible par le client et le serveur. La couche 3_Game se charge avant 4_World et 5_Mission, donc les deux côtés peuvent voir ces valeurs.
Étape 4b : Gestionnaire côté serveur
Le serveur écoute SIH_RPC_REQUEST_INFO, rassemble les données et répond avec 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;
// Rassembler les infos du serveur
string serverName = "";
GetGame().GetHostName(serverName);
int playerCount = 0;
int maxPlayers = 0;
// Obtenir la liste des joueurs
ref array<Man> players = new array<Man>();
GetGame().GetPlayers(players);
playerCount = players.Count();
// Nombre maximum de joueurs depuis la configuration serveur
maxPlayers = GetGame().GetMaxPlayers();
// Renvoyer les données au client demandeur
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
Étape 4c : Récepteur RPC côté client
Le client reçoit la réponse et met à jour le HUD.
Ajoutez ceci au même fichier ServerInfoHUD.c (en bas, en dehors de la classe), ou créez un fichier séparé dans 5_Mission/ServerInfoHUD/ :
Ajoutez ce qui suit en dessous de la classe ServerInfoHUD dans 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;
// Accéder au HUD via 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
Comment fonctionne le flux RPC
CLIENT SERVEUR
| |
|--- SIH_RPC_REQUEST_INFO ----->|
| | lit serverName, playerCount, maxPlayers
|<-- SIH_RPC_RESPONSE_INFO ----|
| |
| met à jour le texte du HUD |2
3
4
5
6
7
Le client envoie la requête une fois par seconde (limité par le minuteur de mise à jour). Le serveur répond avec trois valeurs empaquetées dans le contexte RPC. Le client les lit dans le même ordre qu'elles ont été écrites.
Important : rpc.Write() et ctx.Read() doivent utiliser les mêmes types dans le même ordre. Si le serveur écrit une string puis deux valeurs int, le client doit lire une string puis deux valeurs int.
Étape 5 : Ajouter une bascule avec raccourci clavier
Étape 5a : Définir l'entrée dans inputs.xml
DayZ utilise inputs.xml pour enregistrer des actions de touches personnalisées. Le fichier doit être placé dans Scripts/data/inputs.xml et référencé depuis 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
| Élément | Objectif |
|---|---|
<actions> | Déclare l'action d'entrée par nom. loc est la chaîne d'affichage montrée dans le menu des options de raccourcis clavier. |
<preset> | Assigne la touche par défaut. kF7 correspond à la touche F7. |
Étape 5b : Référencer inputs.xml dans config.cpp
Votre config.cpp doit indiquer au moteur où trouver le fichier d'entrées. Ajoutez une entrée inputs dans le bloc 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
Étape 5c : Lire l'appui de touche
Nous gérons déjà cela dans le hook MissionGameplay de l'Étape 3 :
if (GetUApi().GetInputByName("UAServerInfoToggle").LocalPress())
{
m_ServerInfoHUD.ToggleVisibility();
}2
3
4
GetUApi() renvoie le singleton de l'API d'entrée. GetInputByName recherche notre action enregistrée. LocalPress() renvoie true pour exactement une frame quand la touche est enfoncée.
Référence des noms de touches
Noms de touches courants pour <btn> :
| Nom de touche | Touche |
|---|---|
kF1 à kF12 | Touches de fonction |
kH, kI, etc. | Touches de lettres |
kNumpad0 à kNumpad9 | Pavé numérique |
kLControl | Contrôle gauche |
kLShift | Shift gauche |
kLAlt | Alt gauche |
Les combinaisons de modificateurs utilisent l'imbrication :
<input name="UAServerInfoToggle">
<btn name="kLControl">
<btn name="kH" />
</btn>
</input>2
3
4
5
Cela signifie "maintenir Contrôle gauche et appuyer sur H."
Étape 6 : Finitions
6a : Animation de fondu entrant/sortant
DayZ fournit WidgetFadeTimer pour des transitions d'opacité fluides. Mettez à jour la classe ServerInfoHUD pour l'utiliser :
class ServerInfoHUD : ScriptedWidgetEventHandler
{
// ... champs existants ...
protected ref WidgetFadeTimer m_FadeTimer;
void ServerInfoHUD()
{
m_IsVisible = true;
m_UpdateTimer = 0;
m_FadeTimer = new WidgetFadeTimer();
}
// Remplacer la méthode 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);
}
}
// ... reste de la 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, durée) anime l'opacité du widget de 0 à 1 sur la durée donnée en secondes. FadeOut passe de 1 à 0 et masque le widget une fois terminé.
6b : Panneau de fond avec alpha
Nous avons déjà défini cela dans la disposition (color="0 0 0 0.55"), donnant une surcouche sombre à 55% d'opacité. Si vous voulez ajuster l'alpha à l'exécution :
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 fonction ARGB() prend des valeurs entières 0-255 pour l'alpha, le rouge, le vert et le bleu.
6c : Choix de polices et de couleurs
DayZ inclut plusieurs polices que vous pouvez référencer dans les dispositions :
| Chemin de police | Style |
|---|---|
gui/fonts/MetronBook | Sans-serif propre (utilisé dans le HUD vanilla) |
gui/fonts/MetronMedium | Version plus grasse de MetronBook |
gui/fonts/Metron | Variante la plus fine |
gui/fonts/luxuriousscript | Script décoratif (à éviter pour le HUD) |
Pour changer la couleur du texte à l'exécution :
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 : Respecter les autres interfaces
Notre MissionHook.c détecte déjà quand un menu est ouvert et appelle SetMenuState(true). Voici une approche plus complète qui vérifie spécifiquement l'inventaire :
// Dans la surcharge OnUpdate de MissionGameplay moddé :
bool menuOpen = false;
UIManager uiMgr = GetGame().GetUIManager();
if (uiMgr)
{
UIScriptedMenu topMenu = uiMgr.GetMenu();
if (topMenu)
menuOpen = true;
}
// Vérifier aussi si l'inventaire est ouvert
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
Cela garantit que votre HUD se cache derrière l'écran d'inventaire, le menu pause, l'écran d'options et tout autre menu scripté.
Référence complète du code
Ci-dessous, chaque fichier du mod, dans sa forme finale avec toutes les finitions appliquées.
Fichier 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
Fichier 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
Fichier 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
Fichier 4 : ServerInfoHUD/Scripts/3_Game/ServerInfoHUD/ServerInfoRPC.c
// Identifiants RPC pour le Server Info HUD.
// Utiliser des numéros élevés pour éviter les collisions avec les ERPCs vanilla et les autres mods.
const int SIH_RPC_REQUEST_INFO = 72810;
const int SIH_RPC_RESPONSE_INFO = 72811;2
3
4
5
Fichier 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);
// Seul le serveur gère ce RPC
if (!GetGame().IsServer())
return;
if (rpc_type == SIH_RPC_REQUEST_INFO)
{
HandleServerInfoRequest(sender);
}
}
protected void HandleServerInfoRequest(PlayerIdentity sender)
{
if (!sender)
return;
// Obtenir le nom du serveur
string serverName = "";
GetGame().GetHostName(serverName);
// Compter les joueurs
ref array<Man> players = new array<Man>();
GetGame().GetPlayers(players);
int playerCount = players.Count();
// Obtenir le nombre maximum d'emplacements joueur
int maxPlayers = GetGame().GetMaxPlayers();
// Renvoyer les données au client demandeur
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
Fichier 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;
}
};
// -----------------------------------------------
// Récepteur RPC côté 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
Fichier 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;
// Détecter les menus ouverts
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);
// Touche de bascule
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
Fichier 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
Étendre le HUD
Une fois le HUD de base fonctionnel, voici des extensions naturelles.
Ajouter l'affichage du FPS
Le FPS peut être lu côté client sans aucun RPC :
// Ajouter un champ TextWidget m_FPSText et le trouver dans 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
Appelez RefreshFPS() aux côtés de RefreshTime() dans la méthode de mise à jour. Notez que GetDeltaT() renvoie le temps de la frame courante, donc la valeur du FPS fluctuera. Pour un affichage plus fluide, faites une moyenne sur plusieurs frames :
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());
// Réinitialiser chaque seconde (quand le minuteur principal se déclenche)
m_FPSAccum = 0;
m_FPSFrames = 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Ajouter la position du joueur
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
Panneaux HUD multiples
Pour plusieurs panneaux (boussole, statut, mini-carte), créez une classe gestionnaire parente qui contient un tableau d'éléments 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
Éléments HUD déplaçables
Rendre un widget déplaçable nécessite la gestion des événements souris via 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
Note : pour que le déplacement fonctionne, le widget doit avoir SetHandler(this) appelé dessus pour que le gestionnaire d'événements reçoive les événements. De plus, le curseur doit être visible, ce qui limite les HUD déplaçables aux situations où un menu ou un mode d'édition est actif.
Erreurs courantes
1. Mise à jour à chaque frame au lieu d'être limitée
Incorrect :
override void OnUpdate(float timeslice)
{
super.OnUpdate(timeslice);
m_ServerInfoHUD.RefreshTime(); // S'exécute 60+ fois par seconde !
m_ServerInfoHUD.RequestServerInfo(); // Envoie 60+ RPCs par seconde !
}2
3
4
5
6
Correct : Utilisez un accumulateur de temps (comme montré dans le tutoriel) pour que les opérations coûteuses s'exécutent au plus une fois par seconde. Le texte du HUD qui change à chaque frame (comme un compteur FPS) peut être mis à jour par frame, mais les requêtes RPC doivent être limitées.
2. Ne pas nettoyer dans OnMissionFinish
Incorrect :
modded class MissionGameplay
{
ref ServerInfoHUD m_HUD;
override void OnInit()
{
super.OnInit();
m_HUD = new ServerInfoHUD();
m_HUD.Init();
// Aucun nettoyage nulle part -- fuite de widgets à la déconnexion !
}
};2
3
4
5
6
7
8
9
10
11
12
Correct : Détruisez toujours les widgets et annulez les références dans OnMissionFinish(). Le destructeur (~ServerInfoHUD) est un filet de sécurité, mais ne comptez pas dessus -- OnMissionFinish est le bon endroit pour un nettoyage explicite.
3. HUD derrière d'autres éléments d'interface
Les widgets créés plus tard sont rendus au-dessus des widgets créés plus tôt. Si votre HUD apparaît derrière l'interface vanilla, il a été créé trop tôt. Solutions :
- Créez le HUD plus tard dans la séquence d'initialisation (par exemple au premier appel
OnUpdateplutôt que dansOnInit). - Utilisez
m_Root.SetSort(100)pour forcer un ordre de tri plus élevé, poussant votre widget au-dessus des autres.
4. Demander des données trop fréquemment (spam de RPC)
Envoyer un RPC à chaque frame crée 60+ paquets réseau par seconde par joueur connecté. Sur un serveur de 60 joueurs, cela fait 3 600 paquets par seconde de trafic inutile. Limitez toujours les requêtes RPC. Une fois par seconde est raisonnable pour des informations non critiques. Pour des données qui changent rarement (comme le nom du serveur), vous pourriez le demander une seule fois à l'initialisation et le mettre en cache.
5. Oublier l'appel super
// INCORRECT : casse la fonctionnalité du HUD vanilla
override void OnInit()
{
m_HUD = new ServerInfoHUD();
m_HUD.Init();
// super.OnInit() manquant ! Le HUD vanilla ne s'initialisera pas.
}2
3
4
5
6
7
Appelez toujours super.OnInit() (et super.OnUpdate(), super.OnMissionFinish()) en premier. Omettre l'appel super casse l'implémentation vanilla et chaque autre mod qui se connecte à la même méthode.
6. Utiliser la mauvaise couche de script
Si vous essayez de référencer MissionGameplay depuis 4_World, vous obtiendrez une erreur "Undefined type" car les types de 5_Mission ne sont pas visibles pour 4_World. Les constantes RPC vont dans 3_Game, le gestionnaire serveur va dans 4_World (moddant PlayerBase qui y réside), et la classe HUD et le hook de mission vont dans 5_Mission.
7. Chemin de disposition codé en dur
Le chemin de disposition dans CreateWidgets() est relatif aux chemins de recherche du jeu. Si le préfixe de votre PBO ne correspond pas à la chaîne de chemin, la disposition ne se chargera pas et CreateWidgets renvoie NULL. Vérifiez toujours NULL après CreateWidgets et journalisez une erreur si cela échoue.
Prochaines étapes
Maintenant que vous avez une surcouche HUD fonctionnelle, considérez ces progressions :
- Sauvegarder les préférences utilisateur -- Stockez si le HUD est visible dans un fichier JSON local pour que l'état de bascule persiste entre les sessions.
- Ajouter une configuration côté serveur -- Permettez aux administrateurs du serveur d'activer/désactiver le HUD ou de choisir quels champs afficher via un fichier de configuration JSON.
- Construire une surcouche admin -- Étendez le HUD pour afficher des informations réservées aux admins (performance serveur, nombre d'entités, minuteur de redémarrage) en utilisant des vérifications de permissions.
- Créer un HUD boussole -- Utilisez
GetGame().GetCurrentCameraDirection()pour calculer le cap et afficher une barre de boussole en haut de l'écran. - Étudier les mods existants -- Regardez le HUD de quête de DayZ Expansion et le système de surcouche de Colorful UI pour des implémentations HUD de qualité production.
Bonnes pratiques
- Limitez
OnUpdateà des intervalles d'au moins 1 seconde. Utilisez un accumulateur de temps pour éviter d'exécuter des opérations coûteuses (requêtes RPC, formatage de texte) 60+ fois par seconde. Seuls les visuels par frame comme les compteurs FPS devraient se mettre à jour à chaque frame. - Masquez le HUD quand l'inventaire ou un menu est ouvert. Vérifiez
GetGame().GetUIManager().GetMenu()à chaque mise à jour et supprimez votre surcouche. Les éléments d'interface qui se chevauchent déroutent les joueurs et bloquent les interactions. - Nettoyez toujours les widgets dans
OnMissionFinish. Les racines de widgets non libérées persistent entre les changements de serveur, empilant des panneaux fantômes qui consomment de la mémoire et finissent par causer des problèmes visuels. - Utilisez
SetSort()pour contrôler l'ordre de rendu. Si votre HUD apparaît derrière les éléments vanilla, appelezm_Root.SetSort(100)pour le pousser au-dessus. Sans ordre de tri explicite, le timing de création détermine la superposition. - Mettez en cache les données serveur qui changent rarement. Le nom du serveur ne change pas pendant une session. Demandez-le une fois à l'initialisation et mettez-le en cache localement au lieu de le redemander chaque seconde.
Théorie vs pratique
| Concept | Théorie | Réalité |
|---|---|---|
OnUpdate(float timeslice) | Appelé une fois par frame avec le delta time de la frame | Sur un client à 144 FPS, cela se déclenche 144 fois par seconde. Envoyer un RPC à chaque appel crée 144 paquets réseau/seconde par joueur. Accumulez toujours timeslice et n'agissez que quand la somme dépasse votre intervalle. |
Chemin de disposition de CreateWidgets() | Charge la disposition depuis le chemin que vous fournissez | Le chemin est relatif au préfixe du PBO, pas au système de fichiers. Si le préfixe de votre PBO ne correspond pas à la chaîne de chemin, CreateWidgets renvoie silencieusement NULL sans erreur dans le journal. |
WidgetFadeTimer | Anime fluidement l'opacité du widget | FadeOut masque le widget après la fin de l'animation, mais FadeIn n'appelle PAS Show(true) en premier. Vous devez manuellement afficher le widget avant d'appeler FadeIn, sinon rien n'apparaît. |
GetUApi().GetInputByName() | Renvoie l'action d'entrée pour votre raccourci personnalisé | Si inputs.xml n'est pas référencé dans config.cpp sous class inputs, le nom de l'action est inconnu et GetInputByName renvoie null, causant un crash sur .LocalPress(). |
Ce que vous avez appris
Dans ce tutoriel, vous avez appris :
- Comment créer une disposition HUD avec des panneaux ancrés et semi-transparents
- Comment construire une classe contrôleur qui limite les mises à jour à un intervalle fixe
- Comment se connecter à
MissionGameplaypour la gestion du cycle de vie du HUD (initialisation, mise à jour, nettoyage) - Comment demander des données serveur via RPC et les afficher sur le client
- Comment enregistrer un raccourci clavier personnalisé via
inputs.xmlet basculer la visibilité du HUD avec des animations de fondu
Précédent : Chapitre 8.7 : Publier sur le Steam Workshop
