Bloga Geri Dön
Guide7 dk. okuma

FiveM Çoklu Karakter Sistemi: Slotlar, Geçiş ve Veriler

FiveM sunucunuza çoklu karakter desteği ekleyin. Slot arayüzü, karakter verileri, framework bağlantıları ve QBCore ile ESX için en iyi multichar scriptleri.

Agency Scripts

Agency Scripts Kurucusu ve Baş Geliştiricisi

Çoklu Karakter Sistemleri Neden Önemlidir?

Çoklu karakter sistemi, her oyuncunun aynı sunucuda kimliği, envanteri, banka hesabı, mesleği ve sabıka kaydı ayrı olan birden fazla karaktere sahip olmasını sağlar. Bu, ciddi roleplay sunucularının temel bir özelliğidir çünkü oyuncuların ana karakterlerini terk etmeden farklı hikayeleri keşfetmelerine olanak tanır. Bir çete lideri ayrı bir karakterde polis memuru da oynayabilir veya bir işletme sahibi şehre yeni gelmiş ikinci bir karaktere sahip olabilir. Çoklu karakter desteği olmadan oyuncular ya yan hesaplara ihtiyaç duyar ya da tek bir roleplay yoluna kilitlenir. Bu sistemi doğru şekilde inşa etmek, veri izolasyonuna, veritabanı şeması tasarımına ve tüm sunucu deneyiminin havasını belirleyen şık bir karakter seçim UI'ına dikkat edilmesini gerektirir.

Veri Tabanı Şeması Tasarımı

Herhangi bir çoklu karakter sisteminin temeli veri tabanı şemasıdır. Oyuncu düzeyindeki verileri karakter düzeyindeki verilerden ayırmanız gerekir. Oyuncu tablosu lisans tanımlayıcısını, Steam hex, Discord ID ve hesap genelindeki ayarları saklar. Karakterler tablosu bir karaktere özgü her şeyi saklar: isim, doğum tarihi, uyruk, arka plan hikayesi, görünüm, spawn konumu ve oyuncuya geri bağlanan bir foreign key. Veri tabanınızda daha önce bir oyuncu tanımlayıcısına başvuran diğer her tablonun artık şuna başvurması gerekir: character_id kullanın. Buna envanterler, banka hesapları, araçlar, evler, telefon rehberleri, sabıka kayıtları ve meslek atamaları dahildir. Bu şemayı baştan doğru kurmak, ileride yaşanacak sancılı veri taşıma işlemlerini önler.

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

Sunucu Taraflı Karakter Yönetimi

Sunucu tüm karakter CRUD işlemlerini yürütür: yeni karakterler oluşturma, mevcut olanları yükleme, karakter verilerini kaydetme ve karakterleri silme. Bir oyuncu bağlandığında sunucu, oyuncu kaydını ve ilişkili tüm karakterleri veritabanından çeker. Bu veriler, karakter seçimi UI arayüzünü doldurmak için client tarafına gönderilir. Karakter oluşturma işlemi, isim uzunluğu gibi giriş alanlarını doğrular ve sunucunuz benzersiz kimlikler zorunlu kılıyorsa mükerrer isimleri engeller. Bir oyuncu bir karakter seçtiğinde sunucu, ilgili tüm veri tablolarını yükler, oyuncunun aktif karakter kimliğini belleğe atar ve spawn sürecini tetikler. Kritik bir ayrıntı, oyuncu başına her zaman yalnızca bir karakterin aktif olmasını sağlamak ve karakter değiştirirken önceki karakterin verilerini düzgün şekilde kaydedip bellekten kaldırmaktır.

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

Karakter Oluşturma Akışı

Karakter oluşturma süreci sezgisel ve sürükleyici olmalıdır. Bir oyuncu boş bir karakter yuvasına tıkladığında NUI; ad, soyad, doğum tarihi, cinsiyet ve isteğe bağlı olarak bir arka plan hikayesi girdikleri bir oluşturma formu açar. Form gönderildikten sonra sunucu girdiyi doğrular, yeni bir karakter kaydı ekler ve oyuncuyu görünüm düzenleyicisine aktarır. Görünüm düzenleyici, yüz özellikleri, saç, kıyafet ve aksesuarlar dahil olmak üzere GTA'nın yerel bileşen varyasyon sistemini kullanarak ped modellerini özelleştirmelerine olanak tanır. Görünümlerini onayladıklarında dış görünüş verileri JSON olarak serileştirilir ve karakterin skin sütunu. Oyuncu daha sonra varsayılan yeni oyuncu doğma noktasında dünyaya spawn edilir.

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

Karakter Seçimi için Client Tarafı NUI

Karakter seçim ekranı, oyuncuların bağlandıktan sonra gördüğü ilk şeydir, bu nedenle kusursuz görünmeli ve hızlı yüklenmelidir. Haritada ilgi çekici bir konuma yerleştirilmiş bir kamera kullanın ve oyuncunun karakter pedlerini bir sıra veya döner vitrin formatında işleyin. Her karakter kartı adı, son oynanma tarihini, meslek unvanını ve finansal genel bakışı gösterir. Boş yuvalar, oyuncuyu yeni bir karakter oluşturmaya davet eden bir artı simgesi sergiler. NUI, istemci Lua scripti ile şu yolla iletişim kurar: SendNUIMessage ve RegisterNUICallback, ve client Lua, ped oluşturma, kamera kurulumu ve seçimleri sunucuya iletme işlemlerini yönetir. Bir karakter tamamen yüklenmeden önce herhangi bir oyun içi etkileşimi önlemek için seçim sırasında oyuncuyu dondurun ve HUD'ı gizleyin.

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

Spawn Seçimi ve Veri İzolasyonu

Karakter seçiminden sonra oyuncunun nerede spawn olacağını seçmesi gerekir. Yaygın seçenekler arasında son bilinen konumları, apartmanları veya evleri, en son bayıldılarsa hastane veya varsayılan bir şehir spawn noktası yer alır. Spawn seçici yalnızca karakterle ilgili seçenekleri göstermelidir, örneğin bir apartman seçeneği yalnızca o karakter gerçekten birine sahipse görünmelidir. Çoklu karakter sisteminde veri izolasyonu en kritik mimari konudur. Sunucunuzda oyuncu başına veri depolayan her kaynak anahtar olarak oyuncunun lisansı veya sunucu ID'si yerine karakter ID'sini kullanmalıdır. Buna envanterler, telefon verileri, bankacılık, araç sahipliği, ev ve sabıka kayıtları dahildir. Yaygın bir hata, oyuncunun sunucu kaynak ID'sini veritabanı anahtarı olarak kullanmaktır, bu durum karakterler değiştirildiğinde tamamen bozulur. Sunucunuzdaki her kaynağı denetleyin ve hepsinin şu export gibi sunduğunuz aktif karakter ID'sine başvurduğundan emin olun: exports['multichar']:GetCharacterId(source).

Karakter Değiştirme ve Oturum Yönetimi

Oyuncuların sunucu bağlantısını tamamen kesip yeniden bağlanmadan karakter değiştirmelerine izin vermek, oyuncuların fazlasıyla takdir ettiği bir yaşam kalitesi özelliğidir. Bir oyuncu karakter değişimini tetiklediğinde, sunucu mevcut karaktere ait tüm verileri kaydeder, karaktere özgü tüm durumları bellekten temizler ve oyuncuyu seçim ekranına geri gönderir. İstemci tarafında bu işlem; mevcut ped'in yok edilmesi, karakterin mesleğine veya mülklerine bağlı tüm blip ve işaretçilerin kaldırılması, aktif tüm NUI arayüzlerinin temizlenmesi ve kameranın seçim görünümüne sıfırlanması anlamına gelir. Sunucudaki her kaynak bir multichar:characterUnloaded kendi durumlarını temizleyebilmeleri için event. Yetersiz veri izolasyonunun kendini gösterdiği yer burasıdır: Bir resource verileri karakter ID'si yerine kaynak ID'sine göre önbelleğe alırsa, karakter değiştirmek verilerin karakterler arasında sızmasına neden olur. Karakter değişimini kapsamlı bir şekilde test etmek şarttır. Farklı mesleklere, envanterlere ve banka bakiyelerine sahip iki karakter oluşturun, ardından aralarında hiçbir verinin sızmadığını doğrulamak için hızla ileri geri geçiş yapın.

Başlamaya Hazır mısınız?

Scriptleri mağazamızdan edinin veya destek, güncellemeler ve gelecek yeniliklere göz atmak için Discord'a katılın.