キャラクターUIを別resourceに分ける理由
前回までに、HaruuuCore では character slot、active / deleted の区別、create / select / delete の分離を整理しました。 次に必要になるのは、その操作をゲーム画面から扱うための character selection UI です。
ただし、UI resource に character DB の実体まで持たせると、責務がすぐに混ざります。
UI は画面を開く、閉じる、slot payload を表示する、button 操作を server に送るところまでを担当し、character の作成・選択・削除そのものは hc_character に任せます。
この作業では、hc_character_ui を追加し、/charui から character selection UI を開き、slot の表示、character の選択、作成、削除を画面から操作できる状態まで確認しました。
hc_character_uiが担当する範囲
hc_character_ui は、character selection の UI / NUI / client state / focus cleanup を担当する resource です。
character DB の実体や slot payload の生成は担当しません。
hc_character_ui:
/charui
/charui_close
/charui_cleanup
NUI open / close
NUI focus cleanup
requestId 管理
request timeout
stale response ignore
getSlots request
selectCharacter request
createCharacter request
deleteCharacter request
create form
delete confirmation modalこの resource は、画面操作の入口です。
UI が必要な情報は server event で要求し、server 側で hc_character の exports を呼び、その結果を client / NUI へ返します。
NUI
↓
hc_character_ui client
↓
hc_character_ui server
↓
hc_character exportsUI 側から直接 DB を触らないことで、UI の見た目や操作が変わっても、character 管理の正本は hc_character に残せます。
NUIで詰まりやすいポイント
FiveM の NUI は、見た目を作るだけなら通常の HTML / CSS / JavaScript に近い感覚で扱えます。 ただし、ゲーム操作と重なるため、閉じ方や focus の戻し方を曖昧にすると、プレイヤーが操作不能に近い状態になることがあります。
特に問題になりやすいのは、NUI focus が残ること、server response を待ち続けること、古い response が後から返って UI を壊すこと、modal が閉じないことです。
詰まりやすい状態:
UIを閉じたのにNUI focusが残る
request中のままbuttonが戻らない
timeout後に古いresponseが反映される
modalが残ってESCで戻れない
resource停止時にfocusが残るそのため、hc_character_ui では、画面を作ることよりも先に、閉じる経路、cleanup、request timeout、stale response ignore を決めています。
/charuiで開き、CloseとESCで閉じる
character UI は、開発中の確認用として /charui から開きます。
すでに開いている状態で /charui を実行した場合は、toggle として閉じる扱いにします。
/charuiUI を開くと、client 側で NUI focus を取り、NUI に open message を送ります。
その後、slot payload を取得するために server へ getSlots request を送ります。
/charui
↓
isOpen = true
SetNuiFocus(true, true)
SendNUIMessage({ action = "open" })
↓
getSlots request閉じる経路は、Close button、ESC、/charui toggle、/charui_close、/charui_cleanup、select 成功時、resource stop の複数を用意しています。
どの経路でも、最終的には focus を戻す処理へ集約します。
NUI focusを必ず戻す
NUI を開いた resource は、自分で focus を戻す責務を持ちます。
SetNuiFocus(true, true) で UI 操作へ移ったなら、閉じるときには必ず SetNuiFocus(false, false) を実行します。
cleanup:
isOpen = false
isRequesting = false
currentRequestId を進める
SetNuiFocus(false, false)
SendNUIMessage({ action = "close" })currentRequestId を進めるのは、閉じたあとに古い server response が返ってきても無視できるようにするためです。
UI を閉じたのに response だけが後から届き、再度 loading や error を反映してしまう状態を避けます。
resource stop 時にも cleanup を実行します。 開発中に resource を restart したとき、focus が残ると検証が止まりやすいためです。
requestIdで古いresponseを無視する
NUI から server へ request を送る処理では、毎回 requestId を発行します。
server はその requestId を response に含めて返します。
client:
currentRequestId = currentRequestId + 1
requestId = currentRequestId
TriggerServerEvent(..., requestId)
server:
TriggerClientEvent(..., requestId, ok, payload, reason)
client:
response.requestId == currentRequestId のときだけ反映するこれにより、古い response が後から返ってきても UI へ反映しません。 request timeout、UI close、resource cleanup のあとに遅延 response が届いても、現在の UI state を壊さないようにします。
この仕組みは、getSlots、selectCharacter、createCharacter、deleteCharacter のすべてで共通に使います。
timeoutで待ちっぱなしを防ぐ
server から response が返らない場合に備えて、client 側では request timeout を持たせます。 現在の timeout は 5000ms です。
REQUEST_TIMEOUT_MS = 5000
timeout時:
isRequesting = false
loading を解除
request_timeout を表示
UI は閉じない
再操作可能にするtimeout しても UI は閉じません。 閉じてしまうと、何が起きたのか分からないままゲーム画面に戻るためです。 error を表示して、Refresh や再操作ができる状態に戻します。
また、timeout 後に server response が返ってきても、requestId が進んでいれば stale response として無視します。
slot payloadを画面に表示する
UI を開いた直後と Refresh button 押下時には、slot payload を再取得します。
server 側では hc_character の getCharacterSlotsForSelection(source) を呼びます。
NUI Refresh
↓
hc_character_ui:server:getSlots
↓
exports["hc_character"]:getCharacterSlotsForSelection(source)
↓
hc_character_ui:client:getSlotsResult
↓
SendNUIMessage({ action = "setSlots", payload = payload })UI 側では、maxSlots、selectedCharacterId、selectedSlot、各 slot の state を表示します。
slot が occupied なら character 情報を出し、empty なら create button を出します。
slot card:
state
selected / not selected
character_id
name
model
canSelect
canCreate
canDelete
deleteBlockReason表示はあくまで payload に従います。
UI 側で「この slot は削除できるはず」と推測せず、server から返された canDelete や deleteBlockReason をそのまま使います。
Select成功時だけUIを閉じる
occupied slot の Select button を押すと、client は server へ selectCharacter request を送ります。
server 側では hc_character の selectCharacter(source, slot) を呼びます。
Select button
↓
hc_character_ui:server:selectCharacter
↓
exports["hc_character"]:selectCharacter(source, slot)
↓
select success
↓
UI close
SetNuiFocus(false, false)select が成功した場合だけ UI を閉じます。
character が選択されると、hc_character から hc_core へ character selected が伝わり、player state は ready 側へ進みます。
一方で、select に失敗した場合は UI を閉じません。 error を表示し、再操作できる状態に戻します。
select failed:
UI は閉じない
error を表示する
loading を解除する
再操作可能にするCreate後は閉じずにrefreshする
empty slot の Create button を押すと、NUI 内で create form を表示します。 現時点では、入力するのは first name と last name だけです。
create form:
firstName
lastName
固定値:
gender = unknown
model = mp_m_freemode_01Create を実行すると、client は server へ createCharacter request を送ります。
server 側では hc_character の createCharacter(source, slot, data) を呼びます。
Create button
↓
create form
↓
hc_character_ui:server:createCharacter
↓
exports["hc_character"]:createCharacter(source, slot, data)
↓
create success
↓
getSlots refreshcreate が成功しても UI は閉じません。 作成した character を自動選択せず、slot payload を再取得して、empty だった slot が occupied に変わったことを画面に反映します。
作成と選択を分けることで、「キャラクターを作る」と「そのキャラクターで開始する」を別の操作として扱えます。
Delete後も閉じずにrefreshする
occupied slot の Delete button を押すと、NUI 内で delete confirmation modal を表示します。
確認後、client は server へ deleteCharacter request を送ります。
Delete button
↓
delete confirmation modal
↓
hc_character_ui:server:deleteCharacter
↓
exports["hc_character"]:deleteCharacter(source, slot)
↓
delete success
↓
getSlots refreshdelete が成功しても UI は閉じません。 slot payload を再取得し、削除された slot が empty へ変わった状態を表示します。
現在選択中 character は削除できません。
その場合、payload 側で canDelete=false とし、UI では Delete button を disabled にします。
selected character:
isSelected = true
canDelete = false
deleteBlockReason = character_currently_selected選択中 character の削除を許可すると、core state、money target、job、spawn なども同時に整理する必要があります。 そのため、現時点の UI では削除不可として扱います。
window.promptを使わない
初期検証では簡単な入力方法として window.prompt を使いたくなります。
しかし、FiveM NUI / CEF 上では、標準の prompt / alert / confirm に依存しない方針にしています。
使わない:
window.prompt
window.alert
window.confirm
使う:
NUI内のHTML form
NUI内のmodal
button event
fetch callback標準 dialog に頼ると、focus や入力状態の管理が FiveM 側の NUI cleanup と噛み合いにくくなります。 そのため、create は HTML form、delete は confirmation modal として実装します。
modal を閉じる経路も明示します。 Cancel button、上部 Cancel button、ESC で modal だけを閉じ、modal が開いていない状態で ESC を押した場合は UI 全体を閉じます。
error表示だけ人間向けに変換する
server reason は内部識別子として扱います。
invalid_slot や character_currently_selected のような reason は、server log や client log ではそのまま残します。
ただし、NUI 上ではそのまま表示すると分かりにくい場合があります。
そのため、NUI 表示時だけ formatErrorMessage で人間向けの文言に変換します。
server reason:
character_currently_selected
NUI表示:
You cannot delete the currently selected character.この方針により、内部の reason は変えず、UI 表示だけを調整できます。 server / txAdmin log の追跡性と、画面上の分かりやすさを分けて扱います。
hc_character_uiが担当しない範囲
hc_character_ui は、character selection の画面操作に集中します。
そのため、見た目として将来必要になりそうなものでも、現時点では入れないものを明確にしています。
hc_character_ui が担当しないこと:
character DB 操作の実体
character slot payload 生成の実体
active / deleted character 判定の実体
Players[source] 管理
hc_core state 管理
hc_money cash / bank 管理
hc_progression rank / XP 管理
job
spawn 処理
camera 制御
player freeze
screen fade
preview ped
appearance 編集
money 表示
rank 表示これらを UI resource に入れると、character selection UI が spawn system や money system まで抱えることになります。 まずは UI の開閉、request、response、cleanup に絞り、他の gameplay system は別 resource に残します。
確認した操作
character UI の挙動は、command と画面操作で確認しました。 目的は、UI から slot payload を取得し、Select / Create / Delete を実行しても、NUI focus や request state が残らないことを確認することです。
/charui/charui で UI を開き、slot 1〜3 の payload が表示されることを確認します。
画面を開いた直後に getSlots request が実行されます。
/charui_close
/charui_cleanupClose button や ESC とは別に、開発用保険として強制 close / cleanup 用 command も用意しています。 通常運用で使うものではなく、検証中に focus が残った場合の確認用です。
確認したUI操作:
Close button で閉じる
ESC で閉じる
Refresh でslot payloadを再取得する
empty slot からCreate formを開く
Create成功後にslot payloadをrefreshする
occupied slot からSelectする
Select成功後にUIを閉じる
occupied slot からDelete modalを開く
Delete成功後にslot payloadをrefreshする
selected character のDelete buttonをdisabledにするキャラクターUIで到達した状態
この作業で、HaruuuCore は server command だけで character を操作する段階から、NUI 上の character selection UI で操作できる段階へ進みました。
確認済み:
/charui で character UI を開ける
Close / ESC で UI を閉じられる
NUI focus を戻せる
resource stop 時に cleanup できる
requestId で古い response を無視できる
timeout で待ちっぱなしを防げる
slot 1〜3 を UI に表示できる
Select 成功後に UI を閉じられる
Create 成功後に UI を閉じず refresh できる
Delete 成功後に UI を閉じず refresh できる
現在選択中 character の Delete を disabled にできる
server reason を NUI 表示用 message に変換できるまだ扱っていないものもあります。 spawn 制御、camera 制御、freeze、fade、preview ped、appearance 編集、money 表示、rank 表示、自動 UI 表示、last selected 保存などは未実装です。
ただし、character selection UI の開閉、focus cleanup、request timeout、stale response ignore、Select / Create / Delete の画面操作が入ったことで、ゲーム内から character を選ぶ導線の土台は作れました。