完成動画
はじめに
本記事では、電車のモデルを操作できるようにする方法を紹介します。プレイヤーが電車に近づいて座り、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追加と変更箇所
- ツールボックスからSubway Trainを追加
- Subway Trainにプレイヤーが座るSeatを追加
- Seatの子要素にScriptを追加
- Subway TrainにEntrance(Part)を追加
- Entranceを電車の入り口付近に設置
- Entranceを透明にし、CanCollideをオフにする
- Entranceの子要素にProximityPromptを追加
- Subway TrainのPrimaryPartを設定
- ReplicatedStorageにRemoteEventを追加
- ServerScriptServiceにScriptを追加
- 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 = trueSeat.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
endPrimaryPartはモデルの基準点。ここを基準に電車を移動させるため、設定されていないと動かせません。- 未設定なら警告を出して処理中止。
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)- プレイヤーが
Wで1、Sで-1、キーを離すと0を送信。 throttleMapにその入力を記録。
(※こうすることで座席に座ってる人だけが電車を動かせるようにできる)
6. 毎フレームの速度更新と移動処理
RunService.Heartbeat:Connect(function(dt)dtは前のフレームからの経過時間(秒)。フレームレートに依存しない計算に使う。
7. 座っているプレイヤーの取得
local occupant = seat.OccupantOccupantに何か入っていれば誰かが座っている。
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)- 入力中は加減速(
+accelerationor-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)- 速度を
-maxSpeed〜maxSpeedの範囲に制限。暴走防止。
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)
end1. サービスの取得
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 = nilLocalPlayerで現在のプレイヤーを取得。CurrentCameraで現在のカメラを操作できるように。cameraConnectionはカメラ追従の接続を保持し、乗り直したときに前のループを解除できるようにしている。
3. RemoteEvent(ThrottleEvent)の取得と変数初期化
local throttleEvent = ReplicatedStorage:WaitForChild("ThrottleEvent")
local throttleInput = 0ReplicatedStorageから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")- プレイヤーキャラが生成されたときに呼ばれる関数。
- キャラ内の
HumanoidとHumanoidRootPartを取得(動作&移動に必要)。
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キーで運転 → 降りるとリセット」という流れを実現できます。




コメント