วิธีใช้ Routing Bucket ทำ Instance หรือ Dimension ใน FiveM
การทำ Instance หรือ Dimension ใน FiveM สามารถใช้ระบบ Routing Bucket ของ OneSync เพื่อแยก Player, Vehicle, Ped และ Object ออกเป็น Session คนละชุดภายใน FXServer เดียวกันได้
ตัวอย่างเช่น Server มีผู้เล่นหลายกลุ่มกำลังทำ Mission ที่ตำแหน่งเดียวกัน
Party A
→ Bucket 1001
Party B
→ Bucket 1002
Party C
→ Bucket 1003
แม้ทุก Party จะใช้ Coordinates เดียวกัน แต่ Player และ Entity ในแต่ละ Bucket จะถูกแยกจากกันตาม Routing Context ของ OneSync
โครงสร้างพื้นฐานคือ
SetPlayerRoutingBucket(
playerId,
bucketId
)
และถ้ามี Vehicle, Ped หรือ Object ที่เป็นส่วนหนึ่งของ Instance ต้องกำหนด Bucket ให้ Entity ด้วย
SetEntityRoutingBucket(
entity,
bucketId
)
เมื่อต้องการให้ Player กลับ Main World
SetPlayerRoutingBucket(
playerId,
0
)
เพียงเท่านี้ก็สามารถเริ่มสร้างระบบ
Private Mission
Party Instance
Match
Lobby
Character Selection
Training Session
Dungeon
Multi-mode
ได้
แต่ระบบที่พร้อมใช้จริงยังต้องมี Bucket Allocator, Server-side Validation, Entity Management และ Cleanup ซึ่งบทความนี้จะทำให้ครบตั้งแต่ต้นจนจบ
① Routing Bucket ทำ Instance ได้อย่างไร
Routing Bucket แบ่ง Player และ Entity ออกเป็น Routing Context
ตัวอย่าง
Bucket 0
Main World
Bucket 100
Mission A
Bucket 101
Mission B
Player ใน Bucket 100 จะอยู่กับ Entities ของ Bucket 100
Player ใน Bucket 101 อยู่ในอีก Instance
จึงสามารถใช้พื้นที่เดียวกัน เช่น Warehouse เดียวกัน เป็น Mission แยกหลายรอบพร้อมกันได้
Cfx.re ระบุ Routing Bucket เป็นระบบที่มีแนวคิดคล้าย Dimension หรือ Virtual World และยก Session/Party Systems เป็นหนึ่งใน Use Case โดยตรง
② Routing Bucket ต้องทำฝั่ง Server
การกำหนด Bucket เป็น Server-side Logic
ตัวอย่าง
SetPlayerRoutingBucket(
source,
100
)
ไม่ควรให้ Client เป็น Authority ว่าอยากอยู่ Bucket ไหน
Architecture ควรเป็น
Client
↓
ขอเข้า Mission
↓
Server ตรวจสอบ
↓
Server เลือก Bucket
↓
SetPlayerRoutingBucket
ไม่ใช่
Client
↓
ส่ง bucketId อะไรก็ได้
↓
Server ย้ายตามทันที
③ เริ่มจาก Instance Table
Server ควรมี State สำหรับเก็บ Instances
local Instances = {}
ตัวอย่างข้อมูล
Instances[1001] = {
owner = 15,
players = {},
entities = {},
state = 'active'
}
Concept คือ
Bucket ID
↓
Instance
├── Owner
├── Players
├── Entities
└── Mission State
Routing Bucket ไม่ได้สร้าง Application State เหล่านี้ให้เอง
Developer ต้องจัดการเอง
④ สร้าง Bucket Allocator
ไม่ควรสุ่มเลข Bucket แบบไม่มีระบบ
ตัวอย่างพื้นฐาน
local nextBucketId = 1000
local function allocateBucket()
nextBucketId =
nextBucketId + 1
return nextBucketId
end
เมื่อสร้าง Instance
local bucket =
allocateBucket()
จะได้
1001
1002
1003
...
ช่วยลดปัญหา Bucket ชนกัน
⑤ ทำไมไม่ใช้ source เป็น Bucket เสมอไป
บางคนสร้าง
local bucket =
source
ทำได้สำหรับระบบง่ายบางแบบ
แต่ Player Source สามารถถูก Reuse หลัง Player Disconnect/Reconnect และ Party หนึ่งอาจมีหลาย Players
ระบบใหญ่ควรแยก
Player ID
กับ
Instance ID / Bucket ID
ออกจากกัน
จะจัดการ Lifecycle ง่ายกว่า
⑥ ฟังก์ชันสร้าง Instance
ตัวอย่าง
local Instances = {}
local nextBucketId = 1000
local function createInstance(owner)
nextBucketId =
nextBucketId + 1
local bucket =
nextBucketId
Instances[bucket] = {
owner = owner,
players = {
[owner] = true
},
entities = {},
state = 'waiting'
}
return bucket
end
ตอนเรียก
local bucket =
createInstance(source)
Server จะได้ Instance ใหม่พร้อม Bucket ID
⑦ Configure Bucket ก่อนใช้งาน
สามารถกำหนด Policy ของ Bucket เช่น
SetRoutingBucketEntityLockdownMode(
bucket,
'strict'
)
และ
SetRoutingBucketPopulationEnabled(
bucket,
false
)
เหมาะกับ Private Mission ที่ไม่ต้องการ Random Population และไม่ต้องการให้ Client สร้าง Entity เอง
⑧ strict Entity Lockdown คืออะไร
Mode
strict
หมายถึงไม่อนุญาตให้ Client สร้าง Entity ภายใน Bucket นั้น
จึงเหมาะกับ Architecture แบบ
Client
↓
Request Entity
↓
Server Validate
↓
Server Create Entity
แทนการปล่อย Client Spawn Vehicle/Ped/Object เอง
เอกสาร OneSync ปัจจุบันระบุ Lockdown Modes หลักเป็น strict, relaxed, inactive และมี full สำหรับ FiveM บน GTAV Enhanced โดยเฉพาะ
⑨ ปิด Population ใน Private Instance
ใช้
SetRoutingBucketPopulationEnabled(
bucket,
false
)
เหมาะกับ Instance เช่น
Character Selection
Controlled Mission
Training Room
Lobby
Dungeon
ที่ไม่ต้องการ Random NPC หรือ Traffic
แต่ถ้า Mission ต้องมี Open-world Traffic ก็สามารถเปิด Population ได้
⑩ ย้าย Player เข้า Instance
หลังสร้าง Instance
local bucket =
createInstance(source)
ย้าย Player
SetPlayerRoutingBucket(
source,
bucket
)
ตอนนี้ Player อยู่ใน Routing Bucket ใหม่
ตรวจได้ด้วย
local current =
GetPlayerRoutingBucket(
source
)
⑪ ตัวอย่าง Event เข้า Mission
RegisterNetEvent(
'mission:createInstance',
function()
local src =
source
if GetPlayerRoutingBucket(src)
~= 0 then
return
end
local bucket =
createInstance(src)
SetRoutingBucketEntityLockdownMode(
bucket,
'strict'
)
SetRoutingBucketPopulationEnabled(
bucket,
false
)
SetPlayerRoutingBucket(
src,
bucket
)
end
)
Client ไม่ได้ส่ง Bucket ID
Server เป็นผู้ Allocate เองทั้งหมด
⑫ ทำไมต้องตรวจ Current Bucket
ก่อนสร้าง Mission ใหม่ควรตรวจ
GetPlayerRoutingBucket(src)
เช่น
if GetPlayerRoutingBucket(src)
~= 0 then
return
end
ช่วยป้องกัน Player ที่อยู่ Mission อยู่แล้วสร้าง Instance ซ้อนโดยไม่ตั้งใจ
แต่ระบบจริงอาจใช้ Server-side Player Session State ร่วมด้วย
⑬ Party Instance ทำอย่างไร
Party หลายคนต้องอยู่ Bucket เดียวกัน
สมมติ
local partyPlayers = {
15,
24,
31
}
สามารถทำ
for _, playerId in ipairs(
partyPlayers
) do
SetPlayerRoutingBucket(
playerId,
bucket
)
Instances[bucket]
.players[playerId] =
true
end
ผลคือ
Player 15
Player 24
Player 31
↓
Bucket 1001
ทั้ง Party อยู่ Instance เดียวกัน
⑭ อย่าให้ Client ส่งรายชื่อ Party มาแล้วเชื่อทันที
ไม่ควร
TriggerServerEvent(
'mission:start',
{
15,
24,
31
}
)
แล้ว Server ย้ายทุก ID ตาม List
Server ควรอ่าน Party Membership จาก Server-side Party System
Flow ที่ดีกว่า
Client
↓
ขอ Start Mission
↓
Server หา Party ของ source
↓
ตรวจ Leader
↓
ตรวจ Members
↓
สร้าง Bucket
↓
ย้าย Members
⑮ Party Leader Validation
ตัวอย่าง Concept
local party =
GetServerParty(src)
if not party then
return
end
if party.leader ~= src then
return
end
จากนั้นค่อยสร้าง Instance
ช่วยให้ Player Member ธรรมดาไม่สามารถ Start Party Mission ตามใจได้
⑯ ตรวจ Player Online ก่อนย้าย
ก่อน
SetPlayerRoutingBucket(
playerId,
bucket
)
ควรตรวจว่า Player ยังอยู่ Server
เช่นใช้ Server-side Player APIs ตามระบบ
เพราะ Player อาจ Disconnect ระหว่าง Lobby และ Mission Start
⑰ Spawn Vehicle ภายใน Instance
Cfx.re แนะนำ Server-created Entities เป็น Best Practice สำหรับ OneSync หลาย Use Case
ตัวอย่าง
local vehicle =
CreateVehicleServerSetter(
joaat('sultan'),
'automobile',
215.0,
-810.0,
30.0,
157.0
)
จากนั้นนำ Entity เข้า Bucket
SetEntityRoutingBucket(
vehicle,
bucket
)
⑱ อย่าลืม Vehicle Bucket
ถ้าทำเพียง
SetPlayerRoutingBucket(
src,
100
)
แต่รถยังอยู่
Bucket 0
Player ใน Bucket 100 อาจไม่อยู่ Routing Context เดียวกับรถนั้น
ดังนั้น Mission Entity ควรมี
SetEntityRoutingBucket(
entity,
bucket
)
ตาม Instance
⑲ เก็บ Entity ไว้ใน Instance State
เมื่อสร้าง Vehicle
Instances[bucket]
.entities[vehicle] =
true
เมื่อ Mission จบจะสามารถ Loop Cleanup ได้
for entity in pairs(
instance.entities
) do
if DoesEntityExist(entity) then
DeleteEntity(entity)
end
end
นี่ช่วยป้องกัน Mission Vehicles ค้างใน Server
⑳ Server-created Entity Persistence
สำหรับ Server-created Entity ที่ต้องการให้ Server ไม่ลบเพียงเพราะไม่มี Owner สามารถใช้
SetEntityOrphanMode(
entity,
2
)
Cfx.re แนะนำ KeepEntity Orphan Mode สำหรับ Server-created Entity ที่ต้องการ Runtime Persistence ลักษณะนี้
แต่ไม่ได้หมายถึง Entity จะอยู่ข้าม Server Restart
㉑ Spawn Mission Ped
สามารถสร้าง Ped ฝั่ง Serverด้วย API ที่รองรับ
แล้วกำหนด Bucket
SetEntityRoutingBucket(
ped,
bucket
)
พร้อมเก็บ
instance.entities[ped] =
true
Concept เดียวกับ Vehicle
㉒ Spawn Mission Object
Object ที่เป็นส่วนหนึ่งของ Instance ก็ต้องคิดเหมือนกัน
Player
Vehicle
Ped
Object
↓
Bucket เดียวกัน
ถ้า Object ยังอยู่ Bucket 0 แต่ Mission Player อยู่ Bucket 1001 ก็อาจเกิดปัญหา Visibility/Routing
㉓ วิธีสร้าง Helper สำหรับ Track Entity
local function addEntityToInstance(
bucket,
entity
)
local instance =
Instances[bucket]
if not instance then
return false
end
if not DoesEntityExist(entity) then
return false
end
SetEntityRoutingBucket(
entity,
bucket
)
SetEntityOrphanMode(
entity,
2
)
instance.entities[entity] =
true
return true
end
ช่วยลด Code ซ้ำ
㉔ ใช้ State Bag กับ Mission Entity
สามารถผูก State เพิ่ม
Entity(vehicle).state:set(
'mission:id',
missionId,
true
)
หรือ
Entity(vehicle).state:set(
'mission:bucket',
bucket,
true
)
เพื่อให้ Clients/Resources ที่เกี่ยวข้องอ่าน Runtime State ได้
แต่ Bucket จริงยังควรหา Server-side จาก Routing APIs เมื่อใช้ Security Validation
㉕ อย่าเชื่อ mission:bucket จาก Client
State Bag ที่ Client เขียนได้ตาม Policy อาจไม่เหมาะเป็น Security Authority
ถ้า Server ต้องตรวจ Player จริง ๆ อยู่ Bucket ไหน ใช้
GetPlayerRoutingBucket(
src
)
และ Entity ใช้
GetEntityRoutingBucket(
entity
)
อย่าให้ Clientบอก Server ว่าอยู่ Bucket ไหน
㉖ ตรวจ Instance ตอน Mission Complete
Server Event
RegisterNetEvent(
'mission:complete',
function()
local src =
source
local bucket =
GetPlayerRoutingBucket(
src
)
local instance =
Instances[bucket]
if not instance then
return
end
if not instance.players[src] then
return
end
-- additional validation
end
)
นี่ทำให้ Server ตรวจได้ว่า Player อยู่ Instance ที่ระบบรู้จักจริง
㉗ Bucket Validation อย่างเดียวพอไหม
ไม่
ผู้เล่นอยู่ Mission Bucket จริง ไม่ได้หมายความว่าทำ Mission สำเร็จแล้ว
ยังต้องตรวจ
Mission State
Position
Progress
Items
Cooldown
Objectives
Permission
Routing Bucket เป็นอีกหนึ่ง Validation Signal
ไม่ใช่ Proof ว่า Action ถูกต้องทั้งหมด
㉘ ตัวอย่าง Position Validation ใน Instance
local ped =
GetPlayerPed(src)
if ped == 0 then
return
end
local coords =
GetEntityCoords(ped)
if #(coords - finishCoords)
> 10.0 then
return
end
จากนั้นตรวจ
if instance.state
~= 'ready_to_finish' then
return
end
ก่อนให้ Reward
㉙ อย่าให้ Client กำหนด Reward
แม้ Player อยู่ Bucket ถูกต้อง
ก็ยังไม่ควรทำ
TriggerServerEvent(
'mission:complete',
500000
)
แล้ว Server ใช้ Reward ตาม Client
ควร
local reward =
MissionConfig.reward
หรือคำนวณจาก Server-side Progress
Security Principle เดิมยังใช้เหมือน Network Event ทุกชนิด
㉚ ออกจาก Instance อย่างไร
เมื่อ Mission จบ
SetPlayerRoutingBucket(
src,
0
)
แต่ต้อง Update Instance State ด้วย
instance.players[src] =
nil
จากนั้นตรวจว่าเหลือ Player หรือไม่
㉛ ตรวจ Instance ว่าง
Lua สามารถใช้
local empty =
next(instance.players)
== nil
ถ้าไม่มี Players
if empty then
destroyInstance(bucket)
end
ช่วยให้ Instance ถูก Cleanup อัตโนมัติ
㉜ สร้าง destroyInstance()
local function destroyInstance(
bucket
)
local instance =
Instances[bucket]
if not instance then
return
end
for entity in pairs(
instance.entities
) do
if DoesEntityExist(entity) then
DeleteEntity(entity)
end
end
Instances[bucket] =
nil
end
นี่เป็นหัวใจสำคัญของ Instance Lifecycle
㉝ อย่าลืม Players ก่อน Destroy
ถ้า Instance ยังมี Players ไม่ควรลบ State ทิ้งทันที
ก่อน Destroy ควรแน่ใจว่า
Players = 0
หรือย้ายทุกคนกลับ Main World
ตัวอย่าง
for playerId in pairs(
instance.players
) do
SetPlayerRoutingBucket(
playerId,
0
)
end
แล้วจึง Cleanup Entities
㉞ Player กดออก Mission ก่อนจบ
สร้าง Event
RegisterNetEvent(
'mission:leave',
function()
local src =
source
leaveInstance(src)
end
)
และ Function
local function leaveInstance(
src
)
local bucket =
GetPlayerRoutingBucket(
src
)
local instance =
Instances[bucket]
if not instance then
return
end
instance.players[src] =
nil
SetPlayerRoutingBucket(
src,
0
)
if next(instance.players)
== nil then
destroyInstance(bucket)
end
end
㉟ Player Disconnect ต้อง Cleanup
ใช้
AddEventHandler(
'playerDropped',
function()
local src =
source
-- cleanup membership
end
)
แต่ต้องระวัง Lifecycle เพราะ Player Routing State และ Resource State อาจเปลี่ยนระหว่าง Disconnect
วิธีที่ Robust คือเก็บ Mapping
PlayerInstances[src] =
bucket
ฝั่ง Server
㊱ ทำ Player → Instance Mapping
local PlayerInstances = {}
ตอน Join
PlayerInstances[playerId] =
bucket
ตอน Leave
PlayerInstances[playerId] =
nil
ข้อดีคือไม่ต้องพึ่ง Query Routing Bucket อย่างเดียวในทุกจังหวะ
และทำ Cleanup ตอน playerDropped ได้ง่ายขึ้น
㊲ ตัวอย่าง playerDropped
AddEventHandler(
'playerDropped',
function()
local src =
source
local bucket =
PlayerInstances[src]
if not bucket then
return
end
PlayerInstances[src] =
nil
local instance =
Instances[bucket]
if not instance then
return
end
instance.players[src] =
nil
if next(instance.players)
== nil then
destroyInstance(
bucket
)
end
end
)
ช่วยลด Instance ค้างหลัง Party Disconnect หมด
㊳ Party Member Disconnect กลาง Mission
ไม่จำเป็นต้อง Destroy Instance ทันทีถ้ายังมี Member เหลือ
3 Players
↓
1 Disconnect
↓
2 Players ยังอยู่
↓
Mission ทำต่อ
แต่ถ้า Gameplay บังคับทุกคนต้องอยู่ครบ อาจ Mark Mission Failed
เป็น Business Rule ของระบบ ไม่ใช่ Routing Bucket Rule
㊴ Party Leader Disconnect ทำอย่างไร
ตัวเลือก เช่น
เลือก Leader ใหม่
Mission ทำต่อ
หรือ
Mission Cancel
ทุกคนกลับ Bucket 0
Cleanup
Routing Bucket ไม่ได้ตัดสินให้
ต้องกำหนด Mission Lifecycle เอง
㊵ ย้าย Party เข้า Bucket ต้องทำ Transaction-like Logic
สมมติ Party มี 4 คน แต่ย้ายสำเร็จเพียง 3
Resource ควรมี Recovery
เช่นตรวจ Players ก่อนเริ่มทั้งหมด
แล้วเปลี่ยน Mission State
creating
↓
moving_players
↓
active
หากเกิด Error
rollback
↓
ทุกคน Bucket 0
↓
Cleanup Entities
ลด Half-created Instances
㊶ Character Selection Instance
Routing Bucket เหมาะมากกับ Character Screen และ Cfx.re ยก Use Case นี้โดยตรง
Flow ตัวอย่าง
Player Connecting
↓
Server Assign Private Bucket
↓
Character Selection
↓
Player เลือก Character
↓
Server Load Character
↓
Spawn
↓
Bucket 0
Player ที่ยังเลือก Character จะไม่ปะปนกับ Main World
㊷ Private Character Bucket
ตัวอย่าง
local function enterCharacterSelection(
src
)
local bucket =
allocateBucket()
SetRoutingBucketPopulationEnabled(
bucket,
false
)
SetRoutingBucketEntityLockdownMode(
bucket,
'strict'
)
SetPlayerRoutingBucket(
src,
bucket
)
PlayerInstances[src] =
bucket
end
หลังเลือกเสร็จ
SetPlayerRoutingBucket(
src,
0
)
PlayerInstances[src] =
nil
แล้ว Release Instance State
㊸ Matchmaking Instance
ระบบ Deathmatch สามารถใช้
Match #501
→ Bucket 1501
Match #502
→ Bucket 1502
แต่ต้องแยก
Score
Teams
Timer
Spawn Points
Entities
ใน Server State ต่อ Match
เช่น
Instances[bucket].teams = {
red = {},
blue = {}
}
㊹ Racing Instance
Race หลายชุดสามารถใช้เส้นทางเดียวกันแต่ Bucket ต่างกัน
Race A
Bucket 2001
Race B
Bucket 2002
รถแข่งแต่ละกลุ่มอยู่ Bucket ของตัวเอง
ช่วยให้ไม่ชนกับ Race อื่นที่ใช้ Map เดียวกัน
㊺ Bucket กับ Interior ต้องระวัง
Routing Buckets เหมาะกับ Instance/Session แต่ Cfx.re ระบุชัดว่า Interior ปกติไม่ใช่ Use Case ที่แนะนำ
เหตุผลคือ GTA V มี Interior ที่สัมพันธ์กับ Outside World และการแยกทั้ง Routing Context ไม่เหมาะกับ Interior ทุกประเภท
ดังนั้นอย่าทำ
เข้าบ้านทุกหลัง
→ Bucket ใหม่
โดยอัตโนมัติเพียงเพราะต้องการให้คนอื่นไม่เห็น
㊻ แล้ว Apartment Instance ทำอย่างไร
ถ้า Gameplay ของ Apartment ถูกออกแบบให้เป็น Private Session ที่แยกจากโลกภายนอกโดยสมบูรณ์ Routing Bucket อาจยังถูกนำไปประยุกต์ตาม Architecture
แต่ถ้าเป็น Interior ที่ควรมี Visibility/Interaction กับ Outside World ต้องพิจารณา Conceal/Interior Systems อื่นแทน
ต้องเลือกจาก Requirement ไม่ใช่ใช้ Bucket กับทุก Interior
㊼ onPlayerBucketChange ใช้ Debug ได้ดี
FiveM มี Server Event
AddEventHandler(
'onPlayerBucketChange',
function(
player,
bucket,
oldBucket
)
print(
('Player %s: %s -> %s')
:format(
player,
oldBucket,
bucket
)
)
end
)
Event นี้ถูกเรียกเมื่อ Player เปลี่ยน Routing Bucket และให้ทั้ง Bucket ใหม่และเก่า
เหมาะกับ
Debug
Audit
Session Tracking
Detect Unexpected Move
㊽ onEntityBucketChange
Entity ก็มี Event
AddEventHandler(
'onEntityBucketChange',
function(
entity,
bucket,
oldBucket
)
print(
('Entity %s: %s -> %s')
:format(
entity,
oldBucket,
bucket
)
)
end
)
เอกสารปัจจุบันระบุ Event นี้ทำงานเมื่อ Entity เปลี่ยน Routing Bucket
เหมาะกับการหาปัญหา Mission Vehicle หายหรือถูกย้ายผิด Instance
㊾ อย่าใช้ Bucket Change Event แทน State ทั้งหมด
Event เหล่านี้ช่วยบอก
มีการเปลี่ยน Bucket
แต่ Instance Membership ที่เป็น Business State ยังควรเก็บใน
Instances
PlayerInstances
ของ Server
เพราะระบบของคุณอาจมีข้อมูลมากกว่า Bucket Number เช่น
partyId
missionId
leader
state
score
㊿ Bucket ID ควรเป็น Secret ไหม
ไม่จำเป็นต้องพึ่งการซ่อน Bucket ID เป็น Security
Security ที่ดีกว่าคือ
Client รู้ Bucket ID
↓
ก็เข้าเองไม่ได้
↓
เพราะ Server ไม่รับคำสั่ง setBucket แบบ Arbitrary
เช่นเดียวกับ Event Security
อย่าใช้ Obscurity เป็น Permission System
51 อย่าเปิด Event แบบ setBucket
ตัวอย่างอันตราย
RegisterNetEvent(
'instance:setBucket',
function(bucket)
SetPlayerRoutingBucket(
source,
bucket
)
end
)
เพราะ Client เลือก
0
100
9999
ได้ตามใจ
อาจเข้า Private Instance ของ Player อื่นได้ถ้ารู้ Bucket ID
52 Event ที่ถูกต้องควรเป็น Business Action
แทน
setBucket(1001)
ใช้
mission:join(45)
หรือ
party:startMission()
จากนั้น Server หา Bucket เอง
local mission =
Missions[missionId]
local bucket =
mission.bucket
แล้ว Server ย้าย Player
53 ตรวจ Membership ก่อน Join
if not mission.members[src] then
return
end
ก่อน
SetPlayerRoutingBucket(
src,
mission.bucket
)
ช่วยป้องกัน Player เข้า Instance อื่นด้วยการเดา Mission ID
54 Event ไปเฉพาะ Instance Players
เมื่อ Mission Update
ไม่จำเป็นต้อง
TriggerClientEvent(
'mission:update',
-1,
data
)
ควร Target Players ใน Instance
for playerId in pairs(
instance.players
) do
TriggerClientEvent(
'mission:update',
playerId,
data
)
end
ลด Network Work และลดข้อมูลที่ Player ไม่เกี่ยวข้องได้รับ
55 สร้าง Helper สำหรับ Broadcast Instance
local function emitToInstance(
bucket,
eventName,
...
)
local instance =
Instances[bucket]
if not instance then
return
end
for playerId in pairs(
instance.players
) do
TriggerClientEvent(
eventName,
playerId,
...
)
end
end
ใช้งาน
emitToInstance(
bucket,
'mission:updateTimer',
120
)
ช่วยให้ Code สะอาดขึ้น
56 Bucket มี Memory Cost
เอกสาร Cfx.re ระบุว่า Routing Bucket แต่ละ Bucket มี Memory Cost
ดังนั้นไม่ควรสร้าง
Bucket ใหม่
ทุกครั้ง
ตลอดไป
แล้วไม่ล้าง State
ระบบที่ดีควรมี
Allocate
↓
Use
↓
Destroy
↓
Reuse
57 ทำ Free Bucket Pool
แทนเพิ่มเลขไม่สิ้นสุด สามารถมี Pool
local freeBuckets = {}
ตอน Destroy
freeBuckets[#freeBuckets + 1] =
bucket
ตอน Allocate
local bucket =
table.remove(
freeBuckets
)
if not bucket then
nextBucketId =
nextBucketId + 1
bucket =
nextBucketId
end
ช่วย Reuse Bucket IDs
แต่ต้องแน่ใจว่า Instance เก่าถูก Cleanup จริงก่อน Reuse
58 ตรวจอะไรตอน Reuse Bucket
ก่อน Reuse ควรแน่ใจว่า
ไม่มี Player เก่า
ไม่มี Mission Entity
ไม่มี Instance State
ไม่มี Timer
ไม่มี Pending Callback
ไม่มี Reference ใน Server Table
ไม่เช่นนั้น Mission ใหม่อาจได้รับ State ของ Instance เก่า
59 Routing Bucket Instance Checklist
ก่อนนำระบบขึ้น Production ตรวจอย่างน้อย
① OneSync พร้อมหรือไม่?
② Bucket ถูก Allocate ฝั่ง Server หรือไม่?
③ Client สามารถกำหนด Bucket เองหรือไม่?
④ Instance มี Server-side State หรือไม่?
⑤ Player → Instance Mapping มีหรือไม่?
⑥ Party Membership ตรวจฝั่ง Server หรือไม่?
⑦ Player ถูกย้ายครบทุกคนหรือไม่?
⑧ Vehicle อยู่ Bucket เดียวกันหรือไม่?
⑨ Peds อยู่ Bucket เดียวกันหรือไม่?
⑩ Objects อยู่ Bucket เดียวกันหรือไม่?
⑪ Population ต้องเปิดหรือปิด?
⑫ Entity Lockdown Mode เหมาะสมหรือไม่?
⑬ Server-created Entities ถูกใช้เมื่อเหมาะสมหรือไม่?
⑭ Mission Event ตรวจ Bucket หรือไม่?
⑮ Mission Event ตรวจ Progress เพิ่มหรือไม่?
⑯ Reward คำนวณ Server-side หรือไม่?
⑰ Events ส่งเฉพาะ Instance Players หรือไม่?
⑱ Player Leave ถูกจัดการหรือไม่?
⑲ playerDropped ถูก Cleanup หรือไม่?
⑳ Leader Disconnect มี Policy หรือไม่?
㉑ Instance Empty ถูก Destroy หรือไม่?
㉒ Mission Entities ถูก Delete หรือไม่?
㉓ Bucket ID ถูก Release หรือไม่?
㉔ Bucket Change มี Debug/Audit หรือไม่?
㉕ Resource Restart มี Recovery หรือไม่?
㉖ กำลังใช้ Bucket กับ Interior โดยไม่จำเป็นหรือไม่?
⑥⓪ ตัวอย่างระบบ Instance FiveM ฉบับเต็ม
ตัวอย่างนี้รวม
Allocate Bucket
Instance State
Player Mapping
Strict Lockdown
Population
Server-created Vehicle
Party Join
Leave
Disconnect Cleanup
server.lua
local Instances = {}
local PlayerInstances = {}
local nextBucketId = 1000
local freeBuckets = {}
local function allocateBucket()
local reused =
table.remove(
freeBuckets
)
if reused then
return reused
end
nextBucketId =
nextBucketId + 1
return nextBucketId
end
local function releaseBucket(
bucket
)
freeBuckets[
#freeBuckets + 1
] = bucket
end
local function createInstance(
owner
)
local bucket =
allocateBucket()
Instances[bucket] = {
owner = owner,
state = 'waiting',
players = {},
entities = {}
}
SetRoutingBucketEntityLockdownMode(
bucket,
'strict'
)
SetRoutingBucketPopulationEnabled(
bucket,
false
)
return bucket
end
local function addPlayer(
bucket,
playerId
)
local instance =
Instances[bucket]
if not instance then
return false
end
if PlayerInstances[playerId] then
return false
end
instance.players[playerId] =
true
PlayerInstances[playerId] =
bucket
SetPlayerRoutingBucket(
playerId,
bucket
)
return true
end
local function addEntity(
bucket,
entity
)
local instance =
Instances[bucket]
if not instance then
return false
end
if not DoesEntityExist(entity) then
return false
end
SetEntityRoutingBucket(
entity,
bucket
)
SetEntityOrphanMode(
entity,
2
)
instance.entities[entity] =
true
return true
end
local function destroyInstance(
bucket
)
local instance =
Instances[bucket]
if not instance then
return
end
for playerId in pairs(
instance.players
) do
PlayerInstances[playerId] =
nil
SetPlayerRoutingBucket(
playerId,
0
)
end
for entity in pairs(
instance.entities
) do
if DoesEntityExist(entity) then
DeleteEntity(entity)
end
end
Instances[bucket] =
nil
releaseBucket(
bucket
)
end
local function removePlayer(
playerId
)
local bucket =
PlayerInstances[playerId]
if not bucket then
return
end
local instance =
Instances[bucket]
PlayerInstances[playerId] =
nil
if not instance then
return
end
instance.players[playerId] =
nil
SetPlayerRoutingBucket(
playerId,
0
)
if next(instance.players)
== nil then
destroyInstance(
bucket
)
end
end
สร้าง Mission Instance
RegisterNetEvent(
'mission:create',
function()
local src =
source
if PlayerInstances[src] then
return
end
if GetPlayerRoutingBucket(src)
~= 0 then
return
end
local bucket =
createInstance(src)
if not addPlayer(
bucket,
src
) then
destroyInstance(
bucket
)
return
end
local ped =
GetPlayerPed(src)
if ped == 0 then
destroyInstance(
bucket
)
return
end
local coords =
GetEntityCoords(ped)
local vehicle =
CreateVehicleServerSetter(
joaat('sultan'),
'automobile',
coords.x + 5.0,
coords.y,
coords.z,
0.0
)
if vehicle ~= 0 then
addEntity(
bucket,
vehicle
)
Entity(vehicle)
.state:set(
'mission:vehicle',
true,
true
)
end
Instances[bucket].state =
'active'
end
)
Join Existing Instance
RegisterNetEvent(
'mission:join',
function(missionId)
local src =
source
if type(missionId)
~= 'number' then
return
end
-- ตัวอย่าง:
-- หา Mission จาก
-- Server-side Mission Registry
--
-- local mission =
-- Missions[missionId]
--
-- if not mission then
-- return
-- end
--
-- if not mission.members[src] then
-- return
-- end
--
-- addPlayer(
-- mission.bucket,
-- src
-- )
end
)
Leave Instance
RegisterNetEvent(
'mission:leave',
function()
local src =
source
removePlayer(
src
)
end
)
Disconnect Cleanup
AddEventHandler(
'playerDropped',
function()
local src =
source
local bucket =
PlayerInstances[src]
if not bucket then
return
end
PlayerInstances[src] =
nil
local instance =
Instances[bucket]
if not instance then
return
end
instance.players[src] =
nil
if next(instance.players)
== nil then
destroyInstance(
bucket
)
end
end
)
Debug Player Bucket
AddEventHandler(
'onPlayerBucketChange',
function(
player,
bucket,
oldBucket
)
print(
('Player %s moved %s -> %s')
:format(
player,
oldBucket,
bucket
)
)
end
)
Debug Entity Bucket
AddEventHandler(
'onEntityBucketChange',
function(
entity,
bucket,
oldBucket
)
print(
('Entity %s moved %s -> %s')
:format(
entity,
oldBucket,
bucket
)
)
end
)
ระบบนี้สามารถต่อยอดเป็น
Party
↓
Lobby
↓
Ready Check
↓
Create Instance
↓
Move Players
↓
Spawn Mission Entities
↓
Mission Active
↓
Mission Complete
↓
Reward Validation
↓
Move Players Bucket 0
↓
Delete Entities
↓
Release Bucket
ได้ทันที
คำถามที่พบบ่อยเกี่ยวกับการใช้ Routing Bucket ทำ Instance
Routing Bucket ใช้ทำ Dimension FiveM ได้ไหม
ได้ และเป็น Dimension/Instance System ของ FiveM OneSync
Player เข้า Instance อย่างไร
Server ใช้
SetPlayerRoutingBucket(
playerId,
bucket
)
Vehicle เข้า Instance อย่างไร
ใช้
SetEntityRoutingBucket(
vehicle,
bucket
)
Ped และ Object ใช้ได้ไหม
ได้สำหรับ Entity ที่ระบบรองรับ โดยกำหนด Entity Routing Bucket เช่นเดียวกัน
ออกจาก Instance ทำอย่างไร
โดยทั่วไปย้าย Player กลับ
SetPlayerRoutingBucket(
playerId,
0
)
และ Cleanup Instance State
Party หลายคนอยู่ Bucket เดียวกันได้ไหม
ได้ และเป็นหนึ่งใน Use Case หลักของ Routing Buckets
ทำ Mission หลายรอบที่ Coordinates เดียวกันได้ไหม
ได้ หากแต่ละ Mission อยู่คนละ Routing Bucket
Character Selection ใช้ได้ไหม
ได้ และ Cfx.re ยกเป็น Use Case โดยตรง
ใช้กับ Interior ได้ไหม
Cfx.re ไม่แนะนำ Routing Buckets เป็นวิธีทั่วไปสำหรับ Interiors
Client ควรเป็นคนส่ง Bucket ID ไหม
ไม่ควร Server ควรกำหนด Bucket จาก Mission/Party State เอง
Routing Bucket ป้องกัน Cheat ไหม
ไม่โดยอัตโนมัติ Network Events และ Business Logic ยังต้อง Validate Server-side
strict Lockdown คืออะไร
เป็น Mode ที่ไม่ให้ Client สร้าง Entity ใน Bucket นั้น
ควรใช้ strict ทุก Instance ไหม
ไม่เสมอ ต้องตรวจ Compatibility ของ Resources เพราะ Script ที่พึ่ง Client-created Entities อาจทำงานไม่ได้
ปิด NPC ใน Instance ได้ไหม
ได้ด้วย
SetRoutingBucketPopulationEnabled(
bucket,
false
)
Bucket มี Memory Cost ไหม
มี เอกสาร Cfx.re ระบุว่า Routing Bucket แต่ละ Bucket มี Memory Cost จึงควรสร้างเท่าที่จำเป็นและ Cleanup หลังใช้งาน
มี Event ตอน Player เปลี่ยน Bucket ไหม
มี onPlayerBucketChange ซึ่งให้ Player, Bucket ใหม่ และ Bucket เดิม
มี Event ตอน Entity เปลี่ยน Bucket ไหม
มี onEntityBucketChange
Player Disconnect แล้ว Instance หายเองไหม
Application State และ Mission Cleanup ต้องให้ Resource ของคุณจัดการเอง
Bucket ID Reuse ได้ไหม
ได้ตาม Architecture หาก Instance เดิมถูก Cleanup สมบูรณ์แล้ว
Routing Bucket แยก Database ไหม
ไม่ Database และ Server-side Lua State ยังเป็นระบบเดียวกัน
Network ID ยังใช้ใน Bucket ได้ไหม
ได้ Network ID บอก Entity ตัวไหน ส่วน Routing Bucket บอก Routing Context ของ Entity นั้น
สรุปวิธีใช้ Routing Bucket ทำ Instance หรือ Dimension ใน FiveM
วิธีสร้าง Instance ที่ถูกต้องไม่ใช่แค่
SetPlayerRoutingBucket(
source,
100
)
แล้วจบ
ระบบ Production ควรมี Flow ประมาณ
Client ขอเข้า Mission
↓
Server Validate
↓
Allocate Bucket
↓
Create Instance State
↓
Configure Population
↓
Configure Entity Lockdown
↓
Move Players
↓
Create/Move Entities
↓
Mission Active
↓
Validate Gameplay Server-side
↓
Mission Finish
↓
Return Players to Bucket 0
↓
Delete Entities
↓
Clear State
↓
Release Bucket
สำหรับ Server ที่ต้องการควบคุม Entity อย่างเข้มงวด สามารถใช้
SetRoutingBucketEntityLockdownMode(
bucket,
'strict'
)
ร่วมกับ Server-created Entities
และหากไม่ต้องการ Random Population
SetRoutingBucketPopulationEnabled(
bucket,
false
)
Routing Bucket เหมาะมากกับ
Mission Instance
Party
Lobby
Character Selection
Match
Race
Multi-mode
แต่ Cfx.re ระบุชัดว่าไม่ได้แนะนำ Routing Bucket เป็น Solution ทั่วไปสำหรับ Interior
อีกเรื่องที่ต้องจำคือ Routing Bucket เป็นเพียง Networking Isolation Layer ไม่ใช่ Security System ทั้งหมด
แม้ Player จะอยู่ Bucket ที่ถูกต้อง Server ยังต้องตรวจ
Mission State
Position
Permission
Progress
Inventory
Reward
ก่อนทำ Action สำคัญ
สำหรับผู้ที่กำลังเรียน FiveM Developer กับ comsiam วิธีคิดที่ดีที่สุดคือ อย่าให้ Client เลือก Dimension แต่ให้ Client ขอเข้ากิจกรรม แล้ว Server เป็นผู้เลือก Instance และ Bucket ให้
ส่วนหลักจาก comsiam อีกข้อคือ Instance ต้องมี Lifecycle ครบตั้งแต่ Create → Join → Active → Leave → Cleanup → Reuse ถ้าขาดขั้น Cleanup ไม่นาน Server จะเต็มไปด้วย Player State, Mission Entity และ Bucket Registry ที่ไม่มีใครใช้งาน
เมื่อเข้าใจ Routing Bucket และการสร้าง Instance แล้ว หัวข้อถัดไปจะเข้าสู่ระบบ Networking ตัวใหญ่ของ FiveM โดยตรง คือ FiveM OneSync คืออะไร และเหตุใด Entity, State Bags, Routing Buckets และ Server-side Entity APIs จึงเชื่อมโยงกับ OneSync ทั้งหมด
Comments
Post a Comment