Pourquoi les systèmes à plusieurs caractères sont importants
Un système multi-personnages permet à chaque joueur de posséder plusieurs personnages distincts sur le même serveur, chacun avec sa propre identité, son inventaire, son compte bancaire, son travail et son casier judiciaire. Il s’agit d’une fonctionnalité fondamentale des serveurs de jeu de rôle sérieux, car elle permet aux joueurs d’explorer différents scénarios sans abandonner leur personnage principal. Un chef de gang peut également incarner un policier sur un personnage distinct, ou un propriétaire d'entreprise peut avoir un deuxième personnage fraîchement arrivé dans la ville. Sans prise en charge de plusieurs personnages, les joueurs ont besoin de comptes alternatifs ou sont enfermés dans un seul chemin de jeu de rôle. Construire correctement ce système nécessite une attention particulière à l'isolation des données, à la conception du schéma de base de données et à une interface utilisateur de sélection de caractères raffinée qui donne le ton à l'ensemble de l'expérience du serveur.
Conception de schéma de base de données
La base de tout système multi-caractères est le schéma de base de données. Tu dois séparer les données au niveau du joueur des données au niveau du personnage. La table du joueur stocke l'identifiant de licence, l'hexadécimal Steam, l'ID Discord et les paramètres à l'échelle du compte. La table des personnages stocke tout ce qui est spécifique à un personnage : nom, date de naissance, nationalité, histoire, apparence, position d'apparition et une clé étrangère reliant le joueur. Toutes les autres tables de ton base de données qui faisaient auparavant référence à un identifiant de joueur doivent désormais référencer un character_id plutôt. Cela comprend les inventaires, les comptes bancaires, les véhicules, les logements, les contacts téléphoniques, les casiers judiciaires et les affectations de travail. Obtenir ce schéma dès le départ évite une migration douloureuse ultérieure.
-- Database schema (MySQL)
CREATE TABLE players (
id INT AUTO_INCREMENT PRIMARY KEY,
license VARCHAR(60) NOT NULL UNIQUE,
steam VARCHAR(60) DEFAULT NULL,
discord VARCHAR(30) DEFAULT NULL,
max_slots INT DEFAULT 3,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE characters (
id INT AUTO_INCREMENT PRIMARY KEY,
player_id INT NOT NULL,
slot TINYINT NOT NULL DEFAULT 1,
firstname VARCHAR(50) NOT NULL,
lastname VARCHAR(50) NOT NULL,
dob DATE DEFAULT '1990-01-01',
nationality VARCHAR(50) DEFAULT 'American',
gender TINYINT DEFAULT 0,
backstory TEXT DEFAULT NULL,
skin LONGTEXT DEFAULT NULL,
job VARCHAR(50) DEFAULT 'unemployed',
job_grade INT DEFAULT 0,
cash INT DEFAULT 500,
bank INT DEFAULT 5000,
position VARCHAR(100) DEFAULT '{"x":-269.4,"y":-955.3,"z":31.2,"heading":205.0}',
is_dead TINYINT DEFAULT 0,
last_played TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE,
UNIQUE KEY unique_slot (player_id, slot)
);
CREATE TABLE character_inventories (
id INT AUTO_INCREMENT PRIMARY KEY,
character_id INT NOT NULL,
item VARCHAR(100) NOT NULL,
count INT DEFAULT 1,
metadata JSON DEFAULT NULL,
slot INT DEFAULT 1,
FOREIGN KEY (character_id) REFERENCES characters(id) ON DELETE CASCADE
);
Gestion des personnages côté serveur
Le serveur gère toutes les opérations CRUD sur les personnages : création de nouveaux personnages, chargement de ceux existants, sauvegarde des données de personnage et suppression de caractères. Lorsqu'un joueur se connecte, le serveur récupère son dossier de joueur et tous les personnages associés dans la base de données. Ces données sont envoyées au client pour remplir l'interface utilisateur de sélection de caractères. La création de personnages valide les champs de saisie tels que la longueur du nom et empêche les noms en double si ton serveur impose des identités uniques. Lorsqu'un joueur sélectionne un personnage, le serveur charge toutes les tables de données associées, définit l'ID de personnage actif du joueur en mémoire et déclenche le processus d'apparition. Un détail essentiel est de garantir qu'un seul personnage par joueur peut être actif à tout moment et que le changement de personnage enregistre et décharge correctement les données du personnage précédent.
-- server.lua
local activeCharacters = {} -- source -> characterId
RegisterNetEvent('multichar:requestCharacters', function()
local src = source
local license = GetPlayerIdentifierByType(src, 'license')
if not license then return DropPlayer(src, 'No license identifier found.') end
local player = MySQL.single.await(
'SELECT * FROM players WHERE license = ?', {license}
)
if not player then
MySQL.insert.await(
'INSERT INTO players (license, steam, discord) VALUES (?, ?, ?)',
{license, GetPlayerIdentifierByType(src, 'steam'), GetPlayerIdentifierByType(src, 'discord')}
)
player = MySQL.single.await('SELECT * FROM players WHERE license = ?', {license})
end
local characters = MySQL.query.await(
'SELECT id, slot, firstname, lastname, dob, gender, job, job_grade, cash, bank, last_played FROM characters WHERE player_id = ? ORDER BY slot ASC',
{player.id}
)
TriggerClientEvent('multichar:showSelection', src, characters, player.max_slots, player.id)
end)
RegisterNetEvent('multichar:selectCharacter', function(charId)
local src = source
local license = GetPlayerIdentifierByType(src, 'license')
local char = MySQL.single.await(
'SELECT c.* FROM characters c JOIN players p ON c.player_id = p.id WHERE c.id = ? AND p.license = ?',
{charId, license}
)
if not char then return end
-- Save previous character if switching
if activeCharacters[src] then
saveCharacter(src, activeCharacters[src])
end
activeCharacters[src] = charId
MySQL.update('UPDATE characters SET last_played = NOW() WHERE id = ?', {charId})
local pos = json.decode(char.position)
TriggerClientEvent('multichar:spawnCharacter', src, char, pos)
end)
AddEventHandler('playerDropped', function()
local src = source
if activeCharacters[src] then
saveCharacter(src, activeCharacters[src])
activeCharacters[src] = nil
end
end)
Flux de création de personnage
Le processus de création de personnage doit être intuitif et immersif. Lorsqu'un joueur clique sur un emplacement de personnage vide, le NUI ouvre un formulaire de création dans lequel il saisit un prénom, un nom, une date de naissance, un sexe et éventuellement une histoire. Après avoir soumis le formulaire, le serveur valide l'entrée, insère un nouvel enregistrement de personnage et fait passer le joueur à l'éditeur d'apparence. L'éditeur d'apparence leur permet de personnaliser leur modèle pédagogique à l'aide du système de variation de composants natif de GTA, notamment les caractéristiques du visage, les cheveux, les vêtements et les accessoires. Une fois leur apparence confirmée, les données de skin sont sérialisées sur JSON et stockées dans le dossier du personnage. skin colonne. Le joueur apparaît ensuite dans le monde à l'emplacement d'apparition par défaut du nouveau joueur.
-- Character creation (server.lua continued)
RegisterNetEvent('multichar:createCharacter', function(data, playerId)
local src = source
local license = GetPlayerIdentifierByType(src, 'license')
-- Validate ownership
local player = MySQL.single.await(
'SELECT id, max_slots FROM players WHERE id = ? AND license = ?',
{playerId, license}
)
if not player then return end
-- Check slot availability
local charCount = MySQL.scalar.await(
'SELECT COUNT(*) FROM characters WHERE player_id = ?', {player.id}
)
if charCount >= player.max_slots then
TriggerClientEvent('multichar:error', src, 'Maximum characters reached.')
return
end
-- Validate input
local firstname = tostring(data.firstname or ''):gsub('[^%a]', '')
local lastname = tostring(data.lastname or ''):gsub('[^%a]', '')
if #firstname < 2 or #lastname < 2 then
TriggerClientEvent('multichar:error', src, 'Name must be at least 2 characters.')
return
end
local nextSlot = MySQL.scalar.await(
'SELECT COALESCE(MAX(slot), 0) + 1 FROM characters WHERE player_id = ?',
{player.id}
)
local charId = MySQL.insert.await(
'INSERT INTO characters (player_id, slot, firstname, lastname, dob, gender, backstory) VALUES (?, ?, ?, ?, ?, ?, ?)',
{player.id, nextSlot, firstname, lastname, data.dob or '1990-01-01', data.gender or 0, data.backstory or ''}
)
TriggerClientEvent('multichar:openAppearanceEditor', src, charId)
end)
NUI côté client pour la sélection des caractères
L'écran de sélection des personnages est la première chose que les joueurs voient après la connexion, il doit donc être soigné et se charger rapidement. Utilisez une caméra positionnée à un endroit intéressant sur la carte, avec les personnages du joueur rendus sous forme d'alignement ou de carrousel. Chaque carte de personnage affiche le nom, la date de la dernière partie jouée, le titre du poste et un aperçu financier. Les emplacements vides affichent une icône plus invitant le joueur à créer un nouveau personnage. Le NUI communique avec le script client Lua via SendNUIMessage et RegisterNUICallback, et le client Lua gère la génération de ped, la configuration de la caméra et le transfert des sélections vers le serveur. Gelez le lecteur et masquez le HUD lors de la sélection pour empêcher toute interaction de jeu avant qu'un personnage ne soit complètement chargé.
-- client.lua
local selectionCam = nil
local previewPeds = {}
RegisterNetEvent('multichar:showSelection', function(characters, maxSlots, playerId)
SetNuiFocus(true, true)
DoScreenFadeIn(500)
-- Setup camera at selection location
local camPos = vector3(-75.0, -818.0, 326.0)
selectionCam = CreateCamWithParams('DEFAULT_SCRIPTED_CAMERA',
camPos.x, camPos.y, camPos.z, -35.0, 0.0, 0.0, 60.0)
SetCamActive(selectionCam, true)
RenderScriptCams(true, true, 1000, true, false)
-- Send data to NUI
SendNUIMessage({
action = 'showCharacterSelect',
characters = characters,
maxSlots = maxSlots,
playerId = playerId
})
end)
RegisterNUICallback('selectCharacter', function(data, cb)
SetNuiFocus(false, false)
cleanupSelection()
TriggerServerEvent('multichar:selectCharacter', data.id)
cb({ok = true})
end)
RegisterNUICallback('createCharacter', function(data, cb)
TriggerServerEvent('multichar:createCharacter', data, data.playerId)
cb({ok = true})
end)
RegisterNUICallback('deleteCharacter', function(data, cb)
TriggerServerEvent('multichar:deleteCharacter', data.id)
cb({ok = true})
end)
function cleanupSelection()
if selectionCam then
SetCamActive(selectionCam, false)
RenderScriptCams(false, true, 500, true, false)
DestroyCam(selectionCam, false)
selectionCam = nil
end
for _, ped in ipairs(previewPeds) do
if DoesEntityExist(ped) then DeleteEntity(ped) end
end
previewPeds = {}
end
Sélection de spawn et isolation des données
Après avoir sélectionné un personnage, le joueur doit choisir où apparaître. Les options courantes incluent leur dernière position connue, leur appartement ou leur maison, l'hôpital s'ils ont été abattus pour la dernière fois ou un point d'apparition par défaut de la ville. Le sélecteur d'apparition ne doit afficher que les options pertinentes pour le personnage, par exemple une option d'appartement ne doit apparaître que si ce personnage en possède réellement un. L'isolation des données est la préoccupation architecturale la plus critique dans un système multi-caractères. Chaque ressource de ton serveur qui stocke des données par joueur doit utiliser l'ID du personnage plutôt que la licence du joueur ou l'ID du serveur comme clé. Cela comprend les inventaires, les données téléphoniques, les opérations bancaires, la possession de véhicules, le logement et les casiers judiciaires. Une erreur courante consiste à utiliser l'ID source du serveur du joueur comme clé de base de données, qui se brise complètement lorsque les personnages changent. Auditez chaque ressource de ton serveur et assurez-tu qu'elles font toutes référence à l'ID de personnage actif, que tu exposes via une exportation comme exports['multichar']:GetCharacterId(source).
Changement de personnage et gestion de session
Permettre aux joueurs de changer de personnage sans se déconnecter ni se reconnecter complètement est une fonctionnalité de qualité de vie que les joueurs apprécient grandement. Lorsqu'un joueur déclenche un changement de personnage, le serveur enregistre toutes les données du personnage actuel, efface de la mémoire tous les états spécifiques au personnage et renvoie le joueur à l'écran de sélection. Sur le client, cela signifie détruire le ped actuel, supprimer tous les blips et marqueurs liés au travail ou aux propriétés du personnage, effacer toutes les interfaces NUI actives et réinitialiser la caméra sur la vue de sélection. Chaque ressource du serveur reçoit un multichar:characterUnloaded événement afin qu'ils puissent nettoyer leur propre état. C'est là que se révèle une mauvaise isolation des données : si une ressource met en cache les données par ID source au lieu de l'ID de caractère, le changement de caractère fera perdre les données entre les caractères. Il est essentiel de tester minutieusement le changement de personnage. Créez deux personnages avec des tâches, des inventaires et des soldes bancaires différents, puis alternez rapidement pour confirmer qu'aucune donnée ne fuit entre eux.
