Robloxでゲージゲームを作成したいけど作り方がよくわからない人向け。
目次
実現したいこと
- GUIのゲージを自動で伸縮させる
- プレイヤーのタイミングで止めることができる
- 基本的な処理はローカルで行うが、重要な判定はサーバで行う
UI作成
エクスプローラー内のStarterGui直下にScreenGuiを追加し、GUIオブジェクトを画面に配置していきます。ゲージの停止ボタンは画面タップで止めるようにするため画面を覆うようにします。
作成したUIのエクスプローラーオブジェクト階層は以下のようになります。
プロパティの値は基本的に自由ですが、ゲージゲームを作成する上で注意するオブジェクトが4つあります。
- Frame/CenterLower/LeftGauge/2_Gauge
- Frame/CenterLower/LeftGauge/2_Gauge/ImageLabel
- Frame/CenterLower/RightGauge/2_Gauge
- Frame/CenterLower/RightGauge/2_Gauge/ImageLabel
動きとしてはLeftGauge側は左から右へ、RightGauge側は右から左へゲージが伸びるようにします。
- LeftGauge側:AnchorPointXは0、PositionXのScaleは0、SizeXはScaleを使用する
- RightGauge側:AnchorPointXは1、PositionXのScaleは1、SizeXはScaleを使用する
ゲージの増減を制御する
作成したUIを制御するためのローカルスクリプトをStarterPlayer/StarterPlayerScripts直下に作成します。名前はGaugeにします。作成したファイルにUIの参照を取るための処理を記述します。
-- UI参照 -- Service local Players = game:GetService("Players"); -- GUI local ScreenGui = Players.LocalPlayer:WaitForChild("PlayerGui"):WaitForChild("ScreenGui"); local CenterMiddle = ScreenGui:WaitForChild("Frame"):WaitForChild("CenterMiddle"); local CenterLower = ScreenGui:WaitForChild("Frame"):WaitForChild("CenterLower"); -- CenterMiddle local StopButton = CenterMiddle:WaitForChild("StopButton"):WaitForChild("TextButton"); -- CenterLower Left local Timer = CenterLower:WaitForChild("Timer"):WaitForChild("TextLabel"); -- CenterLower Left local LeftGauge = CenterLower:WaitForChild("LeftGauge"):WaitForChild("2_Gauge"); local LeftWin = CenterLower:WaitForChild("LeftGauge"):WaitForChild("3_Number"):WaitForChild("2_Win"); local LeftNumber = CenterLower:WaitForChild("LeftGauge"):WaitForChild("3_Number"):WaitForChild("3_TextLabel"); -- CenterLower Right local RightGauge = CenterLower:WaitForChild("RightGauge"):WaitForChild("2_Gauge"); local RightWin = CenterLower:WaitForChild("RightGauge"):WaitForChild("3_Number"):WaitForChild("2_Win"); local RightNumber = CenterLower:WaitForChild("RightGauge"):WaitForChild("3_Number"):WaitForChild("3_TextLabel");
ゲージの溜まる時間やゲージゲームの最大時間などの定数や諸々の変数を記述します。
-- 定数 local GAUGE_MAX_TIME = 1; -- ゲージが最大値になる時間(秒) local GAUGE_LIMIT_TIME = 3; -- ゲージゲームの時間(秒) local GAUGE_VALUE_FORMAT = "%3d"; -- 値の表示形式 local SIDE_TYPE = { -- 左右の種類 Left = 1, Right = 2, }; -- 変数 local elapsedTime = 0; -- 経過時間 local gaugeValue = 0; -- ゲージの値 -- 左側 local isLeftUpdate = false; -- 更新フラグ(左) local leftValue = 0; -- 値(左) -- 右側 local isRightUpdate = false; -- 更新フラグ(右) local rightValue = 0; -- 値(左) -- 更新 local RunService = game:GetService("RunService"); local updateConnection = nil; -- 更新接続
更新処理を行う前に値の初期化を行います。ゲージのサイズやタイマーUIなどもここで初期化を行います。
-- 変数 初期化 elapsedTime = 0; gaugeValue = 0; isLeftUpdate = false; leftValue = 0; isRightUpdate = false; rightValue = 0; updateConnection = nil; -- UI関連初期化 local size = LeftGauge.Size; LeftGauge.Size = UDim2.new(0, size.X.Offset, size.Y.Scale, size.Y.Offset); LeftNumber.Text = string.format(GAUGE_VALUE_FORMAT, gaugeValue); LeftWin.Visible = false; size = RightGauge.Size; RightGauge.Size = UDim2.new(0, size.X.Offset, size.Y.Scale, size.Y.Offset); RightNumber.Text = string.format(GAUGE_VALUE_FORMAT, gaugeValue); RightWin.Visible = false; Timer.Text = tostring(GAUGE_LIMIT_TIME); StopButton.Parent.Visible = false;
ボタン(画面タップ)で左のゲージが止まるようにします。
-- 停止処理 local function Stop(_type) if _type == SIDE_TYPE.Left then isLeftUpdate = false; -- 更新停止(左) elseif _type == SIDE_TYPE.Right then isRightUpdate = false; -- 更新停止(左) end end -- 停止ボタン StopButton.MouseButton1Down:Connect(function() Stop(SIDE_TYPE.Left); end);
更新処理を記述します。ゲージゲームの時間を超えた場合は強制で0になるようにします。
-- 更新 if updateConnection == nil then isLeftUpdate = true; isRightUpdate = true; StopButton.Parent.Visible = true; updateConnection = RunService.Stepped:Connect(function(currentTime, deltaTime) if isLeftUpdate == true or isRightUpdate == true then elapsedTime = elapsedTime + deltaTime; -- 経過時間の加算 local value = gaugeValue + deltaTime; -- ゲージの値の加算 local per = 0; -- ゲージサイズ割合 -- 値が最大値を超えたか local floor = math.floor(value / GAUGE_MAX_TIME); if floor > math.floor(gaugeValue / GAUGE_MAX_TIME) then gaugeValue = floor * GAUGE_MAX_TIME; per = 1; else gaugeValue = value; per = gaugeValue / GAUGE_MAX_TIME; local floor = math.floor(per) per = per - floor; end local gauge = math.floor(per * 100); -- ゲージの値 -- 制限時間を過ぎた if elapsedTime >= GAUGE_LIMIT_TIME then elapsedTime = GAUGE_LIMIT_TIME; per = 0; gauge = 0; end if isLeftUpdate then -- 左の更新 leftValue = gauge; -- テキスト表示 LeftNumber.Text = string.format(GAUGE_VALUE_FORMAT, leftValue); --ゲージ更新 local size = LeftGauge.Size; LeftGauge.Size = UDim2.new(per, size.X.Offset, size.Y.Scale, size.Y.Offset); -- 制限時間を過ぎた if elapsedTime >= GAUGE_LIMIT_TIME then Stop(SIDE_TYPE.Left); end end if isRightUpdate then -- 右の更新 rightValue = gauge; -- テキスト表示 RightNumber.Text = string.format(GAUGE_VALUE_FORMAT, rightValue); --ゲージ更新 local size = RightGauge.Size; RightGauge.Size = UDim2.new(per, size.X.Offset, size.Y.Scale, size.Y.Offset); -- 制限時間を過ぎた if elapsedTime >= GAUGE_LIMIT_TIME then Stop(SIDE_TYPE.Right); end end Timer.Text = math.ceil(GAUGE_LIMIT_TIME - elapsedTime); -- 残り入力時間 end end); end
右側(CPU)も自動で止まるように宣言、初期化、更新に処理を追加する。
-- 宣言 ... -- CPU関連 local CPUGaugeStopRange = {20, 80}; -- CPUのゲージ停止範囲 local cpuGaugeValue = 0; -- CPU側の止める値 ... -- 変数 初期化 ... cpuGaugeValue = math.random(CPUGaugeStopRange[1], CPUGaugeStopRange[2]); ... -- 更新 ... if isRightUpdate then -- 右の更新 ... -- 制限時間を過ぎた if elapsedTime >= GAUGE_LIMIT_TIME then Stop(SIDE_TYPE.Right); elseif gauge >= cpuGaugeValue then -- CPUの止める値を超えたら Stop(SIDE_TYPE.Right); end end ...
左右のゲージが止まった時にどちらの値が大きいかのチェックを行い、UIを変更するようにします。
-- 停止処理 local function Stop(_type) ... if isLeftUpdate == false and isRightUpdate == false then if leftValue > rightValue then LeftWin.Visible = true; elseif leftValue < rightValue then RightWin.Visible = true; end end end
ここまでの実装でゲージゲームの基本部分ができました。
シーンを組み込む
初期化 → 更新 → 終了 → 初期化…とシーンを変更できるようにします。
必要なシーンは以下になります。
- 初期化
- 更新
- 判定
- 終了
まずシーンステートの定義をします。
-- 定数 local SCENE_STATE = { -- シーンステート Init = 1, Update = 2, Judge = 3, Finish = 4, }; -- 変数 -- シーンステート local changeStateFunc = nil; -- ステート変更関数の参照
次の各シーンの関数を作成します。
-- 初期化 local function Init() end -- 更新 local function Update() end -- 判定 local function Judge() end -- 終了 local function Finish() end
ステートの変更関数を作成します。
-- ステート変更 local function changeState(_state) if _state == SCENE_STATE.Init then -- 初期化 Init(); elseif _state == SCENE_STATE.Update then -- 更新 Update(); elseif _state == SCENE_STATE.Judge then -- 判定 Judge(); elseif _state == SCENE_STATE.Finish then -- 終了 Finish(); else -- その他 print("error change state"..tostring(_state)); end end
luaでは定義より上で呼び出せないです。
-- 例 hoge();-- エラーになる local function hoge() -- hoge関数 end hoge(); -- hoge()を呼び出せる
そのため定義よりも上で関数を使用するには、変数に関数を入れて呼び出すようにします。
-- 例 local hogeFunc = nil; local function func() hogeFunc(); -- hoge()を呼び出せる end local function hoge() -- hoge関数 end hogeFunc = hoge;
実際に変数から呼び出せるようにします。
-- 変数 -- シーンステート local changeStateFunc = nil; -- ステート変更関数の参照 ... -- ステート変更 local function changeState(_state) ... end changeStateFunc = changeState;
変数やUI関連の初期化をInit()にまとめます。
-- 初期化 local function Init() -- 変数 elapsedTime = 0; gaugeValue = 0; isLeftUpdate = false; leftValue = 0; isRightUpdate = false; rightValue = 0; updateConnection = nil; cpuGaugeValue = math.random(CPUGaugeStopRange[1], CPUGaugeStopRange[2]); -- UI関連 local size = LeftGauge.Size; LeftGauge.Size = UDim2.new(0, size.X.Offset, size.Y.Scale, size.Y.Offset); LeftNumber.Text = string.format(GAUGE_VALUE_FORMAT, gaugeValue); LeftWin.Visible = false; size = RightGauge.Size; RightGauge.Size = UDim2.new(0, size.X.Offset, size.Y.Scale, size.Y.Offset); RightNumber.Text = string.format(GAUGE_VALUE_FORMAT, gaugeValue); RightWin.Visible = false; Timer.Text = tostring(GAUGE_LIMIT_TIME); StopButton.Parent.Visible = false; wait(); changeStateFunc(SCENE_STATE.Update); end
更新処理をUpdate()にまとめます。
-- 更新 local function Update() isLeftUpdate = true; isRightUpdate = true; StopButton.Parent.Visible = true; updateConnection = RunService.Stepped:Connect(function(currentTime, deltaTime) if isLeftUpdate == true or isRightUpdate == true then elapsedTime = elapsedTime + deltaTime; -- 経過時間の加算 ... Timer.Text = math.ceil(GAUGE_LIMIT_TIME - elapsedTime); -- 残り入力時間 end end); end
判定処理をStop()からJudge()に移動させます。
-- 停止処理 local function Stop(_type) ... -- 左右のゲージが止まった if isLeftUpdate == false and isRightUpdate == false then changeStateFunc(SCENE_STATE.Judge); end end ... -- 判定 local function Judge() -- 値の大きい方のUIを変更する(引き分けは何もしない) if leftValue > rightValue then LeftWin.Visible = true; elseif leftValue < rightValue then RightWin.Visible = true; end wait(3); -- 待機3秒 changeStateFunc(SCENE_STATE.Finish); end
終了処理をFinish()へまとめます。
-- 終了 local function Finish() if updateConnection ~= nil then updateConnection:Disconnect(); updateConnection = nil; end wait(1); -- 待機1秒 changeStateFunc(SCENE_STATE.Init); end
最後に一番下に初期化シーンを呼び出す処理を追加します。
-- 最初のシーンは初期化から始める changeStateFunc(SCENE_STATE.Init);
これで何度もゲージゲームを遊ぶことができるようになりました。
様々な判定や処理をサーバに任せる
今までローカルのみで処理や判定を行ってきましたが、ローカルはデータ改竄、漏洩など安心して使うには少し頼りない気がします。そこでプレイヤーが触ることのできないサーバへ色々な処理を移動させます。
移動させる処理は以下になります。
- シーンステート
- ゲージの溜まる時間やゲージゲーム時間
- CPUの値
- ゲージを止めた時の値チェック
サーバ処理を行うためのスクリプトをServerScriptService直下に作成します。名前はGaugeにします。
ローカルスクリプトからサーバで使用できるものをコピーします。
-- 定数 local GAUGE_MAX_TIME = 1; -- ゲージが最大値になる時間(秒) local GAUGE_LIMIT_TIME = 3; -- ゲージゲームの時間(秒) local SIDE_TYPE = { -- 左右の種類 None = 0, Left = 1, Right = 2, }; local SCENE_STATE = { -- シーンステート Init = 1, Update = 2, Judge = 3, Finish = 4, }; -- 変数 local leftValue = 0; -- 左の値 local rightValue = 0; -- 右の値 local winSide = 0; -- 勝った方 -- CPU関連 local CPUGaugeStopRange = {20, 80}; -- CPUのゲージ停止範囲 local cpuGaugeValue = 0; -- CPU側の止める値 -- シーンステート local changeStateFunc = nil; -- ステート変更関数の参照 -- 初期化 local function Init() -- 変数 leftValue = -1; rightValue = -1; winSide = SIDE_TYPE.None; cpuGaugeValue = math.random(CPUGaugeStopRange[1], CPUGaugeStopRange[2]); end -- 更新 local function Update() end -- 判定 local function Judge() end -- 終了 local function Finish() end -- ステート変更 local function changeState(_state) if _state == SCENE_STATE.Init then -- 初期化 Init(); elseif _state == SCENE_STATE.Update then -- 更新 Update(); elseif _state == SCENE_STATE.Judge then -- 判定 Judge(); elseif _state == SCENE_STATE.Finish then -- 終了 Finish(); else -- その他 print("error change state"..tostring(_state)); end end changeStateFunc = changeState; -- 最初のシーンは初期化から始める changeStateFunc(SCENE_STATE.Init);
再生を行うとローカルスクリプトでゲージ増減が動き出しているのでシーン遷移を行わないようにします。
ローカルスクリプトのsceneState変数、changeStateFunc変数、changeState関数、wait()を全て削除します。
local sceneState = SCENE_STATE.Init; -- 現在のシーンステート local changeStateFunc = nil; -- ステート変更関数の参照 -- ステート変更 local function changeState(_state) ... end wait(数値);
ローカルスクリプトの一番下に記述した初期化呼び出しは以下のように変更します
-- 最初のシーンは初期化から始める changeStateFunc(SCENE_STATE.Init); ↓ -- 最初のシーンは初期化から始める Init();
サーバ <=> クライアントでデータやり取りを行うことができるRemoteEventの機能を使用し、シーンの遷移を実装します。
ReplicatedStorage直下にRemoteEventを作成します。
作成するRemoteEventは以下になります。
- InitEvent : 初期化イベント(サーバ → クライアント、クライアント → サーバ)
- UpdateEvent : 更新イベント(サーバ → クライアント)
- JudgeEvent : 判定イベント(サーバ → クライアント)
- FinishEvent : 終了イベント(サーバ → クライアント)
- StopEvent : 停止イベント(クライアント → サーバ)
サーバとクライアントのGaugeスクリプトに作成したRemoteEventの参照を取得するソースを追加します。
-- Service local ReplicatedStorage = game:GetService("ReplicatedStorage"); -- Event local InitEvent = ReplicatedStorage:WaitForChild("InitEvent"); local UpdateEvent = ReplicatedStorage:WaitForChild("UpdateEvent"); local JudgeEvent = ReplicatedStorage:WaitForChild("JudgeEvent"); local FinishEvent = ReplicatedStorage:WaitForChild("FinishEvent"); local StopEvent = ReplicatedStorage:WaitForChild("StopEvent");
ローカルスクリプトにサーバから呼び出すイベントを登録します。
-- 初期化 local function Init() ... end InitEvent.OnClientEvent:Connect(Init); -- 更新 local function Update() ... end UpdateEvent.OnClientEvent:Connect(Update); -- 判定 local function Judge() ... end JudgeEvent.OnClientEvent:Connect(Judge); -- 終了 local function Finish() ... end FinishEvent.OnClientEvent:Connect(Finish);
サーバスクリプトにゲージを止めた時にデータを受け取る関数と呼び出しイベントを追加します。
-- 停止 local function Stop() end StopEvent.OnServerEvent:Connect(Stop);
サーバスクリプトでシーンの遷移を調整します。それぞれのイベントで必要な引数を追加します。
-- 初期化 local function Init() -- 変数 leftValue = -1; rightValue = -1; winSide = SIDE_TYPE.None; cpuGaugeValue = math.random(CPUGaugeStopRange[1], CPUGaugeStopRange[2]); -- 初期化イベント(クライアント) InitEvent:FireAllClients(GAUGE_MAX_TIME, GAUGE_LIMIT_TIME, cpuGaugeValue); -- ステート変更 changeStateFunc(SCENE_STATE.Update); end -- 更新 local function Update() -- 待ち wait(1); -- 更新イベント(クライアント) UpdateEvent:FireAllClients(); end -- 判定 local function Judge() if leftValue > rightValue then -- 左の勝ち winSide = SIDE_TYPE.Left; elseif leftValue = 0 and rightValue >= 0 then -- ステート変更 changeStateFunc(SCENE_STATE.Judge); end end
サーバスクリプトで引数を追加したため、ローカルスクリプトも引数を追加します。
-- 初期化 local function Init(_gaugeMacTime, _gaugeLimitTime, _cpuGaugeValue) -- 定数 GAUGE_MAX_TIME = _gaugeMacTime; GAUGE_LIMIT_TIME = _gaugeLimitTime; -- 変数 cpuGaugeValue = _cpuGaugeValue; ... end -- 判定 local function Judge(_winSide) -- 勝った方のUIを変更する(引き分けは何もしない) if _winSide == SIDE_TYPE.Left then LeftWin.Visible = true; elseif _winSide == SIDE_TYPE.Right then RightWin.Visible = true; end -- 判定イベント(クライアント) JudgeEvent:FireAllClients(winSide); end ... Init(-1, -1, -1);
ゲージ停止のイベントをローカルスクリプトに追加します。
-- 停止処理 local function Stop(_type) if _type == SIDE_TYPE.Left then isLeftUpdate = false; -- 更新停止(左) -- 停止イベント(サーバ) StopEvent:FireServer(_type, leftValue); elseif _type == SIDE_TYPE.Right then isRightUpdate = false; -- 更新停止(左) -- 停止イベント(サーバ) StopEvent:FireServer(_type, rightValue); end end
そのまま再生させるとローカルの初期化が終わる前にサーバの更新が始まることがあります。同期を取るためにローカルの初期化後にサーバへイベントを使い同期させます。
ローカルスクリプトは以下のようにします。
-- 初期化 local function Init(_gaugeMaxTime, _gaugeLimitTime, _cpuGaugeValue) ... StopButton.Parent.Visible = false; if _gaugeMaxTime >= 0 and _gaugeLimitTime >= 0 and _cpuGaugeValue >= 0 then -- サーバイベント(初期化) InitEvent:FireServer(); end end
サーバスクリプトは以下のようにします。
-- 初期化 local function Init() ... -- 初期化イベント(クライアント) InitEvent:FireAllClients(GAUGE_MAX_TIME, GAUGE_LIMIT_TIME, cpuGaugeValue); end local function InitEnd(_player) -- ステート変更 changeStateFunc(SCENE_STATE.Update); end InitEvent.OnServerEvent:Connect(InitEnd); -- 更新 local function Update() -- 更新イベント(クライアント) UpdateEvent:FireAllClients(); end
これで今までと同じようにゲージゲームを遊ぶことができます。
次はローカルから来たデータが改竄されていないかのチェックを行います。やり方は色々ありますが今回はUpdateの呼び出し時間を使います。
ローカルスクリプトに前フレームとの時間差を格納する配列を用意し、Stop()、Init()、Update()にそれぞれ処理を追記します。
-- 変数 local deltaTimes = {}; -- 経過時間配列 ... -- 停止処理 local function Stop(_type) if _type == SIDE_TYPE.Left then ... StopEvent:FireServer(_type, leftValue, deltaTimes); elseif _type == SIDE_TYPE.Right then ... StopEvent:FireServer(_type, rightValue, deltaTimes); end end ... -- 初期化 local function Init(_gaugeMaxTime, _gaugeLimitTime, _cpuGaugeValue) ... deltaTimes = {}; ... end -- 更新 local function Update() if updateConnection == nil then isLeftUpdate = true; isRightUpdate = true; StopButton.Parent.Visible = true; updateConnection = RunService.Stepped:Connect(function(currentTime, deltaTime) if isLeftUpdate == true or isRightUpdate == true then elapsedTime += deltaTime; -- 経過時間の加算 table.insert(deltaTimes, deltaTime); ... end end); end end
時間切れのチェックも行うのでelapsedTimeもサーバに渡すようにします。
-- 停止処理 local function Stop(_type) ... if _type == SIDE_TYPE.Left then isLeftUpdate = false; -- 更新停止(左) -- 停止イベント(サーバ) StopEvent:FireServer(_type, leftValue, deltaTimes, elapsedTime); elseif _type == SIDE_TYPE.Right then isRightUpdate = false; -- 更新停止(左) -- 停止イベント(サーバ) StopEvent:FireServer(_type, rightValue, deltaTimes, elapsedTime); end end
ゲージの割合計算の一部をサーバとローカルで使用したいため、まずは関数を作成します。
制限時間を過ぎた処理は引数や返り値が増えるため入れないようにします。
-- ゲージの割合計算 local function calcGaugeRatio(_deltaTime, _gaugeValue, _gaugeMaxTime) local per = 0; -- ゲージサイズ割合 local value = _gaugeValue + _deltaTime; -- ゲージの値の加算 -- 値が最大値を超えたか local floor = math.floor(value / _gaugeMaxTime); if floor > math.floor(_gaugeValue / _gaugeMaxTime) then _gaugeValue = floor * _gaugeMaxTime; per = 1; else _gaugeValue = value; per = _gaugeValue / _gaugeMaxTime; local floor = math.floor(per); per = per - floor; end return _gaugeValue, per; end
Update()の該当箇所をcalcGaugeRatio()に入れ替えます。
-- ゲージの割合計算 local function calcGaugeRatio(_deltaTime, _gaugeValue, _gaugeMaxTime) ... end -- 更新 local function Update() if updateConnection == nil then ... updateConnection = RunService.Stepped:Connect(function(currentTime, deltaTime) if isLeftUpdate == true or isRightUpdate == true then elapsedTime += deltaTime; -- 経過時間の加算 table.insert(deltaTimes, deltaTime); local per = 0; -- ゲージサイズ割合 -- ゲージの割合計算 gaugeValue, per = calcGaugeRatio(deltaTime, gaugeValue, GAUGE_MAX_TIME); local gauge = math.floor(per * 100); -- ゲージの値 -- 制限時間を過ぎた if elapsedTime >= GAUGE_LIMIT_TIME then elapsedTime = GAUGE_LIMIT_TIME; per = 0; gauge = 0; end ... end end); end end
ReplicatedStorage直下にModuleScriptを作成します。名前はGaugeModuleにします。
処理はcalcGaugeRatio()を移動させ、モジュールから呼び出せるようにします。
local GaugeModule = {} -- ゲージの割合計算 function GaugeModule:calcGaugeRatio(_deltaTime, _gaugeValue, _gaugeMaxTime) local per = 0; -- ゲージサイズ割合 local value = _gaugeValue + _deltaTime; -- ゲージの値の加算 -- 値が最大値を超えたか local floor = math.floor(value / _gaugeMaxTime); if floor > math.floor(_gaugeValue / _gaugeMaxTime) then _gaugeValue = floor * _gaugeMaxTime; per = 1; else _gaugeValue = value; per = _gaugeValue / _gaugeMaxTime; local floor = math.floor(per); per = per - floor; end return _gaugeValue, per; end return GaugeModule
作成したGaugeModuleモジュールをローカルスクリプトで使用できるようにします。
-- Service local ReplicatedStorage = game:GetService("ReplicatedStorage"); -- Module local GaugeModule = require(ReplicatedStorage:WaitForChild("GaugeModule")); -- 更新 local function Update() if updateConnection == nil then ... updateConnection = RunService.Stepped:Connect(function(currentTime, deltaTime) if isLeftUpdate == true or isRightUpdate == true then ... -- ゲージの割合計算 gaugeValue, per = GaugeModule:calcGaugeRatio(deltaTime, gaugeValue, GAUGE_MAX_TIME); ... end end); end end
次にサーバ側のチェック処理を作成します。
GaugeModuleモジュールをサーバスクリプトでも使用できるようにします。
-- Service local ReplicatedStorage = game:GetService("ReplicatedStorage"); -- Module local GaugeModule = require(ReplicatedStorage:WaitForChild("GaugeModule"));
Stop()の引数を増やします。
-- 停止 local function Stop(_player, _side, _value, _deltaTimes, _elapsedTime) ... end
Stop()にチェック処理を追加します。チェック結果が偽だった場合は制限時間を過ぎたと同様に値を0にします。
-- 停止 local function Stop(_player, _side, _value, _deltaTimes, _elapsedTime) local value = 0; local per = 0; for i = 1, #_deltaTimes do value, per = GaugeModule:calcGaugeRatio(_deltaTimes[i], value, GAUGE_MAX_TIME); end local gauge = math.floor(per * 100); -- ゲージの値 -- 制限時間を過ぎた if _elapsedTime >= GAUGE_LIMIT_TIME then _elapsedTime = GAUGE_LIMIT_TIME; gauge = 0; end -- チェック結果エラー if gauge ~= _value then _value = 0; end ... end
これでチェック処理はできましたが、チェック後のゲージの値がローカルに反映されていません。
サーバスクリプトのJudge()のJudgeEventでチェック後の値を送るようにします。
-- 判定 local function Judge() ... -- 判定イベント(クライアント) JudgeEvent:FireAllClients(winSide, leftValue, rightValue); ... end
ローカルスクリプトのJudge()に引数を追加します。
-- 判定 local function Judge(_winSide, _leftValue, _rightValue) ... end
受け取った値をゲージに反映させます。
-- 判定 local function Judge(_winSide, _leftValue, _rightValue) leftValue = _leftValue; rightValue = _rightValue; -- テキスト表示 LeftNumber.Text = string.format(GAUGE_VALUE_FORMAT, leftValue); RightNumber.Text = string.format(GAUGE_VALUE_FORMAT, rightValue); --ゲージ更新 local size = LeftGauge.Size; LeftGauge.Size = UDim2.new(leftValue / 100, size.X.Offset, size.Y.Scale, size.Y.Offset); size = RightGauge.Size; RightGauge.Size = UDim2.new(rightValue / 100, size.X.Offset, size.Y.Scale, size.Y.Offset); ... end
これで一通りの実装ができました。
作成したスクリプト
ローカルスクリプト Path:StarterPlayer/StarterPlayerScripts/Gauge
-- Service local Players = game:GetService("Players"); local ReplicatedStorage = game:GetService("ReplicatedStorage"); -- Module local GaugeModule = require(ReplicatedStorage:WaitForChild("GaugeModule")); -- Event local InitEvent = ReplicatedStorage:WaitForChild("InitEvent"); local UpdateEvent = ReplicatedStorage:WaitForChild("UpdateEvent"); local JudgeEvent = ReplicatedStorage:WaitForChild("JudgeEvent"); local FinishEvent = ReplicatedStorage:WaitForChild("FinishEvent"); local StopEvent = ReplicatedStorage:WaitForChild("StopEvent"); -- GUI local ScreenGui = Players.LocalPlayer:WaitForChild("PlayerGui"):WaitForChild("ScreenGui"); local CenterMiddle = ScreenGui:WaitForChild("Frame"):WaitForChild("CenterMiddle"); local CenterLower = ScreenGui:WaitForChild("Frame"):WaitForChild("CenterLower"); -- CenterMiddle local StopButton = CenterMiddle:WaitForChild("StopButton"):WaitForChild("TextButton"); -- CenterLower Time local Timer = CenterLower:WaitForChild("Timer"):WaitForChild("TextLabel"); -- CenterLower Left local LeftGauge = CenterLower:WaitForChild("LeftGauge"):WaitForChild("2_Gauge"); local LeftWin = CenterLower:WaitForChild("LeftGauge"):WaitForChild("3_Number"):WaitForChild("2_Win"); local LeftNumber = CenterLower:WaitForChild("LeftGauge"):WaitForChild("3_Number"):WaitForChild("3_TextLabel"); -- CenterLower Right local RightGauge = CenterLower:WaitForChild("RightGauge"):WaitForChild("2_Gauge"); local RightWin = CenterLower:WaitForChild("RightGauge"):WaitForChild("3_Number"):WaitForChild("2_Win"); local RightNumber = CenterLower:WaitForChild("RightGauge"):WaitForChild("3_Number"):WaitForChild("3_TextLabel"); -- 定数 local GAUGE_MAX_TIME = 1; -- ゲージが最大値になる時間(秒) local GAUGE_LIMIT_TIME = 3; -- ゲージゲームの時間(秒) local GAUGE_VALUE_FORMAT = "%3d"; -- 値の表示形式 local SIDE_TYPE = { -- 左右の種類 None = 0, Left = 1, Right = 2, }; -- 変数 local elapsedTime = 0; -- 経過時間 local gaugeValue = 0; -- ゲージの値 local deltaTimes = {}; -- 経過時間配列 -- 左側 local isLeftUpdate = false; -- 更新フラグ(左) local leftValue = 0; -- 値(左) -- 右側 local isRightUpdate = false; -- 更新フラグ(右) local rightValue = 0; -- 値(左) -- 更新 local RunService = game:GetService("RunService"); local updateConnection = nil; -- 更新接続 -- CPU関連 local cpuGaugeValue = 0; -- CPU側の止める値 -- 停止処理 local function Stop(_side) if _side == SIDE_TYPE.Left then isLeftUpdate = false; -- 更新停止(左) -- 停止イベント(サーバ) StopEvent:FireServer(_side, leftValue, deltaTimes, elapsedTime); elseif _side == SIDE_TYPE.Right then isRightUpdate = false; -- 更新停止(左) -- 停止イベント(サーバ) StopEvent:FireServer(_side, rightValue, deltaTimes, elapsedTime); end end -- 停止ボタン StopButton.MouseButton1Down:Connect(function() Stop(SIDE_TYPE.Left); end); -- 初期化 local function Init(_gaugeMaxTime, _gaugeLimitTime, _cpuGaugeValue) -- 定数 GAUGE_MAX_TIME = _gaugeMaxTime; GAUGE_LIMIT_TIME = _gaugeLimitTime; -- 変数 cpuGaugeValue = _cpuGaugeValue; elapsedTime = 0; gaugeValue = 0; isLeftUpdate = false; leftValue = 0; isRightUpdate = false; rightValue = 0; updateConnection = nil; deltaTimes = {}; -- UI関連 local size = LeftGauge.Size; LeftGauge.Size = UDim2.new(0, size.X.Offset, size.Y.Scale, size.Y.Offset); LeftNumber.Text = string.format(GAUGE_VALUE_FORMAT, gaugeValue); LeftWin.Visible = false; size = RightGauge.Size; RightGauge.Size = UDim2.new(0, size.X.Offset, size.Y.Scale, size.Y.Offset); RightNumber.Text = string.format(GAUGE_VALUE_FORMAT, gaugeValue); RightWin.Visible = false; Timer.Text = tostring(GAUGE_LIMIT_TIME); StopButton.Parent.Visible = false; if _gaugeMaxTime >= 0 and _gaugeLimitTime >= 0 and _cpuGaugeValue >= 0 then -- サーバイベント(初期化) InitEvent:FireServer(); end end InitEvent.OnClientEvent:Connect(Init); -- 更新 local function Update() if updateConnection == nil then isLeftUpdate = true; isRightUpdate = true; StopButton.Parent.Visible = true; updateConnection = RunService.Stepped:Connect(function(currentTime, deltaTime) if isLeftUpdate == true or isRightUpdate == true then elapsedTime += deltaTime; -- 経過時間の加算 table.insert(deltaTimes, deltaTime); local per = 0; -- ゲージサイズ割合 -- ゲージの割合計算 gaugeValue, per = GaugeModule:calcGaugeRatio(deltaTime, gaugeValue, GAUGE_MAX_TIME); local gauge = math.floor(per * 100); -- ゲージの値 -- 制限時間を過ぎた if elapsedTime >= GAUGE_LIMIT_TIME then elapsedTime = GAUGE_LIMIT_TIME; per = 0; gauge = 0; end if isLeftUpdate then -- 左の更新 leftValue = gauge; -- テキスト表示 LeftNumber.Text = string.format(GAUGE_VALUE_FORMAT, leftValue); --ゲージ更新 local size = LeftGauge.Size; LeftGauge.Size = UDim2.new(per, size.X.Offset, size.Y.Scale, size.Y.Offset); -- 制限時間を過ぎた if elapsedTime >= GAUGE_LIMIT_TIME then Stop(SIDE_TYPE.Left); end end if isRightUpdate then -- 右の更新 rightValue = gauge; -- テキスト表示 RightNumber.Text = string.format(GAUGE_VALUE_FORMAT, rightValue); --ゲージ更新 local size = RightGauge.Size; RightGauge.Size = UDim2.new(per, size.X.Offset, size.Y.Scale, size.Y.Offset); -- 制限時間を過ぎた if elapsedTime >= GAUGE_LIMIT_TIME then Stop(SIDE_TYPE.Right); elseif gauge >= cpuGaugeValue then -- CPUの止める値を超えたら Stop(SIDE_TYPE.Right); end end Timer.Text = math.ceil(GAUGE_LIMIT_TIME - elapsedTime); -- 残り入力時間 end end); end end UpdateEvent.OnClientEvent:Connect(Update); -- 判定 local function Judge(_winSide, _leftValue, _rightValue) leftValue = _leftValue; rightValue = _rightValue; -- テキスト表示 LeftNumber.Text = string.format(GAUGE_VALUE_FORMAT, leftValue); RightNumber.Text = string.format(GAUGE_VALUE_FORMAT, rightValue); --ゲージ更新 local size = LeftGauge.Size; LeftGauge.Size = UDim2.new(leftValue / 100, size.X.Offset, size.Y.Scale, size.Y.Offset); size = RightGauge.Size; RightGauge.Size = UDim2.new(rightValue / 100, size.X.Offset, size.Y.Scale, size.Y.Offset); -- 勝った方のUIを変更する(引き分けは何もしない) if _winSide == SIDE_TYPE.Left then LeftWin.Visible = true; elseif _winSide == SIDE_TYPE.Right then RightWin.Visible = true; end end JudgeEvent.OnClientEvent:Connect(Judge); -- 終了 local function Finish() if updateConnection ~= nil then updateConnection:Disconnect(); updateConnection = nil; end end FinishEvent.OnClientEvent:Connect(Finish); Init(-1, -1, -1);
サーバスクリプト Path:ServerScriptService/Gauge
-- Service local ReplicatedStorage = game:GetService("ReplicatedStorage"); -- Module local GaugeModule = require(ReplicatedStorage:WaitForChild("GaugeModule")); -- Event local InitEvent = ReplicatedStorage:WaitForChild("InitEvent"); -- 初期化イベント local UpdateEvent = ReplicatedStorage:WaitForChild("UpdateEvent"); -- 更新イベント local JudgeEvent = ReplicatedStorage:WaitForChild("JudgeEvent"); -- 判定イベント local FinishEvent = ReplicatedStorage:WaitForChild("FinishEvent"); -- 終了イベント local StopEvent = ReplicatedStorage:WaitForChild("StopEvent"); -- 停止イベント -- 定数 local GAUGE_MAX_TIME = 1; -- ゲージが最大値になる時間(秒) local GAUGE_LIMIT_TIME = 3; -- ゲージゲームの時間(秒) local SIDE_TYPE = { -- 左右の種類 None = 0, Left = 1, Right = 2, }; local SCENE_STATE = { -- シーンステート Init = 1, Update = 2, Judge = 3, Finish = 4, }; -- 変数 local leftValue = 0; -- 左の値 local rightValue = 0; -- 右の値 local winSide = 0; -- 勝った方 -- CPU関連 local CPUGaugeStopRange = {20, 80}; -- CPUのゲージ停止範囲 local cpuGaugeValue = 0; -- CPU側の止める値 -- シーンステート local changeStateFunc = nil; -- ステート変更関数の参照 -- 初期化 local function Init() -- 変数 leftValue = -1; rightValue = -1; winSide = SIDE_TYPE.None; cpuGaugeValue = math.random(CPUGaugeStopRange[1], CPUGaugeStopRange[2]); -- 初期化イベント(クライアント) InitEvent:FireAllClients(GAUGE_MAX_TIME, GAUGE_LIMIT_TIME, cpuGaugeValue); end local function InitEnd(_player) -- ステート変更 changeStateFunc(SCENE_STATE.Update); end InitEvent.OnServerEvent:Connect(InitEnd); -- 更新 local function Update() -- 更新イベント(クライアント) UpdateEvent:FireAllClients(); end -- 判定 local function Judge() if leftValue > rightValue then -- 左の勝ち winSide = SIDE_TYPE.Left; elseif leftValue = GAUGE_LIMIT_TIME then _elapsedTime = GAUGE_LIMIT_TIME; gauge = 0; end -- チェック結果エラー if gauge ~= _value then _value = 0; end if _side == SIDE_TYPE.Left then leftValue = _value; elseif _side == SIDE_TYPE.Right then rightValue = _value; end -- 両値が0以上 if leftValue >= 0 and rightValue >= 0 then -- ステート変更 changeStateFunc(SCENE_STATE.Judge); end end StopEvent.OnServerEvent:Connect(Stop); -- ステート変更 local function changeState(_state) if _state == SCENE_STATE.Init then -- 初期化 Init(); elseif _state == SCENE_STATE.Update then -- 更新 Update(); elseif _state == SCENE_STATE.Judge then -- 判定 Judge(); elseif _state == SCENE_STATE.Finish then -- 終了 Finish(); else -- その他 print("error change state"..tostring(_state)); end end changeStateFunc = changeState; -- 待ち wait(3); -- 最初のシーンは初期化から始める changeStateFunc(SCENE_STATE.Init);
モジュールスクリプト Path:ReplicatedStorage/GaugeModule
local GaugeModule = {} -- ゲージの割合計算 function GaugeModule:calcGaugeRatio(_deltaTime, _gaugeValue, _gaugeMaxTime) local per = 0; -- ゲージサイズ割合 local value = _gaugeValue + _deltaTime; -- ゲージの値の加算 -- 値が最大値を超えたか local floor = math.floor(value / _gaugeMaxTime); if floor > math.floor(_gaugeValue / _gaugeMaxTime) then _gaugeValue = floor * _gaugeMaxTime; per = 1; else _gaugeValue = value; per = _gaugeValue / _gaugeMaxTime; local floor = math.floor(per); per = per - floor; end return _gaugeValue, per; end return GaugeModule