FiveM SendNUIMessage และ SetNuiFocus ใช้อย่างไร? เปิด UI ส่งข้อมูล และจัดการ Mouse Keyboard ให้ถูกต้อง

FiveM SendNUIMessage ใช้ส่งข้อมูลจาก Client Script ไปยัง NUI Browser ส่วน SetNuiFocus ใช้กำหนดว่า NUI จะรับ Keyboard และ Mouse Cursor หรือไม่ สองคำสั่งนี้จึงมักถูกใช้คู่กันในระบบ Inventory, Phone, Garage, Shop, Banking, Character Creator และ Menu ต่าง ๆ

Flow พื้นฐานคือ

FiveM Client
↓
SetNuiFocus(true, true)
↓
SendNUIMessage(...)
↓
NUI Browser
↓
HTML / CSS / JavaScript

ตัวอย่างเปิด UI

SetNuiFocus(
    true,
    true
)

SendNUIMessage({
    action = 'open'
})

JavaScript รับข้อมูล

window.addEventListener(
    'message',
    (event) => {

        const data =
            event.data;

        if (data.action === 'open') {
            document
                .getElementById('menu')
                .classList
                .add('active');
        }
    }
);

เมื่อปิด

SetNuiFocus(
    false,
    false
)

SendNUIMessage({
    action = 'close'
})

นี่คือพื้นฐานสำคัญของระบบ Client → NUI ใน FiveM

① SendNUIMessage คืออะไร

SendNUIMessage เป็น Lua Convenience Function สำหรับส่งข้อมูลจาก Client Runtime ไปยัง NUI Page ของ Resource ปัจจุบัน

รูปแบบ

SendNUIMessage({
    action = 'open'
})

Cfx.re ระบุว่า Lua Wrapper จะ Encode Table เป็น JSON สำหรับ NUI ให้อัตโนมัติ

ดังนั้น Browser สามารถรับข้อมูลผ่าน event.data

② SendNUIMessage ทำงานฝั่งไหน

ใช้งานจาก Client Script

Flow คือ

client.lua
↓
SendNUIMessage
↓
NUI Browser

ไม่ใช่

server.lua
↓
SendNUIMessage

หาก Server ต้องการเปลี่ยน NUI ของ Player ต้องส่งข้อมูลมายัง Client ก่อน

③ Server จะอัปเดต NUI อย่างไร

ตัวอย่าง

Server
↓
TriggerClientEvent
↓
Client
↓
SendNUIMessage
↓
NUI

Server

TriggerClientEvent(
    'bank:updateBalance',
    source,
    balance
)

Client

RegisterNetEvent(
    'bank:updateBalance',
    function(balance)

        SendNUIMessage({
            action = 'updateBalance',
            balance = balance
        })
    end
)

Browser

window.addEventListener(
    'message',
    (event) => {

        if (
            event.data.action
            === 'updateBalance'
        ) {
            console.log(
                event.data.balance
            );
        }
    }
);

④ JavaScript รับ SendNUIMessage อย่างไร

ใช้

window.addEventListener(
    'message',
    (event) => {

        console.log(
            event.data
        );
    }
);

ถ้า Lua ส่ง

SendNUIMessage({
    action = 'hello',
    name = 'Somchai'
})

Browser ได้

event.data.action

และ

event.data.name

⑤ action มีไว้ทำอะไร

action ไม่ใช่ Field ที่ FiveM บังคับ

แต่เป็น Convention ที่ดีสำหรับบอกว่า Message นี้ต้องการให้ UI ทำอะไร

เช่น

open
close
updateMoney
updateInventory
updateVehicle
showNotification
setJob

ตัวอย่าง

SendNUIMessage({
    action = 'updateMoney',
    cash = 5000
})

⑥ ใช้ type แทน action ได้ไหม

ได้

เอกสาร Cfx.re เองมีตัวอย่าง

SendNUIMessage({
    type = 'open'
})

ดังนั้นคุณจะใช้

action
type
event

ก็ได้

สิ่งสำคัญคือต้องใช้ Naming Convention ให้สม่ำเสมอทั้ง Resource

⑦ SendNUIMessage ส่ง String ได้ไหม

โดยทั่วไปส่ง Table ที่สามารถแปลงเป็น JSON ได้

เช่น

SendNUIMessage({
    message = 'Hello FiveM'
})

Browser จะได้

event.data.message

เป็น String

⑧ ส่ง Number ได้ไหม

ได้

SendNUIMessage({
    action = 'money',
    cash = 5000,
    bank = 25000
})

Browser

const cash =
    event.data.cash;

⑨ ส่ง Boolean ได้ไหม

ได้

SendNUIMessage({
    action = 'setSeatbelt',
    enabled = true
})

Browser

if (
    event.data.enabled
) {
    // enabled
}

⑩ ส่ง Lua Table ซ้อนกันได้ไหม

ได้ หากข้อมูลสามารถ Serialize เป็น JSON ได้

SendNUIMessage({
    action = 'playerData',

    player = {
        name = 'Somchai',
        cash = 5000,

        job = {
            name = 'police',
            grade = 2
        }
    }
})

Browser

const player =
    event.data.player;

console.log(
    player.job.name
);

⑪ ส่ง Array ได้ไหม

ได้

Lua

SendNUIMessage({
    action = 'vehicles',

    vehicles = {
        {
            id = 1,
            plate = 'ABC123'
        },
        {
            id = 2,
            plate = 'XYZ789'
        }
    }
})

Browser

const vehicles =
    event.data.vehicles;

vehicles.forEach(
    (vehicle) => {
        console.log(
            vehicle.plate
        );
    }
);

⑫ อย่าส่งข้อมูลที่ NUI ไม่จำเป็นต้องรู้

หาก Garage UI ต้องใช้

Vehicle ID
Plate
Model
Stored

ก็ส่งเท่านี้

ไม่จำเป็นต้องส่ง

Database password
Server secrets
Admin tokens
Internal SQL
Private server configuration

NUI อยู่ฝั่ง Client

จึงไม่ควรได้รับ Secrets

⑬ SetNuiFocus คืออะไร

SetNuiFocus เป็น Native สำหรับกำหนด Input Focus ให้ NUI Resource

พื้นฐาน

SetNuiFocus(
    hasFocus,
    hasCursor
)

ตัวอย่าง

SetNuiFocus(
    true,
    true
)

หมายถึงให้ NUI รับ Keyboard Focus และ Mouse Cursor

⑭ Parameter ตัวแรกคืออะไร

ตัวแรก

hasFocus

ควบคุม Keyboard/Input Focus

ตัวอย่าง

SetNuiFocus(
    true,
    false
)

NUI มี Focus แต่ไม่เปิด Mouse Cursor

เหมาะกับ UI บางประเภทที่ใช้ Keyboard เป็นหลัก

⑮ Parameter ตัวที่สองคืออะไร

ตัวที่สอง

hasCursor

ควบคุม Mouse Cursor

ตัวอย่าง

SetNuiFocus(
    true,
    true
)

เหมาะกับ

Inventory
Phone
Shop
Garage
Admin Menu

ที่ต้องคลิก Buttons

⑯ เปิด Menu แบบมาตรฐาน

local menuOpen =
    false

local function openMenu()

    menuOpen =
        true

    SetNuiFocus(
        true,
        true
    )

    SendNUIMessage({
        action = 'open'
    })
end

นี่เป็น Pattern ที่ใช้กันมาก

⑰ ปิด Menu แบบมาตรฐาน

local function closeMenu()

    menuOpen =
        false

    SetNuiFocus(
        false,
        false
    )

    SendNUIMessage({
        action = 'close'
    })
end

จุดสำคัญคือ ปิดทั้ง UI และ Focus

⑱ ทำไมปิด HTML อย่างเดียวไม่พอ

สมมติ JavaScript ทำ

menu.classList.remove(
    'active'
);

แต่ Client ยังมี

SetNuiFocus(
    true,
    true
)

Player อาจเจอ

UI หาย
แต่ Mouse ยังขึ้น
เดินไม่ได้
Keyboard ไม่กลับเข้าเกม

จึงต้องคืน Focus ฝั่ง Client

⑲ SetNuiFocus(false, false) สำคัญมาก

เมื่อ UI ปิด

SetNuiFocus(
    false,
    false
)

ควรเป็นส่วนหนึ่งของ Close Function เสมอ

อย่ากระจาย Logic ปิด UIหลายจุดโดยลืมคืน Focus บาง Branch

⑳ สร้าง Function กลางสำหรับ UI State

แนะนำ

local uiOpen =
    false

local function setUiState(
    state
)

    uiOpen =
        state

    SetNuiFocus(
        state,
        state
    )

    SendNUIMessage({
        action =
            state
            and 'open'
            or 'close'
    })
end

เปิด

setUiState(
    true
)

ปิด

setUiState(
    false
)

ลดโอกาส UI State กับ Focus ไม่ตรงกัน

㉑ ทำ Toggle Menu ได้อย่างไร

RegisterCommand(
    'menu',
    function()

        setUiState(
            not uiOpen
        )

    end,
    false
)

Player พิมพ์

/menu

ครั้งแรกเปิด

ครั้งต่อไปปิด

㉒ NUI ส่งคำสั่งปิดกลับมายัง Client อย่างไร

Browser

await fetch(
    `https://${GetParentResourceName()}/close`,
    {
        method: 'POST',

        headers: {
            'Content-Type':
                'application/json; charset=UTF-8'
        },

        body:
            JSON.stringify({})
    }
);

Client

RegisterNuiCallback(
    'close',
    function(data, cb)

        setUiState(
            false
        )

        cb({
            ok = true
        })
    end
)

㉓ ESC ควรปิดผ่าน Client ด้วย

JavaScript

document.addEventListener(
    'keydown',
    async (event) => {

        if (
            event.key !== 'Escape'
        ) {
            return;
        }

        await closeUi();
    }
);

closeUi() ควรเรียก NUI Callback ไป Client

ไม่ควรเพียงซ่อน DOM เพราะ Focus ยังอยู่ฝั่ง FiveM

㉔ NUI Focus Stack คืออะไร

Cfx.re ระบุว่า Fullscreen NUI มี Focus Stack แบบจำกัด

Resource ที่ถูก Focus ล่าสุดจะอยู่ด้านบนของ Focus Stack

Concept

Inventory Focus
↓
Phone Focus
↓
Admin Menu Focus ล่าสุด

Resource ล่าสุดจะอยู่ด้านบนตามระบบ Focus

จึงต้องระวัง UI หลาย Resource เปิดพร้อมกัน

㉕ NUI หลาย Resource แย่ง Focus กันได้ไหม

ได้

สมมติ

Inventory
Phone
Garage

แต่ละ Resource เรียก

SetNuiFocus(
    true,
    true
)

พร้อมกัน

อาจเกิด

Mouse ไปผิด UI
Keyboard Focus ผิด
UI อยู่ด้านหลัง
Input สับสน

จึงควรมี UI State Coordination บน Server ที่มีหลาย Interface

㉖ Click-through ข้าม NUI Resource ได้ไหม

Cfx.re ระบุว่า NUI Resources ปัจจุบันเป็น Fullscreen Iframes และ ไม่มี Click-through ข้าม Resources

ดังนั้น Resource ที่อยู่ด้านบนสามารถขวาง Interaction ของ Resource ที่อยู่ด้านล่างได้

นี่เป็นอีกเหตุผลที่ Focus Management สำคัญ

㉗ HUD ต้อง SetNuiFocus ไหม

ไม่จำเป็น

HUD ที่แค่แสดง

Health
Armor
Cash
Job
Speed
Fuel

ไม่ต้องให้ Player Click

จึงสามารถใช้

SendNUIMessage(...)

อย่างเดียวโดยไม่ต้อง

SetNuiFocus(
    true,
    true
)

㉘ Interactive Menu ถึงค่อย Focus

เช่น

Inventory
Shop
Character Creator
Banking Menu
Phone

ต้องใช้ Mouse/Keyboard

จึงค่อย

SetNuiFocus(
    true,
    true
)

อย่า Focus NUI หากไม่จำเป็น

㉙ SendNUIMessage ใช้อัปเดต HUD อย่างไร

Lua

SendNUIMessage({
    action = 'updateHud',
    health = health,
    armor = armor,
    cash = cash
})

Browser

if (
    event.data.action
    === 'updateHud'
) {

    updateHud(
        event.data
    );
}

แต่ต้องคิดเรื่อง Frequency ด้วย

㉚ อย่าส่ง NUI Message ทุก Frame โดยไม่มีเหตุผล

ไม่แนะนำ

CreateThread(function()

    while true do

        Wait(0)

        SendNUIMessage({
            action = 'update',
            cash = cash
        })

    end
end)

หาก cash เปลี่ยนทุก 10 นาที ก็ไม่มีเหตุผลส่งประมาณทุก Render Tick

㉛ ส่ง Message เมื่อข้อมูลเปลี่ยนดีกว่า

เช่นเงินเปลี่ยน

RegisterNetEvent(
    'player:moneyChanged',
    function(cash)

        SendNUIMessage({
            action = 'updateCash',
            cash = cash
        })
    end
)

Pattern นี้เป็น Event-driven

Value เปลี่ยน
↓
ส่ง Message

แทน

ถาม Value ซ้ำทั้งเวลา

㉜ ถ้าต้อง Poll ควรมี Wait

บางค่าอาจต้อง Poll

เช่น Speedometer

แต่ไม่จำเป็นต้อง Poll ทุกอย่างด้วย Wait(0)

ตัวอย่าง

CreateThread(function()

    while true do

        Wait(200)

        local ped =
            PlayerPedId()

        local vehicle =
            GetVehiclePedIsIn(
                ped,
                false
            )

        if vehicle ~= 0 then

            local speed =
                GetEntitySpeed(
                    vehicle
                )

            SendNUIMessage({
                action = 'speed',
                speed = speed
            })
        end
    end
end)

Frequency ที่เหมาะขึ้นกับ UX และระบบ

㉝ Cache ค่าเดิมก่อนส่งได้ไหม

ได้ และเป็น Pattern ที่ดี

local lastCash =
    nil

local function updateCash(
    cash
)

    if cash == lastCash then
        return
    end

    lastCash =
        cash

    SendNUIMessage({
        action = 'cash',
        value = cash
    })
end

หากค่าไม่เปลี่ยนก็ไม่ส่ง Message

㉞ ส่ง Object ใหญ่เกินไปมีผลไหม

มี Cost ด้าน

Serialization
Message transfer
Browser parsing
DOM rendering

สมมติ Inventory มี 10,000 Items

ไม่ควรส่งข้อมูลทั้งหมดใหม่ทุกครั้งที่ Item จำนวนหนึ่งเปลี่ยน

สามารถส่ง Delta Update

SendNUIMessage({
    action = 'updateSlot',
    slot = 5,
    item = item
})

แทน Reload Inventory ทั้งชุด

㉟ Full State กับ Delta Update ต่างกันอย่างไร

Full State

ส่ง Inventory ทุก Slot ใหม่

Delta

ส่งเฉพาะ Slot 5 ที่เปลี่ยน

Full State เหมาะตอนเปิด UI ครั้งแรก

Delta เหมาะตอนมี Update เล็ก ๆ ขณะ UI เปิด

㊱ UI ปิดแล้วยังต้อง Send Message ไหม

ขึ้นกับ State Architecture

ถ้า UI ปิดและข้อมูลไม่ต้อง Update Real-time ใน Browser อาจไม่ต้องส่งทุก Update

ตอนเปิดใหม่ส่ง Full State ล่าสุดทีเดียว

ลด Work ฝั่ง Browser

㊲ ตัวอย่าง State Guard

local uiOpen =
    false

local function updateInventory(
    inventory
)

    if not uiOpen then
        return
    end

    SendNUIMessage({
        action = 'inventory',
        items = inventory
    })
end

แต่ต้องมั่นใจว่าตอนเปิด UI จะส่ง State ล่าสุดใหม่เสมอ

㊳ เปิด UI ควรส่งข้อมูลพร้อมกันอย่างไร

ตัวอย่าง

local function openInventory(
    inventory
)

    SetNuiFocus(
        true,
        true
    )

    SendNUIMessage({
        action = 'openInventory',
        items = inventory
    })
end

Browser ได้ทั้ง

เปิด UI
+
ข้อมูลเริ่มต้น

ใน Message เดียว

㊴ แยก open กับ setData ได้ไหม

ได้

SendNUIMessage({
    action = 'setInventory',
    items = inventory
})

SendNUIMessage({
    action = 'open'
})

เหมาะถ้า Frontend Architecture แยก State กับ Visibility

แต่ต้องจัด Ordering ให้คาดเดาได้

㊵ Browser ต้องมี Message Router ไหม

Resource ใหญ่ควรมี

window.addEventListener(
    'message',
    (event) => {

        const data =
            event.data;

        switch (
            data.action
        ) {

            case 'open':
                openUi();
                break;

            case 'close':
                closeUi();
                break;

            case 'updateCash':
                updateCash(
                    data.cash
                );
                break;

            case 'vehicles':
                renderVehicles(
                    data.vehicles
                );
                break;
        }
    }
);

อ่านง่ายกว่า if หลายสิบชุดกระจายหลายไฟล์

㊶ ควร Validate Message ใน Browser ไหม

ควรในระดับ Frontend Robustness

เช่น

if (
    typeof data.cash
    !== 'number'
) {
    return;
}

แต่ Browser Validation ไม่ใช่ Security Boundary

Server ยังเป็น Authority ของข้อมูลสำคัญ

㊷ SendNUIMessage ปลอดภัยไหม

มันเป็น Client→NUI Communication

จึงไม่ได้ทำให้ข้อมูลกลายเป็น Secret

ข้อมูลที่ส่งเข้า Browser ควรถือว่า Player สามารถเข้าถึงได้

อย่าส่ง

Database credential
Private API key
Server-only formulas ที่ไม่จำเป็น
Admin secret

เข้า NUI

㊸ Server ควรส่งข้อมูลอะไรเข้า NUI

ส่งเฉพาะ Presentation Data

เช่น

Player display name
Cash
Vehicle plate
Vehicle model
Inventory items
Shop display prices

โดยระวังว่า Display Price ใน Client/NUI ไม่ควรถูกใช้เป็น Authority ตอนซื้อจริง

Server ควรคำนวณ Price อีกครั้ง

㊹ SetNuiFocus กับ Security เกี่ยวไหม

ไม่

การที่ NUI ไม่มี Focus ไม่ได้ป้องกัน Player จากการ Manipulate Client

และการที่ UI ปิดไม่ได้หมายความว่า Server Event ปลอดภัย

Security ต้องอยู่ที่

Server validation
Permissions
State validation
Rate limits
Authoritative calculations

㊺ UI เปิดแล้ว Player ควรเดินได้ไหม

ขึ้นกับ Design

ถ้า Inventory เปิดและคุณไม่ต้องการให้ Player เดิน

Focus/Input Architecture อาจ Block Gameplay Input

แต่ถ้าต้องการ Overlay ที่ Interaction น้อย ก็ออกแบบต่างได้

อย่าคิดว่า NUI ทุกประเภทต้องมี Focus เหมือนกัน

㊻ Resource Restart แล้ว Focus ค้างได้ไหม

ถ้า Resource ออกแบบ Cleanup ไม่ดี Player อาจพบ Input State แปลกหลัง Development Restart

ควรทำ Cleanup

AddEventHandler(
    'onClientResourceStop',
    function(resourceName)

        if resourceName ~=
            GetCurrentResourceName() then
            return
        end

        SetNuiFocus(
            false,
            false
        )
    end
)

นี่สำคัญมากสำหรับ Resource ที่ Restart บ่อย

㊼ Resource Stop ควรส่ง close message ไหม

สามารถทำ

AddEventHandler(
    'onClientResourceStop',
    function(resourceName)

        if resourceName ~=
            GetCurrentResourceName() then
            return
        end

        SetNuiFocus(
            false,
            false
        )

        SendNUIMessage({
            action = 'close'
        })
    end
)

แต่ Resource Runtime/NUI Lifecycle กำลังถูกหยุดอยู่ จึงควรมอง Focus Cleanup เป็นสิ่งสำคัญที่สุด

Frontend เองก็ควร Initialize State ใหม่ได้เมื่อ Resource Start อีกครั้ง

㊽ Resource Start ควร Reset NUI State ไหม

เป็นแนวทางที่ดี

AddEventHandler(
    'onClientResourceStart',
    function(resourceName)

        if resourceName ~=
            GetCurrentResourceName() then
            return
        end

        SetNuiFocus(
            false,
            false
        )

        SendNUIMessage({
            action = 'reset'
        })
    end
)

ช่วยให้ Development Restart ไม่ค้าง State จาก Logic ก่อนหน้า

㊾ JavaScript Error ทำให้ SendNUIMessage ดูเหมือนไม่ทำงานได้

ได้

Lua อาจส่ง Message ถูกต้อง

แต่ Browser Listener Error เช่น

document
    .getElementById('cash')
    .innerText = data.cash;

แต่ Element ไม่มี

JavaScript จะ Error

จึงควรดู NUI DevTools

㊿ Debug SendNUIMessage อย่างไร

ใส่

print(
    '[NUI] sending open'
)

SendNUIMessage({
    action = 'open'
})

Browser

window.addEventListener(
    'message',
    (event) => {

        console.log(
            '[NUI MESSAGE]',
            event.data
        );
    }
);

ถ้า Lua Log ขึ้นแต่ Browser Console ไม่ขึ้น ให้ตรวจ NUI Page/Manifest/JS

51 Debug SetNuiFocus อย่างไร

ถ้า UI เปิดแต่คลิกไม่ได้ ตรวจ

SetNuiFocus(true, true)?
NUI Resource อยู่บน Focus Stack หรือไม่?
Resource อื่นแย่ง Focus หรือไม่?
CSS pointer-events?
Overlay?
z-index?
JavaScript?

อย่าสรุปว่าเป็น SetNuiFocus ทุกกรณี

52 NUI DevTools ใช้แก้อะไรได้

Cfx.re มี NUI Developer Tools ซึ่งเปิดผ่านคำสั่ง nui_devTools ใน F8 เมื่อ Developer Mode เปิด

ใช้ตรวจ

Console
DOM
CSS
JavaScript
Network
NUI callbacks
Errors

มีประโยชน์มากกว่าการเดา UI ปัญหาจาก Lua อย่างเดียว

53 SendNUIMessage กับ SendNuiMessage ต่างกันไหม

ใน Lua Convenience Wrapper ใช้

SendNUIMessage(...)

เอกสาร Fullscreen NUI ยังแสดง JavaScript Client Runtime ใช้รูปแบบที่ส่ง JSON String ผ่าน SendNuiMessage

ดังนั้น Syntax ขึ้นกับ Runtime Language

บทความนี้ใช้ Lua เป็นหลัก

54 Lua SendNUIMessage ต้อง json.encode เองไหม

ไม่ต้องสำหรับ Wrapper ปกติ

ใช้

SendNUIMessage({
    action = 'open'
})

ได้เลย

Cfx.re ระบุว่า Lua Convenience Wrapper จะ Encode JSON String ให้

จึงไม่ต้องเขียน

SendNUIMessage(
    json.encode({...})
)

แบบสุ่ม

55 SetNuiFocus เรียกจาก Server ได้ไหม

นี่เป็น Client-side NUI Native

การเปิด UI ของ Player จาก Server ควร

Server
↓
TriggerClientEvent
↓
Client
↓
SetNuiFocus
↓
SendNUIMessage

ตัวอย่าง Server

TriggerClientEvent(
    'shop:open',
    source,
    shopData
)

Client

RegisterNetEvent(
    'shop:open',
    function(shopData)

        SetNuiFocus(
            true,
            true
        )

        SendNUIMessage({
            action = 'open',
            shop = shopData
        })
    end
)

56 อย่าให้ Server เปิด UI ทุก Tick

UI State ควรเป็น Event-driven

เช่น

Player กด E
↓
Client Request
↓
Server Validate
↓
Client Open UI

ไม่ใช่ Server ส่ง Event เปิด Menu ซ้ำทุกวินาที

57 ตัวอย่าง Full Resource

โครงสร้าง

com_shop/
├── fxmanifest.lua
├── client.lua
└── html/
    ├── index.html
    ├── style.css
    └── app.js

fxmanifest.lua

fx_version 'cerulean'
game 'gta5'

author 'comsiam'
description 'FiveM NUI message example'
version '1.0.0'

ui_page 'html/index.html'

files {
    'html/index.html',
    'html/style.css',
    'html/app.js'
}

client_script 'client.lua'

client.lua

local uiOpen =
    false

local function setUi(
    state,
    data
)

    uiOpen =
        state

    SetNuiFocus(
        state,
        state
    )

    SendNUIMessage({
        action =
            state
            and 'open'
            or 'close',

        data = data
    })
end

RegisterCommand(
    'shopui',
    function()

        if uiOpen then

            setUi(
                false
            )

            return
        end

        setUi(
            true,
            {
                title =
                    'FiveM Shop',

                items = {
                    {
                        name = 'water',
                        label = 'Water',
                        price = 10
                    },
                    {
                        name = 'bread',
                        label = 'Bread',
                        price = 15
                    }
                }
            }
        )
    end,
    false
)

RegisterNuiCallback(
    'close',
    function(data, cb)

        setUi(
            false
        )

        cb({
            ok = true
        })
    end
)

AddEventHandler(
    'onClientResourceStop',
    function(resourceName)

        if resourceName ~=
            GetCurrentResourceName() then
            return
        end

        SetNuiFocus(
            false,
            false
        )
    end
)

58 JavaScript สำหรับ Resource ตัวอย่าง

const menu =
    document.getElementById(
        'menu'
    );

const title =
    document.getElementById(
        'title'
    );

const items =
    document.getElementById(
        'items'
    );

window.addEventListener(
    'message',
    (event) => {

        const message =
            event.data;

        if (
            message.action
            === 'open'
        ) {

            title.textContent =
                message.data?.title
                ?? 'Shop';

            renderItems(
                message.data?.items
                ?? []
            );

            menu.classList.add(
                'active'
            );
        }

        if (
            message.action
            === 'close'
        ) {

            menu.classList.remove(
                'active'
            );
        }
    }
);

function renderItems(
    shopItems
) {

    items.innerHTML =
        '';

    for (
        const item
        of shopItems
    ) {

        const element =
            document.createElement(
                'div'
            );

        element.textContent =
            `${item.label} - ${item.price}`;

        items.appendChild(
            element
        );
    }
}

59 Checklist SendNUIMessage และ SetNuiFocus

ตรวจ

① Resource มี ui_page หรือไม่?
② NUI files อยู่ใน Manifest หรือไม่?
③ SendNUIMessage เรียกจาก Client หรือไม่?
④ Message เป็น JSON-encodable หรือไม่?
⑤ Browser มี message listener หรือไม่?
⑥ action/type ชื่อตรงกันหรือไม่?
⑦ UI เปิดด้วย SetNuiFocus เมื่อจำเป็นหรือไม่?
⑧ HUD ที่ไม่ Interactive เปิด Focus เกินจำเป็นหรือไม่?
⑨ ปิด UI แล้ว SetNuiFocus(false,false) หรือไม่?
⑩ ESC ปิดผ่าน Client หรือไม่?
⑪ Resource Stop คืน Focus หรือไม่?
⑫ Resource Start Reset State หรือไม่?
⑬ UI หลายตัวแย่ง Focus หรือไม่?
⑭ ส่ง Secrets เข้า NUI หรือไม่?
⑮ Payload ใหญ่เกินไปหรือไม่?
⑯ ส่ง Full State ทั้งชุดทุกครั้งหรือไม่?
⑰ ใช้ Delta Update ได้หรือไม่?
⑱ SendNUIMessage ทุก Tick โดยไม่จำเป็นหรือไม่?
⑲ มี Cache ค่าเดิมก่อนส่งหรือไม่?
⑳ Browser Timers ยังทำงานตอน UI ปิดหรือไม่?
㉑ DOM Update หนักหรือไม่?
㉒ JavaScript Error หรือไม่?
㉓ CSS pointer-events ถูกหรือไม่?
㉔ Overlay/Z-index ขวาง Click หรือไม่?
㉕ NUI DevTools ตรวจแล้วหรือยัง?
㉖ Server Data ผ่าน Client ก่อน NUI หรือไม่?
㉗ Server ยังเป็น Authority หรือไม่?
㉘ Display Price ถูกนำกลับไปเชื่อโดย Server หรือไม่?
㉙ Restart Resource แล้ว Focus ปกติหรือไม่?
㉚ เปิด/ปิด UI ซ้ำหลายครั้งแล้วยังปกติหรือไม่?

⑥⓪ Architecture ที่แนะนำสำหรับ Production

Resource ใหญ่ควรแยก

Client
├── ui state
├── SendNUIMessage
├── SetNuiFocus
└── NUI callbacks

Browser
├── presentation
├── animations
├── local UI state
└── user input

Server
├── validation
├── permissions
├── business logic
└── database

ตัวอย่างเปิด Garage

Player ขอเปิด Garage
↓
Client
↓
Server ตรวจ Garage / Player
↓
Server ส่ง Vehicles
↓
Client
↓
SetNuiFocus(true,true)
↓
SendNUIMessage
↓
Browser Render Vehicles

Player เลือกรถ

Browser
↓
NUI Callback
↓
Client
↓
Server
↓
Validate Vehicle Ownership
↓
Server Create / Approve Vehicle
↓
Client
↓
SendNUIMessage Result
↓
Browser Update

นี่เป็น Flow ที่แบ่ง Client, Browser และ Server ได้ชัดเจน

SendNUIMessage ไม่ได้สร้าง UI ให้เอง

ถ้าคุณส่ง

SendNUIMessage({
    action = 'open'
})

แต่ JavaScript ไม่มี

window.addEventListener(
    'message',
    ...
);

ไม่มีอะไรเกิดขึ้น

SendNUIMessage เป็นเพียง Communication Channel

Browser Code ต้องเป็นผู้ตัดสินว่าจะทำอะไรกับ Message

SetNuiFocus ไม่ได้เปิด HTML ให้เอง

เช่นเดียวกัน

SetNuiFocus(
    true,
    true
)

ไม่ได้ทำให้ Menu Visible อัตโนมัติ

มันเพียงเปลี่ยน Input Focus

โดยทั่วไปจึงใช้คู่กัน

SetNuiFocus(
    true,
    true
)

SendNUIMessage({
    action = 'open'
})

UI State ต้องมี Source of Truth ที่ชัดเจน

ตัวอย่างไม่ดี

Lua คิดว่า UI ปิด
JavaScript คิดว่า UI เปิด
Focus ยัง true

เกิด State Mismatch

ควรมี Function กลาง

local function setUi(
    state
)
    -- update state
    -- update focus
    -- notify NUI
end

เพื่อลดความไม่ตรงกัน

Browser State ไม่ควรแทน Server State

UI อาจแสดง

Cash = 100,000

แต่ผู้เล่นแก้ DOM เป็น

Cash = 999,999,999

ไม่ได้หมายความว่า Server Cash เปลี่ยน

และ Server ไม่ควรรับค่ากลับจาก Browser แล้วเชื่อ

Server ต้องถือข้อมูล Economy ของตัวเอง

Performance ของ NUI ต้องดูสองฝั่ง

อย่าดูแค่

resmon

เพราะ Client Script อาจเบาแต่ Browser ทำงานหนักจาก

DOM
CSS Blur
JavaScript
Animations
Video
Canvas
WebGL

ควรใช้ทั้ง FiveM Profiler/Resmon และ NUI DevTools ตามปัญหา

ตัวอย่างลด Message สำหรับ HUD

แบบไม่ดี

60 FPS
×
Health
×
Armor
×
Cash
×
Job
×
Food
×
Water

ส่งใหม่ทุก Frame

แบบที่ดีกว่าอาจแยก

Speed
→ update ถี่พอสำหรับ UI

Cash
→ update ตอนเปลี่ยน

Job
→ update ตอน Job เปลี่ยน

Food
→ update เมื่อค่าเปลี่ยน/ช่วงเวลาที่เหมาะ

ไม่จำเป็นต้องมี Frequency เดียวกันทุกข้อมูล

คำถามที่พบบ่อยเกี่ยวกับ SendNUIMessage และ SetNuiFocus

FiveM SendNUIMessage คืออะไร

ใช้ส่งข้อมูลจาก Client Script ไปยัง NUI Page ของ Resource

SendNUIMessage ทำงาน Server-side ไหม

ใช้กับ Client-side NUI Communication หาก Server ต้องส่งข้อมูลให้ UI ให้ส่ง Server→Client ก่อน

JavaScript รับข้อมูลอย่างไร

ใช้

window.addEventListener(
    'message',
    (event) => {
        console.log(
            event.data
        );
    }
);

SendNUIMessage ต้อง json.encode เองไหม

Lua Convenience Wrapper จัดการ JSON Encoding ให้

ส่ง Table ได้ไหม

ได้หาก Serialize เป็น JSON ได้

ส่ง Array ได้ไหม

ได้

ส่งข้อมูล Player ได้ไหม

ได้ แต่ส่งเฉพาะข้อมูลที่ Client/NUI ควรรู้

SetNuiFocus คืออะไร

ใช้กำหนด Keyboard Focus และ Mouse Cursor Focus ให้ NUI

เปิด Mouse Cursor อย่างไร

SetNuiFocus(
    true,
    true
)

ปิด Mouse Cursor อย่างไร

SetNuiFocus(
    false,
    false
)

HUD ต้อง SetNuiFocus ไหม

ไม่ หาก HUD มีไว้แสดงผลอย่างเดียว

Inventory ต้องใช้ Focus ไหม

โดยทั่วไปต้องใช้หาก Player ต้องคลิกหรือใช้ Keyboard กับ UI

UI ปิดแต่เมาส์ค้างแก้อย่างไร

คืน Focus ด้วย

SetNuiFocus(
    false,
    false
)

ปิด UI ด้วย JavaScript อย่างเดียวได้ไหม

ไม่ควรหาก NUI มี Focus เพราะต้องแจ้ง Client ให้คืน Focus ด้วย

Resource Restart แล้ว Mouse ค้างแก้อย่างไร

ทำ Cleanup ใน onClientResourceStop

SendNUIMessage ทุก Frame ได้ไหม

ทำได้ แต่ไม่ควรหากข้อมูลไม่ต้องการความถี่ระดับนั้น

SendNUIMessage ทำให้ FPS ตกได้ไหม

การส่ง Message จำนวนมากและ Browser Rendering หนักสามารถเพิ่ม Work ฝั่ง Client ได้ จึงควรส่งเท่าที่จำเป็น

Full State กับ Delta Update แบบไหนดีกว่า

ใช้ Full State ตอน Initialize และ Delta เมื่อมีการเปลี่ยนเล็ก ๆ มักเป็น Architecture ที่มีประสิทธิภาพกว่า

NUI หลาย Resource เปิดพร้อมกันได้ไหม

ได้ แต่ต้องระวัง Focus Stack และ UI ที่อยู่ด้านบน

Click ผ่าน NUI ด้านบนไป UI ด้านล่างได้ไหม

Cfx.re ระบุว่า Fullscreen NUI Resources ปัจจุบันไม่มี Click-through ข้าม Resources

SetNuiFocus เปิด UI หรือไม่

ไม่ มันกำหนด Input Focus เท่านั้น

SendNUIMessage เปิด Cursor หรือไม่

ไม่ ต้องใช้ SetNuiFocus

Server สามารถเชื่อข้อมูลจาก NUI ได้ไหม

ไม่ควร Client/NUI Input ต้อง Validate ฝั่ง Server เมื่อมีผลต่อ Gameplay, Economy หรือ Permissions

จะ Debug NUI ที่ไหน

ใช้ F8 และ NUI Developer Tools รวมถึง Browser Console/Network

nui_devTools คืออะไร

คำสั่งสำหรับเปิด NUI DevTools เมื่อ Developer Mode เปิดตาม Cfx.re Documentation

สรุป FiveM SendNUIMessage และ SetNuiFocus ใช้อย่างไร

สองคำสั่งนี้ทำหน้าที่ต่างกันชัดเจน

SendNUIMessage
→ ส่งข้อมูล Client → Browser

ส่วน

SetNuiFocus
→ กำหนด Keyboard/Mouse Focus ให้ Browser

เปิด Interactive UI

SetNuiFocus(
    true,
    true
)

SendNUIMessage({
    action = 'open'
})

ปิด

SetNuiFocus(
    false,
    false
)

SendNUIMessage({
    action = 'close'
})

Browser รับด้วย

window.addEventListener(
    'message',
    (event) => {

        const data =
            event.data;

        if (
            data.action
            === 'open'
        ) {
            // open
        }
    }
);

หลักที่สำคัญคือ

UI ไม่ Interactive
→ ไม่ต้อง Focus

UI Interactive
→ Focus เมื่อเปิด
→ คืน Focus เมื่อปิด

สำหรับระบบที่ Update ข้อมูลบ่อย อย่ายิง Message ทุก Frame โดยอัตโนมัติ แต่ให้เลือก Frequency ตามข้อมูล หรือใช้ Event-driven Update

เช่น

Money
→ ส่งตอนเปลี่ยน

Job
→ ส่งตอนเปลี่ยน

Inventory
→ ส่งตอนเปิด/Item เปลี่ยน

Speed
→ ส่งเป็นช่วงที่เหมาะกับความลื่นของ UI

อีกเรื่องที่ต้องระวังคือ Fullscreen NUI มี Focus Stack ดังนั้นหาก Inventory, Phone และ Admin Menu เปิดพร้อมกันโดยแต่ละ Resource จัด Focus ของตัวเอง อาจทำให้ Input State สับสนได้

สำหรับผู้ที่พัฒนา FiveM กับ comsiam การออกแบบ NUI ที่ดีจึงต้องมี UI State กลาง ไม่กระจาย SetNuiFocus และ SendNUIMessage ไปทั่ว Resource จนไม่รู้ว่าใครเป็นคนเปิดหรือปิด Interface

หลักสำคัญจาก comsiam คือ ส่งเฉพาะข้อมูลที่เปลี่ยนและคืน Focus ทุกครั้งที่ UI ปิด สองอย่างนี้ช่วยป้องกันทั้ง NUI Message ที่มากเกินจำเป็นและปัญหา Mouse/Keyboard ค้างที่ผู้เล่นพบกันบ่อย

หัวข้อถัดไปคือ FiveM Profiler คืออะไร หา Script กิน CPU อย่างไร ซึ่งจะเริ่มเข้าสู่ Performance โดยอธิบาย Profiler, Resource Tick, Threads, Natives, Events, Database Work และวิธีหา Code ที่ทำให้ Server หรือ Client ใช้ CPU สูงแบบวัดจากข้อมูลจริง

Comments

Popular posts from this blog

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

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

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