You can write your topic however you want, but you need to answer these questions:
-
What do you want to achieve? Keep it simple and clear!
A submarine movement system with forward/backward (W/S) and strafing (A/D) movements are directly linked to its own body, while its vertical movement (E/Q) operates independently. Crucially, I want my submarine’s front to always face the exact direction my camera is looking. -
What is the issue? Include screenshots / videos if possible!
It’s my first time coding a complex system like this, i can’t get the sub to face where i’m looking, and the mapping of the movement system is wrong. W goes left, S goes right, D goes Forward, and A goes Backwards.
-
What solutions have you tried so far? Did you look for solutions on the Creator Hub?
Yes, but nothing really helps. I’ve tried Gemini’ing it and using Roblox’s Assistant, the main things are working, but the niche stuff just doesnt. It’s 3 am for me, and i’ve spent wayyy to long trying to debug it.
After that, you should include more details if you have any. Try to make your topic as descriptive as possible, so that it’s easier for people to help you!
Client-Side Script:
--[[
Submarine Input Local Script (Client-Side)
-------------------------------------------
This Local Script runs on the client to:
1. Detect if the local player is sitting in a VehicleSeat or Seat that is part of a submarine model.
2. Capture mouse movement for pitch (up/down) and yaw (left/right) input.
3. Capture keyboard input (W/S for throttle, E for up, Q for down, A/D for strafe, Space for exiting).
4. Send these input values and the camera's CFrame to a server-side script via a RemoteEvent.
5. Handle client-side visuals like locking the mouse cursor.
Designed for Roblox multiplayer environments.
NOTE: This script should be placed in 'StarterPlayerScripts'.
Ensure your Submarine Models have a 'PrimaryPart' set and contain a 'VehicleSeat'
(or regular 'Seat') for the player to occupy.
A 'RemoteEvent' named "SubmarineInput" must exist in ReplicatedStorage.
]]
-- SERVICES
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- REMOTE EVENT
local submarineInputEvent = ReplicatedStorage:WaitForChild("SubmarineInput")
-- CONFIGURATION
local MAX_PITCH_ANGLE = math.rad(45) -- Maximum angle the submarine can pitch up or down (in radians).
-- INTERNAL STATE
local currentTargetPitch = 0
local currentTargetYaw = 0
-- Roll input is removed as A/D will now be for strafing.
local throttleInput = 0 -- -1 (backward), 0 (none), 1 (forward)
local verticalInput = 0 -- -1 (down), 0 (none), 1 (up)
local strafeInput = 0 -- -1 (strafe left), 0 (none), 1 (strafe right)
local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local hum = character:WaitForChild("Humanoid")
local currentSeat = nil -- Reference to the seat the player is currently occupying.
local currentSubmarine = nil -- Reference to the submarine model the player is controlling.
-- Function to update mouse behavior
local function updateMouseLock(lock)
if lock then
UserInputService.MouseBehavior = Enum.MouseBehavior.LockCenter
else
UserInputService.MouseBehavior = Enum.MouseBehavior.Default
end
end
-- Event handler for when the player enters or exits a seat
hum.Seated:Connect(function(isSeated, seat)
if isSeated then
local model = seat:FindFirstAncestorWhichIsA("Model")
if model and model.PrimaryPart then
currentSeat = seat
currentSubmarine = model
updateMouseLock(true)
-- Reset pitch, yaw. The submarine will now align with the camera.
currentTargetPitch = 0
currentTargetYaw = 0
else
currentSeat = nil
currentSubmarine = nil
updateMouseLock(false)
end
else
currentSeat = nil
currentSubmarine = nil
updateMouseLock(false)
-- Reset inputs when unseated
throttleInput = 0
verticalInput = 0
strafeInput = 0 -- Reset strafe input
-- Send a "reset" or "no input" signal to the server when unseated
if submarineInputEvent then
submarineInputEvent:FireServer(nil, CFrame.new(), 0, 0, 0, 0) -- Send dummy CFrame and reset inputs
end
end
end)
-- INPUT HANDLING (Keyboard)
UserInputService.InputBegan:Connect(function(input, gameProcessedEvent)
if gameProcessedEvent or not currentSubmarine then return end
if input.KeyCode == Enum.KeyCode.W then
throttleInput = 1
elseif input.KeyCode == Enum.KeyCode.S then
throttleInput = -1
elseif input.KeyCode == Enum.KeyCode.E then -- E for up
verticalInput = 1
elseif input.KeyCode == Enum.KeyCode.Q then -- Q for down
verticalInput = -1
elseif input.KeyCode == Enum.KeyCode.A then -- A for strafe left
strafeInput = -1
elseif input.KeyCode == Enum.KeyCode.D then -- D for strafe right
strafeInput = 1
elseif input.KeyCode == Enum.KeyCode.Space then -- Space to exit
if currentSeat and hum then
hum.Sit = false -- Force the humanoid to stand up, exiting the seat
end
end
end)
UserInputService.InputEnded:Connect(function(input, gameProcessedEvent)
if gameProcessedEvent or not currentSubmarine then return end
if input.KeyCode == Enum.KeyCode.W and throttleInput == 1 then
throttleInput = 0
elseif input.KeyCode == Enum.KeyCode.S and throttleInput == -1 then
throttleInput = 0
elseif input.KeyCode == Enum.KeyCode.E and verticalInput == 1 then -- E for up
verticalInput = 0
elseif input.KeyCode == Enum.KeyCode.Q and verticalInput == -1 then -- Q for down
verticalInput = 0
elseif input.KeyCode == Enum.KeyCode.A and strafeInput == -1 then -- A for strafe left
strafeInput = 0
elseif input.KeyCode == Enum.KeyCode.D and strafeInput == 1 then -- D for strafe right
strafeInput = 0
end
end)
-- GAME LOOP FUNCTION (Client-Side)
local function sendSubmarineInput(dt)
if not currentSubmarine or not currentSubmarine.PrimaryPart then
return -- No submarine being controlled, do nothing
end
-- Get the camera's current CFrame to align the submarine with
local cameraCFrame = workspace.CurrentCamera.CFrame
-- Send the camera's CFrame and movement inputs to the server.
if submarineInputEvent then
submarineInputEvent:FireServer(currentSubmarine, cameraCFrame, throttleInput, verticalInput, strafeInput)
end
end
-- CONNECT TO GAME LOOP
RunService.RenderStepped:Connect(sendSubmarineInput)
Location:

Server-Side Script:
--[[
Submarine Server Control Script (Server-Side)
----------------------------------------------
This Server Script runs on the server to:
1. Manage the physics objects (AlignOrientation and BodyPosition) for the submarine.
2. Listen for input from clients via a RemoteEvent.
3. Apply movement and rotation based on the received client input.
4. Ensure network ownership for physics stability.
Designed for Roblox multiplayer environments.
NOTE: This script should be placed directly inside each SUBMARINE MODEL.
Ensure the Submarine Model has its 'PrimaryPart' property set.
A 'RemoteEvent' named "SubmarineInput" must exist in ReplicatedStorage.
]]
-- SERVICES
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
-- REMOTE EVENT
local submarineInputEvent = ReplicatedStorage:WaitForChild("SubmarineInput")
-- MODEL REFERENCES
local submarine = script.Parent
if not submarine:IsA("Model") or not submarine.PrimaryPart then
warn("Submarine server control script error: Script's parent is not a Model or has no PrimaryPart set.", submarine)
script:Destroy()
end
local primaryPart = submarine.PrimaryPart
-- CONFIGURATION
local MAX_SPEED = 50 -- Maximum forward/backward speed of the submarine.
local STRAFE_SPEED = 30 -- Maximum strafe speed of the submarine.
local ACCELERATION_RATE = 20 -- How quickly the speed changes.
local VERTICAL_RATE = 10 -- How quickly the submarine moves up/down.
local ALIGN_ORIENTATION_RESPONSIVENESS = 200 -- Higher for faster/snappier rotation.
local BODY_POSITION_RESPONSIVENESS = 200 -- Higher for faster/snappier movement.
-- INTERNAL STATE (Per Submarine)
local currentSpeed = 0
local currentStrafeSpeed = 0
local currentTargetYPosition = primaryPart.Position.Y
-- This CFrame will now store the *pure rotation* of the camera that the submarine should match.
local currentTargetOrientationCFrameRotation = primaryPart.CFrame.Rotation
-- Store raw input values from the client, to be processed in Heartbeat
local latestThrottleInput = 0
local latestVerticalInput = 0
local latestStrafeInput = 0
-- PHYSICS OBJECTS
local alignOrientation = primaryPart:FindFirstChild("SubmarineAlignOrientation")
local bodyPosition = primaryPart:FindFirstChild("SubmarineBodyPosition")
local attachment0 = primaryPart:FindFirstChild("SubmarineAttachment0")
local attachment1 = primaryPart:FindFirstChild("SubmarineAttachment1")
-- Initialize AlignOrientation and its attachments
if not alignOrientation then
alignOrientation = Instance.new("AlignOrientation")
alignOrientation.Name = "SubmarineAlignOrientation"
alignOrientation.Mode = Enum.OrientationAlignmentMode.OneAttachment
alignOrientation.Responsiveness = ALIGN_ORIENTATION_RESPONSIVENESS
alignOrientation.RigidityEnabled = false
alignOrientation.MaxTorque = math.huge
alignOrientation.Parent = primaryPart
end
if not attachment0 then
attachment0 = Instance.new("Attachment")
attachment0.Name = "SubmarineAttachment0"
attachment0.Parent = primaryPart
end
if not attachment1 then
attachment1 = Instance.new("Attachment")
attachment1.Name = "SubmarineAttachment1"
attachment1.Parent = primaryPart
end
alignOrientation.Attachment0 = attachment0
alignOrientation.Attachment1 = attachment1
-- Initialize BodyPosition
if not bodyPosition then
bodyPosition = Instance.new("BodyPosition")
bodyPosition.Name = "SubmarineBodyPosition"
bodyPosition.maxForce = Vector3.new(math.huge, math.huge, math.huge)
bodyPosition.D = BODY_POSITION_RESPONSIVENESS / 10
bodyPosition.P = BODY_POSITION_RESPONSIVENESS
bodyPosition.Parent = primaryPart
end
local controllingPlayer = nil
-- Listen for client input
submarineInputEvent.OnServerEvent:Connect(function(player, receivedSubmarine, receivedCameraCFrame, receivedThrottle, receivedVertical, receivedStrafe)
if receivedSubmarine ~= submarine then return end
if controllingPlayer and controllingPlayer ~= player then return end
if not controllingPlayer then
-- Assign if there's any active input or if the player just seated
if (receivedThrottle ~= 0 or receivedVertical ~= 0 or receivedStrafe ~= 0) then
controllingPlayer = player
primaryPart:SetNetworkOwner(nil)
currentTargetYPosition = primaryPart.Position.Y
-- When first taking control, immediately align to camera's current rotation
currentTargetOrientationCFrameRotation = receivedCameraCFrame.Rotation
end
end
if controllingPlayer == player then
if receivedSubmarine == nil then -- Player unseated (indicated by client sending nil for submarine)
controllingPlayer = nil
currentSpeed = 0
currentStrafeSpeed = 0
latestThrottleInput = 0
latestVerticalInput = 0
latestStrafeInput = 0
primaryPart:SetNetworkOwner(nil)
else
-- Always update the target orientation CFrame's rotation with the camera's rotation
currentTargetOrientationCFrameRotation = receivedCameraCFrame.Rotation
latestThrottleInput = receivedThrottle
latestVerticalInput = receivedVertical
latestStrafeInput = receivedStrafe
end
end
end)
-- SERVER GAME LOOP (for applying physics)
RunService.Heartbeat:Connect(function(dt)
if controllingPlayer and primaryPart then
-- Update currentSpeed (forward/backward)
if latestThrottleInput ~= 0 then
currentSpeed = math.clamp(currentSpeed + (latestThrottleInput * ACCELERATION_RATE * dt), -MAX_SPEED, MAX_SPEED)
else
if currentSpeed > 0 then
currentSpeed = math.max(0, currentSpeed - (ACCELERATION_RATE * dt))
elseif currentSpeed < 0 then
currentSpeed = math.min(0, currentSpeed + (ACCELERATION_RATE * dt))
end
end
-- Update currentStrafeSpeed (left/right)
if latestStrafeInput ~= 0 then
currentStrafeSpeed = math.clamp(currentStrafeSpeed + (latestStrafeInput * ACCELERATION_RATE * dt), -STRAFE_SPEED, STRAFE_SPEED)
else
if currentStrafeSpeed > 0 then
currentStrafeSpeed = math.max(0, currentStrafeSpeed - (ACCELERATION_RATE * dt))
elseif currentStrafeSpeed < 0 then
currentStrafeSpeed = math.min(0, currentStrafeSpeed + (ACCELERATION_RATE * dt))
end
end
-- Update currentTargetYPosition
currentTargetYPosition = currentTargetYPosition + (latestVerticalInput * VERTICAL_RATE * dt)
-- Update AlignOrientation to camera's orientation (excluding position)
-- Set attachment1's CFrame to a CFrame with origin at (0,0,0) and the target rotation.
attachment1.CFrame = CFrame.new(0,0,0) * currentTargetOrientationCFrameRotation
-- Update BodyPosition
-- Calculate forward movement relative to the submarine's lookVector
local forwardMovement = primaryPart.CFrame.lookVector * currentSpeed
-- Calculate strafe movement relative to the submarine's rightVector
local strafeMovement = primaryPart.CFrame.rightVector * currentStrafeSpeed
-- Combine movements
local totalHorizontalMovement = forwardMovement + strafeMovement
bodyPosition.Position = Vector3.new(primaryPart.Position.X, currentTargetYPosition, primaryPart.Position.Z) + totalHorizontalMovement
else
-- If no one is controlling, smoothly decelerate all movements
if currentSpeed > 0 then
currentSpeed = math.max(0, currentSpeed - (ACCELERATION_RATE * dt))
elseif currentSpeed < 0 then
currentSpeed = math.min(0, currentSpeed + (ACCELERATION_RATE * dt))
end
if currentStrafeSpeed > 0 then
currentStrafeSpeed = math.max(0, currentStrafeSpeed - (ACCELERATION_RATE * dt))
elseif currentStrafeSpeed < 0 then
currentStrafeSpeed = math.min(0, currentStrafeSpeed + (ACCELERATION_RATE * dt))
end
-- Apply any remaining speed
local remainingForwardMovement = primaryPart.CFrame.lookVector * currentSpeed * dt
local remainingStrafeMovement = primaryPart.CFrame.rightVector * currentStrafeSpeed * dt
bodyPosition.Position = primaryPart.Position + remainingForwardMovement + remainingStrafeMovement
-- AlignOrientation should maintain last known orientation when not controlled.
-- It will naturally hold the last rotation applied to attachment1.CFrame.
end
end)
-- Handle player disconnecting
Players.PlayerRemoving:Connect(function(player)
if controllingPlayer == player then
controllingPlayer = nil
currentSpeed = 0
currentStrafeSpeed = 0
latestThrottleInput = 0
latestVerticalInput = 0
latestStrafeInput = 0
primaryPart:SetNetworkOwner(nil)
end
end)
Location:
Any Help Would Be Appreciated. I’ll Try to get back at you as soon as I can!

