Pourquoi le dialogue avec les PNJ est important
Les systèmes de dialogue avec les PNJ transforment les enfants statiques en personnages vivants qui donnent de la personnalité à ton serveur et guident les joueurs dans leurs activités. Sans dialogue, les PNJ ne sont guère plus que des cibles d’interaction qui ouvrent des menus. Avec un système de dialogue approprié, ils deviennent des donneurs de quêtes, des conteurs, des commerçants pleins d'attitude et des informateurs qui réagissent différemment en fonction de la réputation ou du travail du joueur. Un système de dialogue crée des opportunités pour des connaissances spécifiques au serveur, des intrigues ramifiées et des choix de joueurs qui affectent les interactions futures. Pensez à quel point il est plus engageant lorsqu'un joueur s'approche d'un PNJ mécanicien qui le salue par son nom, commente l'état de sa voiture et propose différents services selon qu'il s'agit d'un client fidèle ou d'un nouveau visiteur. Ce niveau d'immersion maintient les joueurs investis dans ton serveur et leur donne des raisons d'explorer au-delà des boucles de travail standard.
Structure des données de l'arborescence de dialogue
La base de tout système de dialogue est la structure de données qui définit le flux de conversation. Un arbre de dialogue se compose de nœuds, où chaque nœud contient le texte du PNJ et une liste d'options de réponse du joueur qui renvoient à d'autres nœuds. Cette structure arborescente prend en charge les conversations ramifiées, les chemins conditionnels basés sur l'état du joueur et les boucles qui reviennent aux points précédents de la conversation. Concevez tes nœuds de dialogue pour qu'ils soient basés sur les données afin que le personnel du serveur puisse créer de nouvelles conversations sans toucher au code Lua. Voici un format d’arbre de dialogue pratique :
Config.Dialogues = {
['mechanic_greeting'] = {
npcName = 'Tony the Mechanic',
nodes = {
['start'] = {
text = "Hey there! Car giving you trouble, or are you just here for a tune-up?",
animation = 'WORLD_HUMAN_WELDING',
options = {
{
label = "I need repairs",
next = 'repairs',
condition = function(player)
return IsPlayerInVehicle(player)
end
},
{
label = "What services do you offer?",
next = 'services'
},
{
label = "Just browsing, thanks",
next = 'goodbye'
},
}
},
['repairs'] = {
text = "Let me take a look... Yeah, your engine's seen better days. I can fix it up for $500. What do you say?",
options = {
{ label = "Fix it up", next = 'repair_accept', action = 'repair_vehicle' },
{ label = "Too expensive", next = 'haggle' },
{ label = "Never mind", next = 'goodbye' },
}
},
['haggle'] = {
text = "Look, parts aren't cheap. But since you seem like a decent person, I can do $350. Final offer.",
options = {
{ label = "Deal!", next = 'repair_accept', action = 'repair_vehicle_discount' },
{ label = "I'll pass", next = 'goodbye' },
}
},
['services'] = {
text = "I do repairs, custom paint jobs, performance tuning, and tire changes. What catches your eye?",
options = {
{ label = "Tell me about tuning", next = 'tuning_info' },
{ label = "Back to start", next = 'start' },
}
},
['repair_accept'] = {
text = "Alright, give me a minute... Done! She's running smooth now. Take care of her out there.",
options = {
{ label = "Thanks, Tony!", next = 'end' },
}
},
['goodbye'] = {
text = "No worries. Come back anytime you need help with your ride!",
options = {
{ label = "See you around", next = 'end' },
}
},
}
},
}
Le condition la fonction sur les options tu permet d'afficher ou de masquer dynamiquement les choix en fonction de l'état actuel du joueur. Une option de réparation n'apparaît que lorsque le joueur arrive dans un véhicule, les options liées aux quêtes ne s'affichent que lorsque le joueur a atteint le bon niveau et les options VIP peuvent être limitées aux joueurs disposant de certaines autorisations. Le action Le champ déclenche des fonctions côté serveur lorsqu'une option spécifique est sélectionnée, reliant les choix de dialogue aux résultats du jeu.
Génération et gestion des PNJ
Les PNJ de dialogue doivent être générés de manière fiable, placés à des endroits cohérents et maintenus à l'abri du chaos que le monde de GTA peut leur infliger. Lorsque tu fais apparaître un PNJ de dialogue, tu dois demander le modèle, créer le ped, le définir comme entité de mission afin que les systèmes de nettoyage de GTA ne le suppriment pas, geler sa position pour qu'il ne s'éloigne pas et le rendre invincible afin que les joueurs ne puissent pas tuer ton donneur de quête. Utilisez un gestionnaire de PNJ centralisé qui génère des PED lorsque des joueurs sont à proximité et les fait disparaître lorsqu'aucun joueur n'est à portée pour économiser de la mémoire sur les serveurs occupés :
local spawnedNPCs = {}
function SpawnDialogueNPC(npcId, config)
if spawnedNPCs[npcId] then return end
local model = GetHashKey(config.model)
RequestModel(model)
while not HasModelLoaded(model) do Wait(10) end
local ped = CreatePed(0, model, config.coords.x, config.coords.y,
config.coords.z, config.heading, false, true)
SetEntityInvincible(ped, true)
SetBlockingOfNonTemporaryEvents(ped, true)
FreezeEntityPosition(ped, true)
SetPedFleeAttributes(ped, 0, false)
SetPedCombatAttributes(ped, 46, true)
SetPedCanRagdoll(ped, false)
SetEntityAsMissionEntity(ped, true, true)
SetModelAsNoLongerNeeded(model)
-- Play idle animation if configured
if config.scenario then
TaskStartScenarioInPlace(ped, config.scenario, 0, true)
end
spawnedNPCs[npcId] = { entity = ped, config = config }
return ped
end
La combinaison de SetBlockingOfNonTemporaryEvents et FreezeEntityPosition garantit que les événements ambiants comme les explosions à proximité, les poursuites policières ou les joueurs agressifs ne poussent pas ton PNJ à fuir, à riposter ou à ragdoll. Sans ces protections, les joueurs pourraient rencontrer un donneur de quête allongé sur le sol, se contractant après avoir été heurté par une voiture qui passait, brisant complètement l'immersion que tu avies travaillé pour créer.
Système de caméra cinématographique
Le travail de la caméra pendant les conversations de dialogue élève l'expérience d'une interaction de menu à un moment cinématographique. Lorsqu'un dialogue commence, créez une caméra qui se concentre sur le visage du PNJ avec un léger décalage, en utilisant la profondeur de champ pour rendre l'arrière-plan flou et attirer l'attention sur la conversation. Basculez entre les angles de caméra au fur et à mesure que la conversation progresse, en passant au joueur lorsqu'il fait un choix et en revenant au PNJ lorsqu'il répond. Le système de caméra natif de GTA tu offre un contrôle total sur la position, la rotation, le champ de vision et la profondeur de champ :
local dialogueCam = nil
function StartDialogueCamera(npcPed)
local npcCoords = GetEntityCoords(npcPed)
local npcHeading = GetEntityHeading(npcPed)
local playerPed = PlayerPedId()
-- Calculate camera position offset from NPC face
local angleRad = math.rad(npcHeading + 160)
local camX = npcCoords.x + (math.sin(angleRad) * 1.5)
local camY = npcCoords.y + (math.cos(angleRad) * 1.5)
local camZ = npcCoords.z + 0.6
dialogueCam = CreateCam('DEFAULT_SCRIPTED_CAMERA', true)
SetCamCoord(dialogueCam, camX, camY, camZ)
PointCamAtPedBone(dialogueCam, npcPed, 31086, 0.0, 0.0, 0.1, true) -- Head bone
-- Depth of field for cinematic look
SetCamNearDof(dialogueCam, 0.5)
SetCamFarDof(dialogueCam, 3.5)
SetCamDofStrength(dialogueCam, 0.6)
SetCamUseShallowDofMode(dialogueCam, true)
SetCamFov(dialogueCam, 40.0) -- Tighter shot
SetCamActive(dialogueCam, true)
RenderScriptCams(true, true, 800, true, false)
-- Disable player controls during dialogue
SetPlayerControl(PlayerId(), false, 0)
-- Make player face NPC
TaskTurnPedToFaceEntity(playerPed, npcPed, 1000)
end
function StopDialogueCamera()
if dialogueCam then
RenderScriptCams(false, true, 500, true, false)
DestroyCam(dialogueCam, true)
dialogueCam = nil
SetPlayerControl(PlayerId(), true, 0)
end
end
Le PointCamAtPedBone native est particulièrement puissante car elle verrouille la mise au point de la caméra sur la tête du PNJ indépendamment de tout léger mouvement d'animation, gardant ainsi le cadrage cohérent tout au long de la conversation. Les durées de transition dans RenderScriptCams créez des fondus de caméra fluides plutôt que des coupures discordantes, et tu dois expérimenter avec des valeurs comprises entre 500 ms et 1 000 ms pour trouver la bonne sensation en fonction du rythme de ton serveur.
Système d'affichage des sous-titres
Un système de sous-titres présente le texte du dialogue du PNJ d'une manière visuellement attrayante en bas de l'écran, imitant la façon dont les jeux basés sur une histoire affichent la conversation. Plutôt que de supprimer tout le bloc de texte d'un coup, implémentez un effet de machine à écrire qui révèle les caractères un par un, créant ainsi l'illusion que le PNJ parle activement. Utilisez NUI pour l'affichage des sous-titres car il tu donne un contrôle CSS total sur les polices, les couleurs, les animations et le positionnement. Envoyez le texte de chaque nœud de dialogue au cadre NUI lorsqu'il devient actif, ainsi que le nom du PNJ et toutes les balises d'émotion qui devraient affecter le style d'affichage :
// Subtitle display JavaScript (html/subtitles.js)
let typewriterTimeout = null;
window.addEventListener('message', (event) => {
const data = event.data;
if (data.action === 'showDialogue') {
clearTimeout(typewriterTimeout);
const container = document.getElementById('subtitle-container');
const nameEl = document.getElementById('npc-name');
const textEl = document.getElementById('dialogue-text');
const optionsEl = document.getElementById('dialogue-options');
container.style.display = 'block';
nameEl.textContent = data.npcName;
textEl.textContent = '';
optionsEl.innerHTML = '';
// Typewriter effect
let charIndex = 0;
const fullText = data.text;
function typeNext() {
if (charIndex < fullText.length) {
textEl.textContent += fullText[charIndex];
charIndex++;
typewriterTimeout = setTimeout(typeNext, 30);
} else {
// Show options after text completes
showOptions(data.options);
}
}
typeNext();
}
if (data.action === 'hideDialogue') {
document.getElementById('subtitle-container').style.display = 'none';
}
});
function showOptions(options) {
const optionsEl = document.getElementById('dialogue-options');
options.forEach((opt, index) => {
const btn = document.createElement('button');
btn.className = 'dialogue-option';
btn.innerHTML = `${index + 1} ${opt.label}`;
btn.onclick = () => {
fetch(`https://${GetParentResourceName()}/selectOption`, {
method: 'POST',
body: JSON.stringify({ index: index })
});
};
optionsEl.appendChild(btn);
});
}
Stylisez le conteneur de sous-titres avec un fond sombre semi-transparent, des coins arrondis et une bordure dégradée subtile qui correspond au thème de ton serveur. Positionnez-le en bas au centre de l'écran avec suffisamment de remplissage pour qu'il ne chevauche pas la mini-carte ou d'autres éléments HUD. Ajoutez des raccourcis clavier pour que les joueurs puissent appuyer sur les touches numériques pour sélectionner rapidement les options sans cliquer, ce qui semble plus naturel pendant la conversation.
Intégration du système de quête
Les systèmes de dialogue deviennent vraiment puissants lorsqu'ils sont connectés à un cadre de quête. L'arbre de dialogue action Le champ sur les options de réponse fournit le point d'ancrage où la logique de la quête s'exécute. Lorsqu'un joueur accepte une mission par le biais d'un dialogue, le gestionnaire d'action doit créer une entrée de quête dans le journal de quête du joueur, définir tous les points de cheminement ou objectifs requis et suivre la progression à travers les interactions de dialogue ultérieures. Stockez la progression de la quête dans la base de données par joueur afin qu'il puisse se déconnecter et reprendre là où il s'était arrêté. Concevez des quêtes comme des machines à états où chaque état correspond à un nœud de dialogue et à un ensemble d'objectifs qui doivent être complétés avant que le prochain dialogue ne soit disponible :
-- Server-side quest actions triggered by dialogue choices
local QuestActions = {
['accept_delivery_job'] = function(src, npcId)
local Player = QBCore.Functions.GetPlayer(src)
local citizenid = Player.PlayerData.citizenid
-- Create quest entry
MySQL.insert(
'INSERT INTO player_quests (citizenid, quest_id, stage, started_at) VALUES (?, ?, ?, NOW())',
{citizenid, 'tony_delivery_1', 'pickup'}
)
-- Set waypoint for pickup location
TriggerClientEvent('quest:client:setWaypoint', src, {
coords = vector3(482.5, -1311.2, 29.2),
blipSprite = 501,
blipColor = 5,
label = 'Package Pickup'
})
TriggerClientEvent('QBCore:Notify', src, 'Quest started: Special Delivery', 'success')
end,
['complete_delivery'] = function(src, npcId)
local Player = QBCore.Functions.GetPlayer(src)
local citizenid = Player.PlayerData.citizenid
MySQL.update(
'UPDATE player_quests SET stage = ?, completed_at = NOW() WHERE citizenid = ? AND quest_id = ?',
{'completed', citizenid, 'tony_delivery_1'}
)
Player.Functions.AddMoney('cash', 1500, 'quest-delivery-reward')
TriggerClientEvent('QBCore:Notify', src, 'Quest complete! Reward: $1,500', 'success')
end,
}
Utilisez l'étape de quête pour modifier dynamiquement les nœuds de dialogue disponibles. Lorsqu'un joueur revient vers Tony après avoir terminé la livraison, le système de dialogue vérifie l'étape de la quête et présente un dialogue de fin avec récompense au lieu du message d'accueil initial. Cela crée un flux de conversation naturel dans lequel les PNJ reconnaissent les progrès du joueur et réagissent en conséquence, rendant le monde réactif et vivant.
Animations et expressions de PNJ
Les PNJ statiques qui restent parfaitement immobiles tout en parlant semblent robotiques et brisent l'immersion. Ajoutez un support d'animation à ton système de dialogue pour que les PNJ fassent des gestes, émettent des émotions et réagissent pendant les conversations. GTA V dispose d'une vaste bibliothèque de dictionnaires d'animation couvrant les gestes, les expressions faciales et le langage corporel que tu peux déclencher à des moments spécifiques du dialogue. Attribuez des animations au niveau des nœuds afin que chaque ligne de dialogue puisse avoir son propre geste d'accompagnement. Lorsque le PNJ annonce une bonne nouvelle, jouez une joyeuse animation de vague de main. Lorsqu'ils discutent de quelque chose de sérieux, adoptez une pose sévère, les bras croisés. Pour les moments d'inactivité entre les réponses des joueurs, bouclez une animation de réflexion ou d'attente. Tu peux également utiliser des animations faciales natives comme SetFacialIdleAnimOverride pour changer l'expression au repos du PNJ pour qu'elle corresponde à l'ambiance de la conversation, le faisant paraître heureux, en colère, effrayé ou confus. Combinez des animations corporelles et faciales pour obtenir les performances les plus convaincantes, et testez toujours les animations dans le jeu, car certains dictionnaires d'animation semblent différents selon les modèles de pédophiles, et ce qui fonctionne sur un pédophile masculin peut être coupé ou paraître gênant sur un pédophile féminin.
Performances et meilleures pratiques
Un système de dialogue qui fait apparaître des dizaines de PNJ sur la carte nécessite une gestion minutieuse des performances. Faites apparaître des PNJ uniquement lorsque les joueurs sont à distance de rendu, généralement entre 50 et 100 mètres, et faites-les disparaître lorsqu'aucun joueur n'est à proximité. Utilisez un seul thread pour gérer toutes les distances d'apparition des PNJ plutôt que de créer des threads séparés par NPC, car cela réduit considérablement la surcharge côté client. Mettez en cache les données de l’arborescence de dialogue au démarrage de la ressource plutôt que de lire les fichiers pendant les conversations. Gardez les transitions de tes caméras de dialogue fluides, mais ne créez et ne détruisez pas de caméras de manière excessive, car les opérations de caméra ont un coût de performance mesurable. Lorsque plusieurs joueurs interagissent simultanément avec le même PNJ, chaque joueur doit avoir sa propre instance de dialogue qui s'exécute indépendamment, ce qui signifie que l'état du dialogue doit être stocké par joueur plutôt que sur l'entité PNJ. Nettoyez toutes les ressources de dialogue lorsque le joueur se déconnecte ou s'éloigne au milieu d'une conversation, détruisant la caméra, libérant le focus NUI et rétablissant les contrôles du joueur. Testez ton système de dialogue en gardant à l'esprit le pire des cas : le jeu du joueur plante au milieu du dialogue, ou il appuie sur F4 alors qu'une caméra est active. Ton système doit détecter ces cas et nettoyer correctement pour éviter les caméras persistantes ou les commandes verrouillées lors de la reconnexion.
