[ROBLOX]ゲーム開発⑥(ステージ作成)
前回はスクリプトを使ってゴールした時の処理を実装した
今回は途中のステージギミックを色々追加してゲームを完成させてゆきたい
死ぬ処理
まず海に落ちたら死んでしまう処理を追加する
流れはゴールの処理と同じでプレイヤーが何かに触れたらイベントを発動させる感じだ
海に落ちた判定用のブロックを追加する
まずシーン全体をカバーするような巨大なブロックパーツをシーンに配置する
水面ギリギリちょっと上ぐらいに位置を調整する
Workspace->Worldの下に移動させHazardという名前を付けておく
Hazardのプロパティで
Transparency:1 (見えなくする)
CanCollide:OFF (衝突判定を消す)
Anchore:ON (物理計算を無効化する)
の3つを設定しておく
Hazardに触れたら死ぬスクリプトを追加する
ServerScrptServiceの下にScriptを追加し、HazardScriptとリネームしておく
スクリプトを編集する
local Workspace = game:GetService("Workspace")
local hazard = Workspace.World.Hazard
local function onHazardTouched(otherPart)
local character = otherPart.Parent
local player = game.Players:GetPlayerFromCharacter(character)
if player and character.Humanoid then
character.Humanoid.Health = 0
end
end
hazard.Touched:Connect(onHazardTouched)
hazardに触れたPlayerのHPを0にすることでキャラクターは死ぬ
Player,Character,Huamnoidとは?
コードの中に現れるPlayer,Character,Humanoidを軽く説明しておく
Playerはユーザーアカウントそのものだ
Characterはユーザーが所持しているアバター
Humanoidはヒト型アバターに必要な情報
をそれぞれ表している
Playerの下にはCharacter以外にも
Backpack: プレイヤーが持っているツールやアイテムを保持
PlayerGui: 各プレイヤーに固有のGUI
PlayerScripts: クライアント側で実行されるスクリプト
StarterGear: プレイヤーがゲームを開始する際に自動的にバックパックに追加されるアイテム
といたものが含まれている
Characterの下にはHumanoid以外にHeadやArm,Legといったアバターを構成するパーツや、アクセサリーやアニメーションなどが含まれている
Humanoidには
MaxHealth: キャラクターの最大ヘルス値。
WalkSpeed: キャラクターの歩行速度。
JumpPower: キャラクターのジャンプ力。
Sit: キャラクターが座っているかどうかを示すブール値。
などといった変数が格納されている
Robloxでは他のゲームでライフとかヒットポイントと呼ばれる値はヘルスと呼ぶようだ
テストプレイ
海に落ちると、アバターがバラバラになってしまう
ステージを作る
ゼロからシーンを組み立てると結構大変なので、なにか使えそうなものがないか探していたら、ツールボックスに使えそうなのがあったのでこれをベースにしてみようと思う。
本来はダンジョンのようだ
スタート地点に一番下の部分をあわせてみる
ちょっとそれっぽい
ゴールは一番上に配置
高い壁にハシゴを追加する
素のままの状態でゴールを目指して歩いてみると、いい塩梅に越えられない壁が複数あるので、いかに壁を越えるかをギミックとして活用する
まず最初の壁はハシゴを設置しよう
ツールボックスにあるハシゴは基本的にどれもよじ登ることができる
踏み台となる動く箱を置く
別の壁では、箱を動かして踏み台にして先に進むギミックをいれてみる
似たような箱のモデルはたくさんツールボックスにあるのだが、できるだけシンプルな構造を持ったものを選んだ
プロパティでAnchoredをOFFににして物理計算を有効にしておく
これをPlayerが蹴って動かしていい位置にずらして足場にして壁を越えるギミックだ
箱を蹴っていると失敗して海に落下してしまうことが起きる
このままだと進行不能になるので、10秒経つと箱が元の位置に戻るようにする
Partの下にスクリプトを追加
コードはこんな感じ
local part = script.Parent -- オブジェクトを指定
local initialCFrame = part.CFrame -- 初期のCFrameを保存
local lastPosition = part.Position -- 最後の位置を保存
local timeSinceLastMove = 0 -- 最後の移動からの時間を初期化
-- 位置と向きをリセットする関数
function resetCFrame()
part.CFrame = initialCFrame -- 保存した初期のCFrameに戻す
timeSinceLastMove = 0 -- リセット時にタイマーをリセット
lastPosition = part.Position -- 最後の位置を更新
end
-- タイマーを監視する関数
function monitorPosition()
while true do
wait(1) -- 1秒ごとに位置をチェック
if (part.Position - lastPosition).magnitude < 0.1 then -- ほとんど動いていない場合
timeSinceLastMove = timeSinceLastMove + 1
else
timeSinceLastMove = 0 -- 動いた場合はタイマーをリセット
lastPosition = part.Position -- 最後の位置を更新
end
if timeSinceLastMove >= 10 then -- 10秒間動かなかった場合
resetCFrame() -- 初期位置に戻す
end
end
end
-- 位置監視を開始
spawn(monitorPosition)
spawnというキーワードはもとのLUAには無いらしい
Roblox固有のもので、指定した関数を非同期で実行させるものだ
敵も配置してみる
シーン上に少し広いエリアがあるので、そこに敵を配置してみる
このPolice Man Zombieは配置するだけでプレイヤーを追いかけてきて攻撃もしてくる
インポートしたままの状態だと遠くから延々とプレイヤーを追いかけて勝手に海に落るので敵として機能してくれない
AIルーチンを修正してみる
Zombieがオブジェクトのルートでその下にAIというスクリプトファイルがはいっていおり、ここに挙動が定義されているようだ
これを編集してみる
まず反応が早すぎるのでプレイヤーが近づいたときにだけ反応するようにしたい
AIを開くと上のほうにこんなパラメータがある
local SearchDistance = 10000 -- How far a player can be before it detects you
SearchDistance = 50
として、けっこう近づくまで動かないように修正する
簡単に修正できたと思いテストプレイしてみると、まだ勝手に海に落ちる
スクリプトをもう少し詳しく読むと、キャラクターはプレイヤーがいないときは勝手にウロウロ歩くようになっていることが分かった
それで勝手に海に落ちる
このコードがウロウロ歩き回る処理なので丸ごとカットした。
-- wandering
spawn(function()
while vars.Wandering.Value == false and human.Health > 0 do
vars.Chasing.Value = false
vars.Wandering.Value = true
local desgx, desgz = hroot.Position.x+math.random(-WanderX,WanderX), hroot.Position.z+math.random(-WanderZ,WanderZ)
local function checkw(t)
local ci = 3
if ci > #t then
ci = 3
end
if t[ci] == nil and ci < #t then
repeat ci = ci + 1 wait() until t[ci] ~= nil
return Vector3.new(1,0,0) + t[ci]
else
ci = 3
return t[ci]
end
end
path = pfs:FindPathAsync(hroot.Position, Vector3.new(desgx, 0, desgz))
waypoint = path:GetWaypoints()
local connection;
local direct = Vector3.FromNormalId(Enum.NormalId.Front)
local ncf = hroot.CFrame * CFrame.new(direct)
direct = ncf.p.unit
local rootr = Ray.new(hroot.Position, direct)
local phit, ppos = game.Workspace:FindPartOnRay(rootr, hroot)
if path and waypoint or checkw(waypoint) then
if checkw(waypoint) ~= nil and checkw(waypoint).Action == Enum.PathWaypointAction.Walk then
human:MoveTo( checkw(waypoint).Position )
human.Jump = false
end
if checkw(waypoint) ~= nil and checkw(waypoint).Action == Enum.PathWaypointAction.Jump then
connection = human.Changed:connect(function()
human.Jump = true
end)
human:MoveTo( waypoint[4].Position )
else
human.Jump = false
end
if connection then
connection:Disconnect()
end
else
for i = 3, #waypoint do
human:MoveTo( waypoint[i].Position )
end
end
wait(math.random(4,6))
vars.Wandering.Value = false
end
end)
さらにプレイヤーが見つからなかった場合にウロウロモードに移行する処理があるので、ここもカット
elseif nrstt == nil then -- if player not detected
vars.Wandering.Value = false
vars.Chasing.Value = false
CchaseName = nil
path = nil
waypoint = nil
human.MoveToFinished:Wait()
これで望んだ挙動になってくれた
先ほどの箱と同様に、敵もこちらを追いかける最中に海に落ちたりするので、プレイヤーが発見できないときは初期位置に戻るようにするコードも追加する
最終的なAIのスクリプトはこうなる
--DuruTeru
--[[
____________________________________________________________________________________________________________________
i smell leik beef
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
____________________________________________________________________________________________________________________
___ ___
( ) ( ) .-.
.--. .--. | |_ | |_ ( __) ___ .-. .--. .--.
/ _ \ / \ ( __) ( __) (''") ( ) \ / \ / _ \
. .' `. ; | .-. ; | | | | | | | .-. . ; ,-. ' . .' `. ;
| ' | | | | | | | | ___ | | ___ | | | | | | | | | | | ' | |
_\_`.(___) | |/ | | |( ) | |( ) | | | | | | | | | | _\_`.(___)
( ). '. | ' _.' | | | | | | | | | | | | | | | | | | ( ). '.
| | `\ | | .'.-. | ' | | | ' | | | | | | | | | ' | | | | `\ |
; '._,' ' ' `-' / ' `-' ; ' `-' ; | | | | | | ' `-' | ; '._,' '
'.___.' `.__.' `.__. `.__. (___) (___)(___) `.__. | '.___.'
( `-' ;
`.__.
____________________________________________________________________________________________________________________
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
____________________________________________________________________________________________________________________
--]]
local SearchDistance = 50 -- How far a player can be before it detects you
local ZombieDamage = 25 -- How much damage the Zombie inficts towards the player
local DamageWait = 2 -- How many seconds to wait before it can damage the player again
local WanderX, WanderZ = 30, 30
-- How many studs the zombie can wander on the x and z axis in studs ; 0, 0 to stay still
--[[
____________________________________________________________________________________________________________________
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
____________________________________________________________________________________________________________________
--]]
function getHumanoid(model)
for _, v in pairs(model:GetChildren())do
if v:IsA'Humanoid' then
return v
end
end
end
local zombie = script.Parent
local human = getHumanoid(zombie)
local hroot = zombie.HumanoidRootPart
local zspeed = hroot.Velocity.magnitude
local head = zombie:FindFirstChild'Head'
local vars = script.vars
local initialCFrame = hroot.CFrame -- 初期のCFrameを保存
local pfs = game:GetService("PathfindingService")
local players = game:GetService('Players')
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
local path
local waypoint
local chaseName = nil
function GetTorso(part)
local chars = game.Workspace:GetChildren()
local chaseRoot = nil
local chaseTorso = nil
local chasePlr = nil
local chaseHuman = nil
local mag = SearchDistance
for i = 1, #chars do
chasePlr = chars[i]
if chasePlr:IsA'Model' and chasePlr ~= zombie then
chaseHuman = getHumanoid(chasePlr)
chaseRoot = chasePlr:FindFirstChild'HumanoidRootPart'
if chaseRoot ~= nil and chaseHuman ~= nil and chaseHuman.Health > 0 and chaseHuman.Name ~= "Zombie" then
if (chaseRoot.Position - part).magnitude < mag then
chaseName = chasePlr.Name
chaseTorso = chaseRoot
mag = (chaseRoot.Position - part).magnitude
end
end
end
end
return chaseTorso
end
function GetPlayersBodyParts(t)
local torso = t
if torso then
local figure = torso.Parent
for _, v in pairs(figure:GetChildren())do
if v:IsA'Part' then
return v.Name
end
end
else
return "HumanoidRootPart"
end
end
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
local damagetime
local damagedb = false
for _, zambieparts in pairs(zombie:GetChildren())do
if zambieparts:IsA'Part' and human.Health > 0 then
zambieparts.Touched:connect(function(p)
if p.Parent.Name == chaseName and p.Parent.Name ~= zombie.Name and not damagedb then -- damage
damagedb = true
damagetime = time()
local enemy = p.Parent
local enemyhuman = getHumanoid(enemy)
vars.Attacking.Value = true
enemyhuman:TakeDamage(ZombieDamage)
vars.Attacking.Value = false
while wait() do
if damagetime ~= nil and time() >= (damagetime + DamageWait) then
damagedb = false
damagetime = nil
end
end
end
end)
end
end
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
while wait() do
local nrstt = GetTorso(hroot.Position)
if nrstt ~= nil and human.Health > 0 then -- if player detected
vars.Wandering.Value = false
vars.Chasing.Value = true
local function checkw(t)
local ci = 3
if ci > #t then
ci = 3
end
if t[ci] == nil and ci < #t then
repeat ci = ci + 1 wait() until t[ci] ~= nil
return Vector3.new(1,0,0) + t[ci]
else
ci = 3
return t[ci]
end
end
path = pfs:FindPathAsync(hroot.Position, nrstt.Position)
waypoint = path:GetWaypoints()
local connection;
local direct = Vector3.FromNormalId(Enum.NormalId.Front)
local ncf = hroot.CFrame * CFrame.new(direct)
direct = ncf.p.unit
local rootr = Ray.new(hroot.Position, direct)
local phit, ppos = game.Workspace:FindPartOnRay(rootr, hroot)
if path and waypoint or checkw(waypoint) then
if checkw(waypoint) ~= nil and checkw(waypoint).Action == Enum.PathWaypointAction.Walk then
human:MoveTo( checkw(waypoint).Position )
human.Jump = false
end
if checkw(waypoint) ~= nil and checkw(waypoint).Action == Enum.PathWaypointAction.Jump then
connection = human.Changed:connect(function()
human.Jump = true
end)
human:MoveTo( waypoint[4].Position )
else
human.Jump = false
end
hroot.Touched:connect(function(p)
local bodypartnames = GetPlayersBodyParts(nrstt)
if p:IsA'Part' and not p.Name == bodypartnames and phit and phit.Name ~= bodypartnames and phit:IsA'Part' and rootr:Distance(phit.Position) < 5 then
connection = human.Changed:connect(function()
human.Jump = true
end)
else
human.Jump = false
end
end)
if connection then
connection:Disconnect()
end
else
for i = 3, #waypoint do
human:MoveTo( waypoint[i].Position )
end
end
path = nil
waypoint = nil
elseif nrstt == nil then -- if player not detected
vars.Wandering.Value = false
vars.Chasing.Value = false
hroot.CFrame = initialCFrame -- 初期位置に瞬間移動
wait(1) -- 少し待って再度チェック
end
end
-- Base script for NPC enemy movement,
-- still a work in progress
動く床を置く
次の壁は上下に動く床を利用して上がるギミックにしてみよう
適当にフロアタイルをチョイスして、スクリプトを挿入
スクリプトを追加する
local part = script.Parent -- 動かす床のPartを指定
local initialPosition = part.Position -- 床の初期位置を保存
local amplitude = 5 -- 動かす高さの振幅
local frequency = 1 -- 動かす速度の周波数
-- 振り子運動を作成する関数
function updatePosition()
while true do
local time = tick() -- 現在の時間を取得
local delta = math.sin(time * frequency) * amplitude -- サインカーブを使って位置を計算
part.Position = initialPosition + Vector3.new(0, delta, 0) -- 初期位置からの相対位置を設定
wait(0.1) -- 更新間隔を設定
end
end
-- 振り子運動を開始
spawn(updatePosition)
綱渡りゾーン
ここはスクリプトでの操作などはなく細い通路を落ちないように進んでゆくギミックだ
途中の通路を削除して、適当に鉄骨のモデルとロープのモデルをつなげて配置してみた
玉ゾーン
ゴールに近づいてきたので、最後は派手な演出を入れたい
転がってくる球をよけながら坂を上るギミックを考えてみた
ツールボックスでball spawnerと検索するといくつか出てくるうちの一つを選んだ
特別スクリプトを修正する必要もなかったのでこのまま利用する
見た目が悪いので、煙突のモデルでBall Spawnersを覆ってみた
これで一通り完成だ
一応プロジェクトファイルを共有しておくので、参考にどうぞ
次回はゲームの公開について考えてみようと思う