FiveM मल्टीकैरेक्टर सिस्टम: स्लॉट्स, स्विचिंग और डेटा
अपने FiveM सर्वर में मल्टीचरित्र समर्थन जोड़ें। स्लॉट UI, चरित्र डेटा, फ्रेमवर्क संबंध और QBCore और ESX के लिए सर्वश्रेष्ठ मल्टीचर स्क्रिप्ट।
Agency Scripts
Agency Scripts के संस्थापक और प्रमुख डेवलपर
मल्टी-कैरेक्टर सिस्टम क्यों महत्वपूर्ण हैं
एक मल्टी-चरित्र प्रणाली प्रत्येक खिलाड़ी को एक ही सर्वर पर कई अलग-अलग चरित्रों का मालिक होने की अनुमति देती है, प्रत्येक की अपनी पहचान, इन्वेंटरी, बैंक खाता, नौकरी, और आपराधिक रिकॉर्ड होता है। यह गंभीर रोलप्ले सर्वरों की एक कोने का पत्थर विशेषता है क्योंकि यह खिलाड़ियों को अपनी मुख्य चरित्र को छोड़ने के बिना विभिन्न कहानियों का अन्वेषण करने देता है। एक गैंग लीडर एक अलग चरित्र पर पुलिस अधिकारी भी खेल सकता है, या एक व्यवसाय मालिक का दूसरा चरित्र हो सकता है जो शहर में नया आगमन है। मल्टी-चरित्र समर्थन के बिना, खिलाड़ियों को या तो वैकल्पिक खाते चाहिए या वे एकल रोलप्ले पथ में फंसे रहते हैं। इस प्रणाली को सही ढंग से बनाने के लिए डेटा पृथक्करण, डेटाबेस स्कीमा डिज़ाइन, और एक परिष्कृत चरित्र चयन UI पर सावधानीपूर्वक ध्यान देने की आवश्यकता होती है जो पूरे सर्वर अनुभव के लिए टोन सेट करता है।
डेटाबेस स्कीमा डिज़ाइन
किसी भी मल्टी-कैरेक्टर प्रणाली की नींव डेटाबेस स्कीमा है। आपको प्लेयर-स्तर डेटा को कैरेक्टर-स्तर डेटा से अलग करना होगा। प्लेयर टेबल में लाइसेंस पहचानकर्ता, Steam hex, Discord ID, और खाता-व्यापी सेटिंग्स संग्रहीत होती हैं। कैरेक्टर्स टेबल में कैरेक्टर से संबंधित सभी चीजें होती हैं: नाम, जन्म तिथि, राष्ट्रीयता, पृष्ठभूमि, रूप, स्पॉन स्थिति, और एक विदेशी कुंजी जो प्लेयर से जुड़ी होती है। आपके डेटाबेस की हर अन्य टेबल जो पहले प्लेयर पहचानकर्ता को संदर्भित करती थी, अब एक को संदर्भित करनी होगी। character_id इसके बजाय। इसमें इन्वेंटरी, बैंक खाते, वाहन, आवास, फोन संपर्क, आपराधिक रिकॉर्ड, और नौकरी असाइनमेंट शामिल हैं। शुरुआत से इस स्कीमा को सही बनाना बाद में दर्दनाक माइग्रेशन को रोकता है।
-- 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
);
सर्वर-साइड कैरेक्टर प्रबंधन
सर्वर सभी चरित्र CRUD ऑपरेशन संभालता है: नए चरित्र बनाना, मौजूदा लोड करना, चरित्र डेटा सहेजना, और चरित्र हटाना। जब कोई खिलाड़ी कनेक्ट करता है, तो सर्वर उनके खिलाड़ी रिकॉर्ड और सभी संबंधित चरित्रों को डेटाबेस से प्राप्त करता है। यह डेटा क्लाइंट को भेजा जाता है ताकि चरित्र चयन UI भरा जा सके। चरित्र निर्माण में नाम की लंबाई जैसे इनपुट फ़ील्ड्स का सत्यापन होता है और यदि आपका सर्वर अद्वितीय पहचान लागू करता है तो डुप्लिकेट नामों को रोका जाता है। जब कोई खिलाड़ी चरित्र चुनता है, तो सर्वर सभी संबंधित डेटा तालिकाओं को लोड करता है, खिलाड़ी के सक्रिय चरित्र ID को मेमोरी में सेट करता है, और स्पॉन प्रक्रिया को ट्रिगर करता है। एक महत्वपूर्ण विवरण यह सुनिश्चित करना है कि प्रति खिलाड़ी केवल एक चरित्र सक्रिय हो सकता है, और चरित्र स्विचिंग सही ढंग से पिछले चरित्र के डेटा को सहेजता और अनलोड करता है।
-- 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)
चरित्र निर्माण प्रवाह
चरित्र निर्माण प्रक्रिया सहज और immersive होनी चाहिए। जब कोई खिलाड़ी एक खाली चरित्र स्लॉट पर क्लिक करता है, तो NUI एक निर्माण फॉर्म खोलता है जहाँ वे पहला नाम, अंतिम नाम, जन्म तिथि, लिंग, और वैकल्पिक रूप से एक पृष्ठभूमि कहानी दर्ज करते हैं। फॉर्म सबमिट करने के बाद, सर्वर इनपुट को मान्य करता है, एक नया चरित्र रिकॉर्ड डालता है, और खिलाड़ी को उपस्थिति संपादक में ले जाता है। उपस्थिति संपादक उन्हें GTA के मूल घटक विविधता प्रणाली का उपयोग करके अपने पेड मॉडल को अनुकूलित करने देता है, जिसमें चेहरे की विशेषताएं, बाल, कपड़े, और सहायक उपकरण शामिल हैं। एक बार जब वे अपनी उपस्थिति की पुष्टि करते हैं, तो त्वचा डेटा JSON में सीरियलाइज़ किया जाता है और चरित्र के skin कॉलम। फिर खिलाड़ी को डिफ़ॉल्ट नए खिलाड़ी के स्पॉन स्थान पर दुनिया में स्पॉन किया जाता है।
-- 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 फॉर कैरेक्टर सेलेक्शन
चरित्र चयन स्क्रीन वह पहली चीज़ है जो खिलाड़ी कनेक्ट होने के बाद देखते हैं, इसलिए इसे polished दिखना चाहिए और जल्दी लोड होना चाहिए। मानचित्र पर एक रोचक स्थान पर स्थित कैमरा का उपयोग करें, जिसमें खिलाड़ी के चरित्र पेड्स को लाइनअप या कैरोसेल प्रारूप में प्रस्तुत किया गया हो। प्रत्येक चरित्र कार्ड नाम, अंतिम बार खेले जाने की तारीख, नौकरी का शीर्षक, और वित्तीय अवलोकन दिखाता है। खाली स्लॉट एक प्लस आइकन दिखाते हैं जो खिलाड़ी को नया चरित्र बनाने के लिए आमंत्रित करता है। NUI क्लाइंट Lua स्क्रिप्ट के साथ संवाद करता है SendNUIMessage और RegisterNUICallbackऔर क्लाइंट Lua पेड स्पॉनिंग, कैमरा सेटअप, और चयन को सर्वर पर अग्रेषित करने को संभालता है। चयन के दौरान खिलाड़ी को फ्रीज करें और HUD छिपाएं ताकि कोई गेमप्ले इंटरैक्शन न हो जब तक कि चरित्र पूरी तरह से लोड न हो जाए।
-- 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
स्पॉन चयन और डेटा पृथक्करण
किसी चरित्र का चयन करने के बाद, खिलाड़ी को स्पॉन स्थान चुनना होता है। सामान्य विकल्पों में उनका अंतिम ज्ञात स्थान, उनका अपार्टमेंट या घर, अस्पताल यदि वे अंतिम बार डाउन थे, या डिफ़ॉल्ट शहर स्पॉन पॉइंट शामिल हैं। स्पॉन सेलेक्टर को केवल उस चरित्र के लिए प्रासंगिक विकल्प दिखाने चाहिए, उदाहरण के लिए अपार्टमेंट विकल्प केवल तभी दिखना चाहिए जब उस चरित्र का वास्तव में कोई अपार्टमेंट हो। डेटा आइसोलेशन मल्टी-चरित्र सिस्टम में सबसे महत्वपूर्ण वास्तुशिल्प चिंता है। आपके सर्वर पर प्रत्येक संसाधन जो प्रति-खिलाड़ी डेटा संग्रहीत करता है, उसे खिलाड़ी के लाइसेंस या सर्वर ID के बजाय चरित्र ID का उपयोग करना चाहिए। इसमें इन्वेंटरीज, फोन डेटा, बैंकिंग, वाहन स्वामित्व, आवास, और आपराधिक रिकॉर्ड शामिल हैं। एक सामान्य गलती खिलाड़ी के सर्वर स्रोत ID को डेटाबेस कुंजी के रूप में उपयोग करना है, जो चरित्र बदलने पर पूरी तरह टूट जाता है। अपने सर्वर के प्रत्येक संसाधन का ऑडिट करें और सुनिश्चित करें कि वे सभी सक्रिय चरित्र ID का संदर्भ देते हैं, जिसे आप एक एक्सपोर्ट के माध्यम से एक्सपोज़ करते हैं जैसे exports['multichar']:GetCharacterId(source).
चरित्र स्विचिंग और सत्र प्रबंधन
खिलाड़ियों को बिना पूरी तरह से डिस्कनेक्ट और पुनः कनेक्ट किए चरित्र बदलने की अनुमति देना एक गुणवत्ता-जीवन सुविधा है जिसे खिलाड़ी बहुत सराहते हैं। जब कोई खिलाड़ी चरित्र स्विच ट्रिगर करता है, तो सर्वर वर्तमान चरित्र के सभी डेटा को सहेजता है, मेमोरी से सभी चरित्र-विशिष्ट स्थिति को साफ़ करता है, और खिलाड़ी को चयन स्क्रीन पर वापस भेजता है। क्लाइंट पर, इसका मतलब है वर्तमान पेड को नष्ट करना, चरित्र के नौकरी या संपत्तियों से जुड़े सभी ब्लिप और मार्कर हटाना, किसी भी सक्रिय NUI इंटरफेस को साफ़ करना, और कैमरा को चयन दृश्य पर रीसेट करना। सर्वर पर हर संसाधन को एक multichar:characterUnloaded इवेंट ताकि वे अपनी स्थिति साफ़ कर सकें। यह वह जगह है जहां खराब डेटा पृथक्करण प्रकट होता है: यदि कोई संसाधन स्रोत ID के बजाय चरित्र ID द्वारा डेटा कैश करता है, तो चरित्र बदलने पर डेटा चरित्रों के बीच रिसाव हो सकता है। चरित्र स्विचिंग का पूरी तरह से परीक्षण करना आवश्यक है। दो चरित्र बनाएं जिनके अलग-अलग नौकरियां, इन्वेंटरी, और बैंक बैलेंस हों, फिर तेजी से स्विच करके पुष्टि करें कि उनके बीच कोई डेटा रिसाव नहीं होता।