FiveM Scriptの基礎と開発について
今までFiveMのScriptを開発してきたノウハウを備忘録として残しています。
一部はまだ動作未検証の部分があったり、環境依存のものもあります。筆者の環境は、QBCoreやox_lib、ox_target、ox_inventoryを使用している環境です。
ESXには対応していないのでご注意ください。
FiveM scriptの構成と役割
・fxmanifest.lua ・・・スクリプト構成を記載(記載のないものは読み込まれない)
・client.lua・・・クライアント側で動作するスクリプト(モーションなど)
・server.lua・・・サーバ側で動作するスクリプト(アイテム、お金の付与など)
・config.lua(shard.lua)・・・スクリプトを動作させるための設定ファイル(座標など)
ざっくりイメージ図
fxmanifest.luaについて
スクリプトの構成を記載しており、記載のないものは読み込まれない。
fx_version 'cerulean'
game 'gta5'
author 'KumaRider' --作成者名
description 'Gacha System with Sound for QBCore' --スクリプトの説明文
version '1.1.0' --スクリプトのバージョン
-- サーバースクリプト
server_scripts {
'server/server.lua',
'shared/shared.lua', --server.luaでshard.luaを使用したいため記載
}
-- クライアントスクリプト
client_scripts {
'client/client.lua',
}
--ここまでは基本的なスクリプトの構成
--ここから下はスクリプトによって必要なものを記載する。
-- QBCoreと依存関係があることを宣言
dependencies {
'qb-core',
}
-- NUIファイル
ui_page 'html/index.html'
-- 必要なファイル
files {
'html/index.html',
'html/script.js',
'html/sounds/*.ogg',
}
クライアントサイドからサーバサイドの呼び出し
TriggerServerEvent('gacha:roll', ticketType)
RegisterNetEvent('gacha:roll')
AddEventHandler('gacha:roll', function(ticketType)
処理内容
end)
TriggerServerEventでサーバに対してイベントを送信(発火)する。
RegisterNetEventで発火したイベントをフックする。
AddEventHandlerで実際にどう処理をするのか記載する。
サーバサイドからクライアントサイドの呼び出し
TriggerClientEvent('gacha:openGacha', source, ticketType)
RegisterNetEvent('gacha:openGacha')
AddEventHandler('gacha:openGacha', function(ticketType)
処理内容
end)
TriggerClientEcentでクライアントに対してイベントを送信(発火)する。
RegisterNetEventで発火したイベントをフックする。
AddEventHandlerで実際にどう処理をするのか記載する。
通知メッセージ(qbcore、ox_lib)
処理としては、サーバからqbcore,ox_libのイベントをクライアントに発火させる。
TriggerClientEvent('QBCore:Notify', src, 'はずれ', 'success')
[src]がプレイヤー、[はずれ]がメッセージ、[success]が情報レベルでの表示
TriggerClientEvent('ox_lib:notify', src, 'You have received', 'success')
[src]がプレイヤー、[You hab received]がメッセージ、[success]が情報レベルでの表示
お金の取引(cash、bank)
処理は、サーバ側での処理で実施すること。(不正防止のため)
--ユーザ指定
local Player = QBCore.Functions.GetPlayer(src)
--銀行口座での取引(受取)
Player.Functions.AddMoney('bank', 1000, 'Transaction_Log')
--銀行口座での取引(支払)
Player.Functions.RemoveMoney('bank', 1000, 'Transaction_Log')
--ユーザ指定
local Player = QBCore.Functions.GetPlayer(src)
--現金での取引(受取)
Player.Functions.AddMoney('cash', 1000, 'Transaction_Log')
--現金での取引(支払)
Player.Functions.RemoveMoney('cash', 1000, 'Transaction_Log')
Transaction_Logは任意の文字列で、なんの取引内容かを特定するためのテキストを入力する。
アイテムの取引
アイテムの取引処理もサーバ側で実施すること。(不正防止のため)
cashはアイテムとしてもあるため、以下でのコードでも付与可能。
--ユーザ指定
local Player = QBCore.Functions.GetPlayer(src)
--アイテムの付与
Player.Functions.AddItem(item_name, 10)
--アイテム付与の通知
TriggerClientEvent('inventory:client:ItemBox', src, item_name, "add")
--アイテムの削除
Player.Functions.RemoveItem(item_name, 10)
--アイテム削除の通知(未検証)
TriggerClientEvent('inventory:client:ItemBox', src, item_name, "remove")
--cashの付与
Player.Functions.AddItem(cash, 10)
--cashの削除
Player.Functions.RemoveItem(cash, 10)
アイテム使用のフック
QBCore.Functions.CreateUseableItem('example_item', function(source, item)
処理内容
end)
※検証している中でox_inventoryからアイテム使用を以下で検知しようとしたが、うまく動作しなかったため、基本的には、QBCoreでフックするようにしている。
(2024.10.15追記)
アイテムの使用をフックするのではなく、アイテムを使用できるように登録するニュアンスのほうが正しい。
--うまくフックできなかったコード
RegisterNetEvent('ox_inventory:useItem', function(data)
座標と向きと範囲
vector3やvector4で確認する。(vector4は向きも込)
config.luaで設定するときは範囲も含めて以下のように記述することが多い。
coords = vector3(31.84, -170.77, 54.42), --3つ目は高さ
heading = 162.9, --向き
radius = 5.0 --範囲
範囲は、球体の範囲になるので、広げると上下にも広がるので注意が必要
アニメーション関連
client.luaに定義する。amb@prop_human_bum_bin@baseがアニメーションの種類。
TaskPlayAnim(playerPed, "amb@prop_human_bum_bin@base", "base", 8.0, -8.0, 3000, 16, 0, false, false, false)
アニメーションの種類は以下のサイトに纏められており、猫のアニメーションなども記載されている。
オブジェクト(モデル)関連
オブジェクトは、クライアント側で生成する。
以下のコードは、自動販売機のモデルを生成するもので、config.luaで管理する構成でのコードである。
Config.VendingMachines = {
{
id = 'Vending_Machine_001',
model = 'prop_vend_snak_01', -- 食品の自動販売機
coords = vector3(-572.53, -1071.41, 21.33), -- 猫カフェ前座標
heading = 0.0,
Citizen.CreateThread(function()
-- Configで定義された各自動販売機を配置
for _, machineConfig in ipairs(Config.VendingMachines) do
local vendingMachineModel = GetHashKey(machineConfig.model)
local vendingMachineCoords = machineConfig.coords
local vendingMachineHeading = machineConfig.heading
-- モデルがロードされているか確認
RequestModel(vendingMachineModel)
while not HasModelLoaded(vendingMachineModel) do
Citizen.Wait(500)
end
-- 自動販売機を生成し、配置
local vendingMachineEntity = CreateObject(vendingMachineModel, vendingMachineCoords.x, vendingMachineCoords.y, vendingMachineCoords.z, true, false, true)
SetEntityHeading(vendingMachineEntity, vendingMachineHeading) --向き
FreezeEntityPosition(vendingMachineEntity, true) --動かないよう設定
SetModelAsNoLongerNeeded(vendingMachineModel)
end
end)
解説メモ
• CreateObject は、指定したモデルを指定した座標に生成するための関数。
• 第1引数:ハッシュ化された自動販売機のモデル。
• 第2〜4引数:生成する場所の座標(X, Y, Z)。
• 第5引数(true):物理的な衝突を有効にするかどうか。
• 第6引数(false):生成されたオブジェクトが動的に生成されるかどうか(falseで静的なオブジェクト)。
• 第7引数(true):ネットワーク化するかどうか(true で他のプレイヤーとも同期されます)。
以下の2つのサイトからObjectNameを検索して、組み込んでいます。
ox_targetとメニュー
心の目で見た際に出てくる項目を出すためのコード
-- ox_target Interaction (座標ベースのターゲットゾーンを追加)
for _, machineConfig in ipairs(Config.VendingMachines) do
exports['ox_target']:addSphereZone({
coords = machineConfig.coords,
radius = 2.0,
options = {
--商品購入の項目(1つ目)
{
name = 'vendingMachineBuy',
label = '商品を購入する',
icon = 'fa-solid fa-shopping-cart',
onSelect = function(data)
showProductMenu(machineConfig.coords)
end
},
--売上金回収の項目(2つ目)
{
name = 'vendingMachineCollect',
label = '売上金を回収する',
icon = 'fa-solid fa-money-bill-wave',
onSelect = function(data)
collectEarnings(machineConfig.coords)
end
}
}
})
end
以下の画像は3つだけど、「商品を購入する」と「売上金を回収する」を表示するためのコード。
選択後の処理(onSelect)は、別の関数に飛ばして処理を記載している。
おそらく直接ここに記載しても動作すると思われる。
HTMLとか、JavaScriptとか
NUIを利用して連携する事が可能。
ゲーム内に以下のようにHTMLベースのページを表示させ、JavaScriptと連携させて商品の購入等を実装。
具体的なJavaScriptとの連携については、別の記事を作成予定。
(簡単な連携は以下の音声鳴動で記載)
音声鳴動
HTMLとJavaScriptを用いて音声を鳴動させる。
以下のサンプルは、server.luでclient.luaのイベントを発火させて、client.luaからHTML/Javascriptと連携して、音声を鳴動させている。
-- NUIファイル
ui_page 'html/index.html'
-- 必要なファイル
files {
'html/index.html',
'html/script.js',
'html/sounds/*.ogg',
}
TriggerClientEvent('gacha:playSound', src, 'get_s.ogg')
RegisterNetEvent('gacha:playSound')
AddEventHandler('gacha:playSound', function(soundFile)
SendNUIMessage({
type = "playSound",
sound = 'sounds/' .. soundFile,
volume = 0.1 -- 必要に応じて調整
})
end)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gacha Sound System</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<script src="script.js"></script>
</body>
</html>
window.addEventListener("message", function (event) {
const data = event.data;
if (data.type === "playSound") {
const notificationSound = new Audio(data.sound);
notificationSound.volume = data.volume || 1.0;
notificationSound.play();
}
});
jobの取得とjobレベルの取得
local playerJob = GetPlayerJob()
job_name = playerJob.name
job_lebel = playerJob.grade.level
job_onduty = playerJob.onduty --未検証(出勤状態かの取得)
jobのon dutyの人数取得(出勤状態の取得)
jobでのon dutyを把握するためには、サーバで確認する必要があるので、サーバ側でコードを作成する。
local src = source
local players = QBCore.Functions.GetPlayers(src) --サーバ上のすべてのプレイヤー取得
local uwuCount = 0
for i = 1, #players do --#playersでサーバの人数分(要素数分)ループする。
local player = QBCore.Functions.GetPlayer(players[i])
if (player.PlayerData.job.name == "uwu" and player.PlayerData.job.onduty) then
uwuCount = uwuCount + 1
end
end
その他
一旦はここまでで順次追記していこうと思います。
あと別記事で作成したスクリプト記事も書こうと思います。