>
Guia 2026-02-16

Configurar Sistemas Multicharacter para FiveM

TDYSKY

TDYSKY

Fundador & Lead Developer na Agency Scripts

Por que os sistemas multicaracteres são importantes

Um sistema multi-personagem permite que cada jogador possua vários personagens separados no mesmo servidor, cada um com sua própria identidade, inventário, conta bancária, trabalho e antecedentes criminais. Este é um recurso fundamental dos servidores de RPG sérios porque permite aos jogadores explorar diferentes histórias sem abandonar seu personagem principal. Um líder de gangue também pode interpretar um policial em um personagem separado, ou um empresário pode ter um segundo personagem recém-chegado à cidade. Sem suporte para vários personagens, os jogadores precisam de contas alternativas ou ficam presos a um único caminho de RPG. Construir este sistema corretamente requer atenção cuidadosa ao isolamento de dados, design de esquema de banco de dados e uma interface de usuário de seleção de caracteres refinada que define o tom para toda a experiência do servidor.

Design de esquema de banco de dados

A base de qualquer sistema multicaractere é o esquema do banco de dados. Você precisa separar os dados do nível do jogador dos dados do nível do personagem. A tabela do jogador armazena o identificador de licença, hexadecimal Steam, ID Discord e configurações de toda a conta. A tabela de personagens armazena tudo específico de um personagem: nome, data de nascimento, nacionalidade, história de fundo, aparência, posição de spawn e uma chave estrangeira vinculada ao jogador. Todas as outras tabelas no seu banco de dados que anteriormente referenciavam um identificador de jogador agora precisam referenciar um character_id. Isso inclui inventários, contas bancárias, veículos, moradia, contatos telefônicos, antecedentes criminais e atribuições de trabalho. Acertar esse esquema desde o início evita uma migração dolorosa posteriormente.

-- 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
);

Gerenciamento de caracteres no servidor

O servidor lida com todas as operações CRUD de caracteres: criação de novos caracteres, carregamento dos existentes, salvamento de dados de caracteres e exclusão de caracteres. Quando um jogador se conecta, o servidor busca o registro do jogador e todos os personagens associados do banco de dados. Esses dados são enviados ao cliente para preencher a IU de seleção de personagens. A criação de caracteres valida campos de entrada como comprimento de nome e evita nomes duplicados se o seu servidor impor identidades exclusivas. Quando um jogador seleciona um personagem, o servidor carrega todas as tabelas de dados relacionadas, define o ID do personagem ativo do jogador na memória e aciona o processo de spawn. Um detalhe crítico é garantir que apenas um personagem por jogador possa estar ativo a qualquer momento e que a troca de personagem salve e descarregue corretamente os dados do personagem anterior.

-- 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)

Fluxo de criação de personagem

O processo de criação de personagem deve ser intuitivo e envolvente. Quando um jogador clica em um slot de personagem vazio, o NUI abre um formulário de criação onde ele insere um nome, sobrenome, data de nascimento, sexo e, opcionalmente, uma história de fundo. Após enviar o formulário, o servidor valida a entrada, insere um novo registro de personagem e faz a transição do jogador para o editor de aparência. O editor de aparência permite que eles personalizem seu modelo ped usando o sistema de variação de componentes nativos do GTA, incluindo características faciais, cabelos, roupas e acessórios. Depois de confirmarem sua aparência, os dados do skin são serializados em JSON e armazenados na coluna skin do personagem. O jogador é então gerado no mundo no local padrão de surgimento do novo jogador.

-- 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 do lado do cliente para seleção de personagens

A tela de seleção de personagem é a primeira coisa que os jogadores veem após a conexão, por isso precisa ter uma aparência elegante e carregar rapidamente. Use uma câmera posicionada em um local interessante do mapa, com os peds do personagem do jogador renderizados em formato de alinhamento ou carrossel. Cada carta de personagem exibe o nome, a data da última jogada, o cargo e a visão geral financeira. Os slots vazios mostram um ícone de adição convidando o jogador a criar um novo personagem. O NUI se comunica com o script Lua do cliente por meio de SendNUIMessage e RegisterNUICallback, e o cliente Lua lida com a geração de ped, configuração da câmera e encaminhamento de seleções para o servidor. Congele o jogador e oculte o HUD durante a seleção para evitar qualquer interação de jogo antes que o personagem esteja totalmente carregado.

-- 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

Seleção de geração e isolamento de dados

Depois de selecionar um personagem, o jogador precisa escolher onde nascer. As opções comuns incluem sua última posição conhecida, seu apartamento ou casa, o hospital, caso tenham sido abatidos pela última vez, ou um ponto de desova padrão da cidade. O seletor de spawn deve mostrar apenas opções relevantes para o personagem, por exemplo, uma opção de apartamento só deve aparecer se o personagem realmente possuir um. O isolamento de dados é a preocupação arquitetônica mais crítica em um sistema multicaractere. Cada recurso em seu servidor que armazena dados por jogador deve usar o ID do personagem em vez da licença do jogador ou ID do servidor como chave. Isso inclui inventários, dados telefônicos, serviços bancários, propriedade de veículos, habitação e antecedentes criminais. Um erro comum é usar o ID de origem do servidor do jogador como chave do banco de dados, que quebra totalmente quando os personagens são trocados. Audite todos os recursos do seu servidor e certifique-se de que todos façam referência ao ID do personagem ativo, que você expõe por meio de uma exportação como exports['multichar']:GetCharacterId(source).

Troca de personagem e gerenciamento de sessão

Permitir que os jogadores troquem de personagem sem desconectar e reconectar totalmente é um recurso de qualidade de vida que os jogadores apreciam muito. Quando um jogador aciona uma troca de personagem, o servidor salva todos os dados do personagem atual, limpa todos os estados específicos do personagem da memória e envia o jogador de volta à tela de seleção. No cliente, isso significa destruir o ped atual, remover todos os blips e marcadores vinculados ao trabalho ou propriedades do personagem, limpar quaisquer interfaces NUI ativas e redefinir a câmera para a visualização de seleção. Cada recurso no servidor recebe um evento multichar:characterUnloaded para que possam limpar seu próprio estado. É aqui que o mau isolamento de dados se revela: se um recurso armazena dados em cache por ID de origem em vez de ID de caractere, a troca de caracteres irá sangrar os dados entre os caracteres. Testar completamente a troca de personagens é essencial. Crie dois personagens com empregos, estoques e saldos bancários diferentes e, em seguida, alterne rapidamente para confirmar que não há vazamento de dados entre eles.

Partilhar este artigo

Pronto para melhorar o teu servidor?

Explora os nossos scripts FiveM premium na loja Agency Scripts ou junta-te à nossa comunidade no Discord para suporte e atualizações.