วิธีใช้ 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

Popular posts from this blog

FiveM ยังน่าเล่นไหม? Enhanced เปลี่ยน FiveM แค่ไหน

FiveM คืออะไร เล่นอย่างไร สำหรับมือใหม่ เริ่มต้นตั้งแต่ศูนย์

วิธีตั้ง Admin Permission ด้วย add_ace และ add_principal FiveM แบบละเอียด