FiveMでキャラクターUIを作る【NUIの詰まりを避ける設計にします】

HaruuuCoreのhc_character_uiで、character selection UI、NUI focus cleanup、requestId、timeout、stale response ignoreをどのように扱うかを整理します。

この記事の見出し 開く

キャラクター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 exports

UI 側から直接 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 として閉じる扱いにします。

/charui

UI を開くと、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 を壊さないようにします。

この仕組みは、getSlotsselectCharactercreateCharacterdeleteCharacter のすべてで共通に使います。

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_charactergetCharacterSlotsForSelection(source) を呼びます。

NUI Refresh

hc_character_ui:server:getSlots

exports["hc_character"]:getCharacterSlotsForSelection(source)

hc_character_ui:client:getSlotsResult

SendNUIMessage({ action = "setSlots", payload = payload })

UI 側では、maxSlotsselectedCharacterIdselectedSlot、各 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 から返された canDeletedeleteBlockReason をそのまま使います。

Select成功時だけUIを閉じる

occupied slot の Select button を押すと、client は server へ selectCharacter request を送ります。 server 側では hc_characterselectCharacter(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_01

Create を実行すると、client は server へ createCharacter request を送ります。 server 側では hc_charactercreateCharacter(source, slot, data) を呼びます。

Create button

create form

hc_character_ui:server:createCharacter

exports["hc_character"]:createCharacter(source, slot, data)

create success

getSlots refresh

create が成功しても 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 refresh

delete が成功しても 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_slotcharacter_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_cleanup

Close 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 を選ぶ導線の土台は作れました。

見出しへ戻る 開く