はじめに
FiveMの街での飲食店のオープンしていないことが住民にとって不便でもあり、店員としても常に開けるのは負担が大きいため、自動販売機を設置してプレイヤーが購入できるようにした。自動販売機の在庫や売上の回収は、店員(特定ジョブ+ランク)のみが出来るように作成した。飲食店の売上のために心なき(NPC)への販売模擬機能も搭載した。
自動販売機スクリプトの内容について
自動販売機の構成としては、以下の通りである。
【仕様】
・プレイヤーが商品を購入できること
・各自動販売機ごと、各商品ごとに在庫管理ができること
・サーバ瞑想(FiveM再起動)時にランダム個数を販売する(NPCへの販売模擬)
・在庫の補充は、特定ジョブができること
・売上の回収は、特定ジョブ+特定ランク以上ができること
【動作環境】
・QBCore, ox_lib, ox_target
※心の目で見た際のメニューをox_libのメニューで実装。
動作画面
・商品購入の動作
容量の関係で早送りみたいになっているが、操作としては、自動販売機に対して、心の目でアクセスして、メニューを表示。
商品を選び、個数を入力すると、支払いを行い、商品を受け取る。
(右上にあるBankのお金が減っているのが確認できる)
スクリプトの概要
ソースコード(一部紹介)
ソースコードの一部を紹介する。
また、Scriptの配布やソースコードの公開については現在検討中のため、ポイントだけ記載する。
◼️ config.lua
取り扱う商品や、自動販売機の設定ファイル
商品名などはqb-core配下のitems.luaから取得しても良かったが、当時はそこまで理解しきれていなかったため、config.luaで記載している状況である。
Config = {}
-- 商品名と日本語ラベルの対応表をConfigで管理
Config.ProductLabels = {
--猫カフェ
kira_kira_currye = 'キラキラカレー',
strawberry_shortcake = 'いちごショートケーキ',
doki_doki_pancakes = 'どきどきパンケーキ',
--(省略)
}
--自動販売機の設定
Config.VendingMachines = {
{
id = 'Vending_Machine_001',-- 自動販売機のユニークID
model = 'prop_vend_snak_01', -- 自動販売機のモデル
coords = vector3(-572.53, -1071.41, 21.33), -- 猫カフェ前
heading = 0.0, --向き
prices = {
["kira_kira_currye"] = 7500,
["strawberry_shortcake"] = 7500,
["doki_doki_pancakes"] = 7500,
--(省略)
},
earnings = 0,
allowedJob = 'uwu', --補充、売上金回収ができるジョブ設定
allowedGrade = 2, -- 売上金を回収できる最低ジョブレベルを指定
autoSell = true, -- ランダム販売を有効にする(心なきへの販売模擬)
randomMin = 1, -- ランダム販売の最小値
randomMax = 10 -- ランダム販売の最大値
},
--(以下自動販売機設置分続く)
}
◼️vending_machine.json
自動販売機の在庫や売上金を管理しているDBファイル
[
{
"earnings": 1725000,
"inventory": {
"kira_kira_currye": 124,
"doki_doki_pancakes": 96,
"strawberry_shortcake": 101
},
"id": "Vending_Machine_001"
}
]
idがconfig.luaの自動販売機と紐づいている。
earningsは、自動販売機に入っている売上金額である。
スクリプト起動中にJSONファイルを直接書き換えても、反映されない。
メモリ上にロードされているため、書き換えたとしても、自動販売機が利用された際にメモリ上のデータに下記変わる。
反映させたい場合は、スクリプトの再起動が必要である。
◼️自動販売機のpropの出現
-- 自動販売機を生成し、配置
local vendingMachineEntity = CreateObject(vendingMachineModel, vendingMachineCoords.x, vendingMachineCoords.y, vendingMachineCoords.z, true, false, true)
SetEntityHeading(vendingMachineEntity, vendingMachineHeading)
FreezeEntityPosition(vendingMachineEntity, true)
SetModelAsNoLongerNeeded(vendingMachineModel)
◼️自動販売機に心の目で選択した際のメニュー表示(ox_targetを利用)
-- ox_target Interaction (座標ベースのターゲットゾーンを追加)
for _, machineConfig in ipairs(Config.VendingMachines) do
exports['ox_target']:addSphereZone({
coords = machineConfig.coords,
radius = 1.5,
options = {
{
name = 'vendingMachineBuy',
label = '商品を購入する',
icon = 'fa-solid fa-shopping-cart',
onSelect = function(data)
showProductMenu(machineConfig.coords)
end
},
{
name = 'vendingMachineRestock',
label = '商品を補充する',
icon = 'fa-solid fa-box',
onSelect = function(data)
showRestockMenu(machineConfig.coords)
end
},
{
name = 'vendingMachineCollect',
label = '売上金を回収する',
icon = 'fa-solid fa-money-bill-wave',
onSelect = function(data)
collectEarnings(machineConfig.coords)
end
}
}
})
end
上記コードの以下の部分で、ox_targetのメニューが表示できる座標と範囲を指定している。
最初は自動販売機のpropのIDに紐づけようとしたがうまくいかず、座標に変更した。
exports['ox_target']:addSphereZone({
coords = machineConfig.coords,
radius = 1.5,
◼️商品購入メニューと販売処理
商品メニュー(ox_target)の生成は、client.luaで実施する。
table.insert(options, {
title = label,
description = " 在庫: " .. quantity .. "個, 価格: $" .. price,
onSelect = function()
local input = exports['ox_lib']:inputDialog('購入数量', {'数量'})
if input and tonumber(input[1]) and tonumber(input[1]) > 0 then
-- server.luaの販売処理を呼び出す
TriggerServerEvent('vendingMachine:buyProduct', uniqueID, product, tonumber(input[1]), price)
else
exports['ox_lib']:notify({type = 'error', description = '無効な数量です。'})
end
end
})
-- 上記で作成したメニューを表示させる部分
if #options > 0 then
exports['ox_lib']:registerContext({
id = 'vending_machine_menu',
title = '購入する商品を選択',
options = options
})
exports['ox_lib']:showContext('vending_machine_menu')
else
exports['ox_lib']:notify({type = 'error', description = '商品がありません。'})
end
アイテムの購入処理は、Server.luaで実施する。
-- 商品購入処理(抜粋)
if machine and machine.inventory[productName] and machine.inventory[productName] >= quantity then
local price = getProductPrice(uniqueID, productName) * quantity -- Configから価格を取得
if Player.Functions.RemoveMoney('bank', price, 'vendingMachine-purchase') then --cash or bank
-- 自動販売機の商品ごとの在庫減少
machine.inventory[productName] = machine.inventory[productName] - quantity
-- 自動販売機の商品ごとの売上金額に加算
machine.earnings = machine.earnings + price
-- プレイヤーにアイテム付与
Player.Functions.AddItem(productName, quantity)
TriggerClientEvent('inventory:client:ItemBox', src, QBCore.Shared.Items[productName], "add")
-- プレイヤーに購入メッセージ表示
TriggerClientEvent('QBCore:Notify', src, productName .. 'を' .. quantity .. '個購入しました!', 'success')
-- JSONファイルへの書き込み処理を呼び出し
saveVendingData()
else
TriggerClientEvent('QBCore:Notify', src, '所持金が不足しています。', 'error')
end
else
TriggerClientEvent('QBCore:Notify', src, '在庫がありません。', 'error')
end
※QBCore:Notifyでメッセージを出力したり、exports['ox_lib']:notifyでメッセージを出力したり、チグハグな状態ではある...
◼️売上回収処理
特定のジョブかつジョブレベルだけ操作できるようにし、他のプレイヤーが操作しようとすると、ox_libでエラーメッセージを出力する。
--プレイヤーのジョブとランクを取得
local playerJob = GetPlayerJob()
for _, machineConfig in ipairs(Config.VendingMachines) do
if #(coords - machineConfig.coords) < 2.0 then
if playerJob.name == machineConfig.allowedJob and playerJob.grade.level >= machineConfig.allowedGrade then
--server.luaの売上を回収する処理を呼び出す
TriggerServerEvent('vendingMachine:collectEarnings', machineConfig.id)
return
end
end
end
exports['ox_lib']:notify({type = 'error', description = 'この操作を行う権限がありません。'})
少し宣伝
FiveMの35VILLAGE RPというサーバで遊んでいます!
犯罪の種類が多かったり、白ジョブ、フリージョブもたくさんあるサーバです。
私が作った自作Scriptもいつくか入っているので、ぜひ遊びに来てください!
最後に
今回の自動販売機のスクリプトは、2作目のスクリプトでpropの生成などのやり方を把握することができた。
また、苦労した点としては、ox_targetを自動販売機のIDと紐づけて実施しようとしたがうまくいかず、座標に対してox_targetを指定することで、メニューを開けるようになった。
課題として、config.luaに自動販売機を追加した際に、JSONファイルも手動で記載する必要がある。一応、JSONファイルに自動販売機が存在しない場合にJSONファイルに追加する処理は入れているものの、期待した形式になっていない状況である。この辺りについては、随時改修していこうと思う。