>
Guide 2026-02-16

Système multi-personnages pour FiveM

TDYSKY

TDYSKY

Fondateur et développeur principal chez Agency Scripts

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.

Partager cet article

Prêt à améliorer votre serveur ?

Découvrez nos scripts FiveM premium dans la boutique Agency Scripts ou rejoignez notre communauté Discord pour le support et les mises à jour.