Submarine Scripts Need Improvement

You can write your topic however you want, but you need to answer these questions:

  1. 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.

  2. 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.

  3. 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. :grinning_face:

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:
image

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!

For the camera direction could you possible use the current camera? (this is just a guess) For the movement mapping you might have to do a process of elimination and start from top to bottom making each import correct.

In terms of the movement, did you check the orientation of the primary part to ensure its facing the same direction as model?

how do I check the direction of the PrimaryPart?

nvm i got the primary part to face forward, so how do i get the sub to face where i’m looking?