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.