見出し画像

[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以外にHeadArm,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を覆ってみた

これで一通り完成だ

一応プロジェクトファイルを共有しておくので、参考にどうぞ

次回はゲームの公開について考えてみようと思う

この記事が気に入ったらサポートをしてみませんか?