【RobloxStudio】電車のモデルを操作できるようにしてみた

○○の作り方

完成動画

はじめに

本記事では、電車のモデルを操作できるようにする方法を紹介します。プレイヤーが電車に近づいて座り、WキーやSキーで前進・後退できるようにします。

主な使用する機能

  • ProximityPrompt
    座席への着席をプレイヤーに促すUI表示
  • Seat
    プレイヤーが座ることのできるパーツ
  • Script / LocalScript / RemoteEvent
    サーバーとクライアント間の通信と処理分担に使用
  • RunService.Heartbeat
    毎フレーム処理

手順動画

エクスプローラー構成

Workspace
└Subway Train
 └Seat
  └Script
 …
 └Entrance(Part)
  └ProximityPrompt
 …
ReplicatedStorage
└ThrottleEvent(RemoteEvent)
ServerScriptService
└Script2
StarterPlayer
└StarterPlayerScripts
 └LocalScript

追加と変更箇所

  1. ツールボックスからSubway Trainを追加
  2. Subway Trainにプレイヤーが座るSeatを追加
  3. Seatの子要素にScriptを追加
  4. Subway TrainにEntrance(Part)を追加
  5. Entranceを電車の入り口付近に設置
  6. Entranceを透明にし、CanCollideをオフにする
  7. Entranceの子要素にProximityPromptを追加
  8. Subway TrainのPrimaryPartを設定
  9. ReplicatedStorageにRemoteEventを追加
  10. ServerScriptServiceにScriptを追加
  11. StarterPlayerScriptsにLocalScriptを追加

スクリプトの内容

Script1

local seat = script.Parent
local prompt = seat:FindFirstChildOfClass("ProximityPrompt")

-- 最初は座席を無効化(勝手に乗れないように)
seat.Disabled = true

-- ProximityPromptが発火したときの処理
prompt.Triggered:Connect(function(player)
	local character = player.Character
	if not character then return end

	local root = character:FindFirstChild("HumanoidRootPart")
	if root then
		-- Seatを一時的に有効化
		seat.Disabled = false

		-- ProximityPromptを非表示にする
		prompt.Enabled = false

		-- プレイヤーを座席位置にワープ(自動で座る)
		root.CFrame = seat.CFrame + Vector3.new(0, 2, 0)
	end
end)

-- プレイヤーが降りたとき(Occupantがnilになる)→ Seatを無効化&プロンプトを再表示
seat:GetPropertyChangedSignal("Occupant"):Connect(function()
	if seat.Occupant == nil then
		-- 降りたら再び座席を無効に
		seat.Disabled = true
		-- ProximityPromptを再び表示
		prompt.Enabled = true
	end
end)

1. 変数の準備

local seat = script.Parent
local prompt = seat:FindFirstChildOfClass("ProximityPrompt")
  • seat: このスクリプトが入っている Seat(座席) を参照。
  • prompt: seat 内の ProximityPrompt を取得。これが「Eキーで座る」などのUIになります。

2. 最初に座席を無効化

seat.Disabled = true
  • Seat.Disabled = true にして、自動で座れないようにしています。
  • これがないと、近くに来ただけでプレイヤーが勝手に座ってしまいます。

3. ProximityPrompt が押されたときの処理

prompt.Triggered:Connect(function(player)
  • プレイヤーが Eキー などでプロンプトを実行したら呼ばれます。

4. プレイヤーのキャラクターと位置取得

local character = player.Character
if not character then return end
local root = character:FindFirstChild("HumanoidRootPart")
  • キャラクターの中心部分(HumanoidRootPart)を取得。
  • これを使ってワープさせます。

5. 座席にワープして自動着席

seat.Disabled = false
prompt.Enabled = false
root.CFrame = seat.CFrame + Vector3.new(0, 2, 0)
  • Disabled = false にすることで、着席可能な状態に一時的に変更
  • プロンプトを非表示にして「Eキーの表示」を消します。
  • 2スタッド分上から座席にCFrameワープすることで、自動で座る挙動を実現。

6. 降りたときの処理(Occupant が nil に)

seat:GetPropertyChangedSignal("Occupant"):Connect(function()
  • Seat に誰も座っていないとき(Occupant == nil)を検知します。

7. 降りたら座席を再度無効化&プロンプト表示

if seat.Occupant == nil then
seat.Disabled = true
prompt.Enabled = true
end
  • 降りたらまた Disabled = true にして勝手に座れないようにします。
  • prompt.Enabled = true にして再び Eキー の案内を表示。

Script2

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local throttleEvent = ReplicatedStorage:WaitForChild("ThrottleEvent")
local trainModel = workspace:WaitForChild("Subway Train")
local seat = trainModel:WaitForChild("Seat") -- Seatの名前に合わせて変更

-- ここでは Seat のあるモデルの PrimaryPart が設定されている必要あり
if not trainModel.PrimaryPart then
	warn("TrainModel に PrimaryPart が設定されていません")
	return
end

local maxSpeed = 80
local acceleration = 60
local currentSpeed = 0
local throttleMap = {}

-- RemoteEvent受信:プレイヤーごとに throttle 記録
throttleEvent.OnServerEvent:Connect(function(player, throttle)
	throttleMap[player] = throttle
end)

-- Heartbeat で速度更新&移動
RunService.Heartbeat:Connect(function(dt)
	local occupant = seat.Occupant
	if occupant then
		local player = game.Players:GetPlayerFromCharacter(occupant.Parent)
		if player then
			local throttle = throttleMap[player] or 0

			if throttle ~= 0 then
				currentSpeed = currentSpeed + (acceleration * throttle * dt)
			else
				if currentSpeed > 0 then
					currentSpeed = math.max(0, currentSpeed - acceleration * dt)
				elseif currentSpeed < 0 then
					currentSpeed = math.min(0, currentSpeed + acceleration * dt)
				end
			end

			currentSpeed = math.clamp(currentSpeed, -maxSpeed, maxSpeed)
			local moveDirection = -trainModel.PrimaryPart.CFrame.LookVector
			local moveDelta = moveDirection * currentSpeed * dt
			trainModel:TranslateBy(moveDelta)
		end
	end
end)

1. 必要なサービスの取得

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
  • ReplicatedStorage: クライアントとサーバー間で通信するために ThrottleEvent を格納。
  • RunService: フレーム毎の処理(Heartbeat)に使います。1フレームごとに処理を実行できます。

2. オブジェクトの取得と確認

local throttleEvent = ReplicatedStorage:WaitForChild("ThrottleEvent")
local trainModel = workspace:WaitForChild("Subway Train")
local seat = trainModel:WaitForChild("Seat")
  • ThrottleEvent: プレイヤーからの操作指示(前進・後退)を受け取るための RemoteEvent
  • trainModel: ワールドにある電車モデル(名前は "Subway Train")。
  • seat: プレイヤーが乗る座席。

3. PrimaryPart の確認

if not trainModel.PrimaryPart then
warn("TrainModel に PrimaryPart が設定されていません")
return
end
  • PrimaryPart はモデルの基準点。ここを基準に電車を移動させるため、設定されていないと動かせません
  • 未設定なら警告を出して処理中止。

4. 速度制御の初期値と入力マップの用意

local maxSpeed = 80
local acceleration = 60
local currentSpeed = 0
local throttleMap = {}
  • maxSpeed: 電車の最高速度(単位: スタッド/秒)。
  • acceleration: 加速の強さ(数値が大きいほど速く加減速)。
  • currentSpeed: 現在の速度。
  • throttleMap: プレイヤーごとの入力(-1, 0, 1)を記録する辞書。

5. RemoteEvent で操作入力を受け取る

throttleEvent.OnServerEvent:Connect(function(player, throttle)
throttleMap[player] = throttle
end)
  • プレイヤーが W1S-1、キーを離すと 0 を送信。
  • throttleMap にその入力を記録。
    (※こうすることで座席に座ってる人だけが電車を動かせるようにできる)

6. 毎フレームの速度更新と移動処理

RunService.Heartbeat:Connect(function(dt)
  • dt は前のフレームからの経過時間(秒)。フレームレートに依存しない計算に使う。

7. 座っているプレイヤーの取得

local occupant = seat.Occupant
  • Occupant に何か入っていれば誰かが座っている。
local player = game.Players:GetPlayerFromCharacter(occupant.Parent)
  • Humanoid → 親がプレイヤーのキャラクターなので、そこからプレイヤーを特定。

8. 入力に応じた速度の変化

local throttle = throttleMap[player] or 0
  • 操作入力があればそれを取得。なければ 0
if throttle ~= 0 then
currentSpeed = currentSpeed + (acceleration * throttle * dt)
  • 入力中は加減速(+acceleration or -acceleration)していく。
else
if currentSpeed > 0 then
currentSpeed = math.max(0, currentSpeed - acceleration * dt)
elseif currentSpeed < 0 then
currentSpeed = math.min(0, currentSpeed + acceleration * dt)
end
  • 入力が 0 なら自然減速。ブレーキのようなイメージです。
currentSpeed = math.clamp(currentSpeed, -maxSpeed, maxSpeed)
  • 速度を -maxSpeedmaxSpeed の範囲に制限。暴走防止。

9. 電車の移動方向と移動距離の計算

local moveDirection = -trainModel.PrimaryPart.CFrame.LookVector
local moveDelta = moveDirection * currentSpeed * dt
trainModel:TranslateBy(moveDelta)
  • LookVector(前方ベクトル)を基に、電車の進行方向を取得
    • -LookVector にして逆方向(プレイヤーから見て前進)に。
  • currentSpeed * dt で「今回のフレームでどれだけ移動するか」を決定。
  • TranslateBy によって 電車全体を滑らかに移動

LocalScript

local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

local player = Players.LocalPlayer
local camera = workspace.CurrentCamera
local cameraConnection = nil

-- RemoteEvent の取得
local throttleEvent = ReplicatedStorage:WaitForChild("ThrottleEvent")

-- 操作状態
local throttleInput = 0

-- 入力処理:Wで前進、Sで後退、キーを離すと0に戻る
UserInputService.InputBegan:Connect(function(input, processed)
	if processed then return end

	if input.KeyCode == Enum.KeyCode.W then
		throttleInput = 1
		throttleEvent:FireServer(throttleInput)
	elseif input.KeyCode == Enum.KeyCode.S then
		throttleInput = -1
		throttleEvent:FireServer(throttleInput)
	end
end)

UserInputService.InputEnded:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.W or input.KeyCode == Enum.KeyCode.S then
		throttleInput = 0
		throttleEvent:FireServer(throttleInput)
	end
end)

-- カメラ追従ループの処理
local function updateCameraLoop(seat)
	-- 古いループを止める(乗り直し対策)
	if cameraConnection then
		cameraConnection:Disconnect()
	end

	cameraConnection = RunService.RenderStepped:Connect(function()
		if seat and seat.Parent then
			local seatCF = seat.CFrame
			local offset = CFrame.new(-2.7, 17, 17)
			local angle = CFrame.Angles(math.rad(-15), math.rad(0), 0)
			camera.CFrame = seatCF * offset * angle
		end
	end)
end

-- キャラクターが Seat に座ったときにカメラ切り替え
local function onCharacterAdded(character)
	local humanoid = character:WaitForChild("Humanoid")
	local rootPart = character:WaitForChild("HumanoidRootPart")

	humanoid.Seated:Connect(function(active, seat)
		if active and seat and seat:IsA("Seat") then
			-- カメラ切り替え
			camera.CameraType = Enum.CameraType.Scriptable

			-- カメラ追従スタート
			updateCameraLoop(seat)

			-- 初期カメラ位置セット
			local seatCF = seat.CFrame
			local offset = CFrame.new(-2.7, 17, 17) 
			local angle = CFrame.Angles(math.rad(-15), math.rad(0), 0)
			camera.CFrame = seatCF * offset * angle
		else
			-- 降車時にカメラリセット&追従解除
			camera.CameraType = Enum.CameraType.Custom

			if cameraConnection then
				cameraConnection:Disconnect()
				cameraConnection = nil
			end

			-- Entrance にテレポートさせる
			local entrance = workspace["Subway Train"]:FindFirstChild("Entrance")
			print(entrance)
			if entrance and entrance:IsA("Part") then
				rootPart.CFrame = entrance.CFrame + Vector3.new(-2, 2, 0) -- 少し上に浮かせて配置
			end
		end
	end)
end


-- キャラクター生成時に接続
player.CharacterAdded:Connect(onCharacterAdded)
if player.Character then
	onCharacterAdded(player.Character)
end

1. サービスの取得

local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
  • Robloxでよく使うサービスを取得しています。
    • Players: プレイヤーの管理。
    • UserInputService: キーボードやマウスなどの入力を検知。
    • ReplicatedStorage: クライアントとサーバー間で共有されるオブジェクトを格納。
    • RunService: 毎フレームの処理を実行するために使用。

2. プレイヤーとカメラ情報の取得

local player = Players.LocalPlayer
local camera = workspace.CurrentCamera
local cameraConnection = nil
  • LocalPlayer で現在のプレイヤーを取得。
  • CurrentCamera で現在のカメラを操作できるように。
  • cameraConnection はカメラ追従の接続を保持し、乗り直したときに前のループを解除できるようにしている。

3. RemoteEvent(ThrottleEvent)の取得と変数初期化

local throttleEvent = ReplicatedStorage:WaitForChild("ThrottleEvent")
local throttleInput = 0
  • ReplicatedStorage から ThrottleEvent(操作入力をサーバーに送るイベント)を取得。
  • throttleInput はキー入力に応じて、前進(1)、後退(-1)、停止(0)の状態を保持する変数。

4. ユーザーのキー入力処理(前進・後退)

UserInputService.InputBegan:Connect(function(input, processed)
if processed then return end
  • プレイヤーがキーを押したときの処理を設定。
  • 他のUIで使われていた入力なら無視する。
	if input.KeyCode == Enum.KeyCode.W then
throttleInput = 1
throttleEvent:FireServer(throttleInput)
elseif input.KeyCode == Enum.KeyCode.S then
throttleInput = -1
throttleEvent:FireServer(throttleInput)
end
end)
  • Wキー → 前進(1)
  • Sキー → 後退(-1)
  • 入力があったらサーバーに現在の入力状態を送信(FireServer

5. キーを離したときの処理

UserInputService.InputEnded:Connect(function(input)
if input.KeyCode == Enum.KeyCode.W or input.KeyCode == Enum.KeyCode.S then
throttleInput = 0
throttleEvent:FireServer(throttleInput)
end
end)
  • W または S キーを離すと、操作を停止状態(0)に。
  • サーバーに再度その状態を送信。

6. カメラ追従用の関数定義

local function updateCameraLoop(seat)
  • プレイヤーが座った座席にカメラを追従させるループ処理の開始。
	if cameraConnection then
cameraConnection:Disconnect()
end
  • 既に接続中のカメラ追従があれば停止(再接続による二重処理を防ぐ)。
	cameraConnection = RunService.RenderStepped:Connect(function()
if seat and seat.Parent then
local seatCF = seat.CFrame
local offset = CFrame.new(-2.7, 17, 17)
local angle = CFrame.Angles(math.rad(-15), math.rad(0), 0)
camera.CFrame = seatCF * offset * angle
end
end)
  • 毎フレーム(RenderStepped)カメラ位置を更新。
  • 座席の位置から少し後ろ&上の視点にカメラを設置。
  • 斜めにカメラを傾けることで自然な視点を演出。

7. キャラクターが座ったときの処理

local function onCharacterAdded(character)
local humanoid = character:WaitForChild("Humanoid")
local rootPart = character:WaitForChild("HumanoidRootPart")
  • プレイヤーキャラが生成されたときに呼ばれる関数。
  • キャラ内の HumanoidHumanoidRootPart を取得(動作&移動に必要)。
	humanoid.Seated:Connect(function(active, seat)
  • プレイヤーが Seat に座る or 降りるイベント。

8. 座ったときの処理(カメラ変更+追従開始)

		if active and seat and seat:IsA("Seat") then
camera.CameraType = Enum.CameraType.Scriptable
updateCameraLoop(seat)
  • 座席に座ったらカメラをスクリプト制御に切り替え。
  • updateCameraLoop を呼び出してカメラの追従開始。
			local seatCF = seat.CFrame
local offset = CFrame.new(-2.7, 17, 17)
local angle = CFrame.Angles(math.rad(-15), math.rad(0), 0)
camera.CFrame = seatCF * offset * angle
  • 初期のカメラ位置をすぐに設定(追従開始前に即反映)。

9. 降車時の処理(カメラリセット+ワープ)

		else
camera.CameraType = Enum.CameraType.Custom
  • カメラを元の自動制御に戻す(FPSやTP視点に戻る)
			if cameraConnection then
cameraConnection:Disconnect()
cameraConnection = nil
end
  • 追従ループを解除して無駄な処理を削除。
			local entrance = workspace["Subway Train"]:FindFirstChild("Entrance")
print(entrance)
if entrance and entrance:IsA("Part") then
rootPart.CFrame = entrance.CFrame + Vector3.new(-2, 2, 0)
end
  • ワールド内の Entrance パーツを探して、そこにプレイヤーを移動(少し浮かせて配置)。
  • Subway Train モデル内に Entrance というパーツが必要。

10. キャラクターが生成されたときに処理を登録

player.CharacterAdded:Connect(onCharacterAdded)
if player.Character then
onCharacterAdded(player.Character)
end
  • キャラクターが復活・生成されたときに onCharacterAdded を呼び出す。
  • プレイヤーがすでにスポーンしている場合にも即時処理する。

まとめ

この仕組みによって、「近づいてEキーで座る → W/Sキーで運転 → 降りるとリセット」という流れを実現できます。

チュートリアル系電子書籍の紹介
Roblox Studioでゲーム制作を始めてみませんか?
日本語版 ※英語版あり 「10時間でできる Roblox Studio アクションゲーム制作入門」では、Roblox Studioでのゲーム制作の基本から応用までを短時間で効率的に学びます。この一冊で、初心者でも簡単にアクションゲームを制作できるようになり、ゲーム制作の面白さと達成感を存分に味わうことができます。Roblox Studioの世界への扉を開き、あなたのクリエイティブな旅を始めましょう。
チュートリアル系動画
電子書籍を動画にしました!※無料クーポンあり!

「10時間でできる Roblox Studio アクションゲーム制作入門」の動画版では、書籍の内容をさらにわかりやすく、実際の画面操作を見ながら学べる形式でお届けします。初心者でも安心して取り組めるように、Roblox Studioの基本操作からアクションゲームの完成までを丁寧に解説。テンポよく進むレクチャー動画で、あなたのペースに合わせて学習できます。この動画シリーズを通して、ゲーム制作の基礎をしっかり習得し、Roblox Studioの可能性を広げましょう!

割引クーポンコードはこちらです↓
※3月12日21時まで!
6E0150A3770784ADF77D

Luaに関する電子書籍の紹介
Roblox Studioで使用するLua言語の基本を学ぼう!
日本語版 ※英語版あり 「5時間でできる Roblox Studio を使ったLuaの基本」では、Roblox Studioで使用するプログラミング言語Luaの基本をRoblox Studio上で学びます。また、学んだ知識を活かして、Roblox StudioでLuaを使用する応用部分も一部盛り込まれています。この一冊でLuaの基本を学ぶことで、よりRoblox Studioでのゲーム制作を楽しめるようになります。
アニメーション制作系電子書籍の紹介
Roblox Studioでアニメーションを作ってみよう!
「5時間でできる Roblox Studio アニメーション制作入門」では、Roblox Studioでのアニメーション制作の基本から実際に作るところまでを短時間で効率的に学びます。この一冊で、アニメーション制作の第一歩を踏み出すことができます。Roblox Studioで自由自在にアニメーションを作成し、よりクリエイティブなゲームを作りましょう!
○○の作り方Roblox StudioScriptingToolbox
シェアする
hideをフォローする
Roblox Studio 制作tips

コメント

タイトルとURLをコピーしました