Vehicle with mounted turret can be driven or have the turret aimed, but not both

As part of the first game I’m working on, I have a pair of vehicles (one land vehicle and one air vehicle) that both have what are supposed to be turrets that are independently controlled by another player in a different seat than the driver’s seat. The code I’ve written so far works great if one person is driving the vehicle or manning the turret, but the moment two players try to do both at the same time in the same vehicle, the script fails to take into account the actions of whichever player sat down first. The vehicles are assembled entirely using constraints, like HingeConstraints, CylindricalConstraints, and SpringConstraints, and don’t use welds except for attaching the seats to the body.

I am aware this may be a network ownership issue and I have seen this solution, but my vehicle’s turrets move in four directions (up/down, left/right) and use the mouse cursor to aim. I don’t know what introducing that level of complexity to that solution would look like or what kind of new headaches it would result in. If another, simpler solution exists then I would much rather prefer to use that one.

I’ve attached the two relevant scripts below. I’m not interested in other possible problems that may exist with the code at the moment, only the issue mentioned above.


Client Script - Located in StarterCharacterScripts:

local TS = game:GetService("TweenService")
local UIS = game:GetService("UserInputService")
local RS = game:GetService("ReplicatedStorage")
local run = game:GetService("RunService")
local tweenInfo = TweenInfo.new(0.4)

local events = RS:WaitForChild("Events")
local boost = events:WaitForChild("Boost")
local BD = events:WaitForChild("BoostDepleted")
local drift = events:WaitForChild("Drift")
local fire = events:WaitForChild("FireTurret")

local character = script.Parent
local pilotForce = script:WaitForChild("VectorForce")
local gravity = Vector3.new(0, workspace.Gravity, 0)

local sitConnection = nil
local flightConnection = nil
local promptConnection = nil
local inputStart = nil
local inputEnd = nil
local speedChange = nil
local seat = nil
local forceValue = nil
local boosting = false
local drifting = false
local firing = false
local vectorForce = nil
local alignOrientation = nil

local force = 1
local drag = 0.5
local yAxis = 0

function getWheels(array)
	local result = {}
	for _, child in ipairs(array) do
		if child.ClassName == "Part" and child.Shape == Enum.PartType.Cylinder then
			table.insert(result, child)
		end
	end
	return result
end
function getPassengerSeats(array)
	local result = {}
	for _, child in ipairs(array) do
		if child.ClassName == "Seat" then
			table.insert(result, child)
		end
	end
	return result
end
function getAllSeats(array)
	local result = {}
	for _, child in ipairs(array) do 
		if (child.ClassName == "VehicleSeat" or child.ClassName == "Seat") and child:FindFirstChildWhichIsA("ProximityPrompt") then
			table.insert(result, child)
		end
	end
	return result
end

function steer(degrees, car)
	local turnL = 0
	local turnR = 0
	local fl = car:FindFirstChild("AttachmentFL", true)
	local fr = car:FindFirstChild("AttachmentFR", true)
	local b = car:FindFirstChild("AttachmentML", true) or car:FindFirstChild("AttachmentBL", true)
	if not fl or not fr or not b then return turnL, turnR end
	
	local radians = math.rad(90 - math.abs(degrees))
	local h = (fl.Position - b.Position).Magnitude
	local z = (fl.Position - fr.Position).Magnitude
	local x = math.tan(radians) * h
	local y = (x + z)
	local outerTurnAngle = 90 - math.deg(math.atan2(y, h))
	if (degrees > 0) then
		turnL = degrees
		turnR = outerTurnAngle
	else
		outerTurnAngle = -outerTurnAngle
		turnL = outerTurnAngle
		turnR = degrees
	end
	
	return turnL, turnR
end

BD.OnClientEvent:Connect(function()
	TS:Create(workspace.Camera, tweenInfo, {FieldOfView = 70}):Play()
end)

character.Humanoid.Seated:Connect(function(active, seatPart)
	local allSeats = getAllSeats(workspace:GetDescendants())
	if sitConnection ~= nil then
		if promptConnection ~= nil then
			promptConnection:Disconnect()
			promptConnection = nil
		end
		if flightConnection ~= nil then
			vectorForce.Enabled = false
			pilotForce.Enabled = false
			alignOrientation.Enabled = false
			flightConnection:Disconnect()
			flightConnection = nil
			vectorForce = nil
			alignOrientation = nil
		end
		if inputStart ~= nil then
			inputStart:Disconnect()
			inputStart = nil
		end
		if inputEnd ~= nil then
			inputEnd:Disconnect()
			inputEnd = nil
		end
		if speedChange ~= nil then
			speedChange:Disconnect()
			speedChange = nil
		end
		sitConnection:Disconnect()
		sitConnection = nil
		forceValue = nil
		firing = false
	end

	if seatPart == nil then
		for _, chair in pairs(allSeats) do
			if chair.Parent.PrimaryPart.CFrame.UpVector.Y < 0 then continue end
			chair.ProximityPrompt.Enabled = true
		end
		
		if seat == nil then return end

		local attachment = nil
		local height = -math.huge
		for i, child in ipairs(seat:GetChildren()) do
			if child.ClassName ~= "Attachment" then continue end
			if child.WorldPosition.Y <= height then continue end
			height = child.WorldPosition.Y
			attachment = child
		end
		character.PrimaryPart.CFrame = attachment.WorldCFrame
		seat = nil
	else
		promptConnection = run.RenderStepped:Connect(function()
			for _, chair in pairs(allSeats) do
				chair.ProximityPrompt.Enabled = false
			end
		end)
		
		seat = seatPart
		
		if seatPart.ClassName == "VehicleSeat" then
			if seatPart.Parent.Name == "ATV" or seatPart.Parent.Name == "Wheeler" then
				local maxAngularVelocity = seatPart.MaxSpeed / (seatPart.Parent.WheelBR.Size.Y / 2)

				inputStart = UIS.InputBegan:Connect(function(input, GPE)
					if GPE then return end
					if input.KeyCode == Enum.KeyCode.LeftShift then
						boosting = true
						boost:FireServer(boosting, seatPart.Parent)
						TS:Create(workspace.Camera, tweenInfo, {FieldOfView = 100}):Play()
					elseif input.KeyCode == Enum.KeyCode.Q then
						drifting = true
						drift:FireServer(drifting)
					end
				end)
				
				inputEnd = UIS.InputEnded:Connect(function(input, GPE)
					if GPE then return end
					if input.KeyCode == Enum.KeyCode.LeftShift then
						boosting = false
						boost:FireServer(boosting, seatPart.Parent)
						TS:Create(workspace.Camera, tweenInfo, {FieldOfView = 70}):Play()
					elseif input.KeyCode == Enum.KeyCode.Q then
						drifting = false
						drift:FireServer(drifting)
					end
				end)

				speedChange = seatPart:GetPropertyChangedSignal("MaxSpeed"):Connect(function()
					maxAngularVelocity = seatPart.MaxSpeed / (seatPart.Parent.WheelBR.Size.Y / 2)
					seatPart.ThrottleFloat = 0
				end)
				
				local attachmentFL = seatPart.Parent.PrimaryPart:FindFirstChild("AttachmentFL")
				local attachmentFR = seatPart.Parent.PrimaryPart:FindFirstChild("AttachmentFR")
				local wheels = getWheels(seatPart.Parent:GetChildren())
				
				sitConnection = seatPart.Changed:Connect(function(property)
					if property == "SteerFloat" then
						local angleL, angleR = steer(-seatPart.SteerFloat * seatPart.TurnSpeed, seatPart.Parent)
						local orientationL, orientationR = Vector3.new(0, angleL, 90), Vector3.new(0, angleR, 90)
						TS:Create(attachmentFL, tweenInfo, {Orientation = orientationL}):Play()
						TS:Create(attachmentFR, tweenInfo, {Orientation = orientationR}):Play()
					elseif property == "ThrottleFloat" then
						local torque = math.abs(seatPart.ThrottleFloat * seatPart.Torque)
						if torque == 0 then torque = 2000 end
						local angularVelocity = math.sign(seatPart.ThrottleFloat) * maxAngularVelocity
						for _, wheel in ipairs(wheels) do
							wheel.CylindricalConstraint.MotorMaxTorque = torque
							wheel.CylindricalConstraint.AngularVelocity = angularVelocity
						end
					end
				end)
			elseif seatPart.Parent.Name == "Flyer" or seatPart.Parent.Name == "Carrier" then
				local vehicle = seatPart.Parent
				local primaryPart = vehicle.PrimaryPart
				vectorForce = primaryPart.VectorForce
				pilotForce.Attachment0 = primaryPart:FindFirstChild("CenterOfMass")
				alignOrientation = primaryPart.AlignOrientation
				local maxSpeed = seatPart.MaxSpeed

				local seats = getPassengerSeats(vehicle:GetChildren())

				local forceValue = force * maxSpeed
				
				speedChange = seatPart:GetPropertyChangedSignal("MaxSpeed"):Connect(function()
					maxSpeed = seatPart.MaxSpeed
					forceValue = force * maxSpeed
					seatPart.ThrottleFloat = 0
				end)

				sitConnection = UIS.InputBegan:Connect(function(input, GPE)
					if GPE then return end
					if input.KeyCode == Enum.KeyCode.F then
						if flightConnection == nil then

							vectorForce.Enabled = true
							pilotForce.Enabled = true
							alignOrientation.CFrame = primaryPart.CFrame
							alignOrientation.Enabled = true

							inputStart = UIS.InputBegan:Connect(function(input, GPE)
								if GPE then return end
								if input.KeyCode == Enum.KeyCode.E then
									yAxis = 1
								elseif input.KeyCode == Enum.KeyCode.Q then
									yAxis = -1
								elseif input.KeyCode == Enum.KeyCode.LeftShift then
									boosting = true
									boost:FireServer(boosting, seatPart.Parent)
									TS:Create(workspace.Camera, tweenInfo, {FieldOfView = 100}):Play()
								end
							end)
							inputEnd = UIS.InputEnded:Connect(function(input, GPE)
								if GPE then return end
								if input.KeyCode == Enum.KeyCode.E or input.KeyCode == Enum.KeyCode.Q then
									yAxis = 0
								elseif input.KeyCode == Enum.KeyCode.LeftShift then
									boosting = false
									boost:FireServer(boosting, seatPart.Parent)
									TS:Create(workspace.Camera, tweenInfo, {FieldOfView = 70}):Play()
								end
							end)

							flightConnection = run.Heartbeat:Connect(function(deltaTime)
								local mousePosition = UIS:GetMouseLocation()
								local mouseRay = workspace.Camera:ViewportPointToRay(mousePosition.X, mousePosition.Y)
								local raycastParams = RaycastParams.new()
								raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
								local passengers = {}
								for _, passenger in pairs(seats) do
									if passenger.Occupant then
										table.insert(passengers, passenger.Occupant.Parent)
									end
								end
								raycastParams.FilterDescendantsInstances = {vehicle, character, passengers}
								local raycastResult = workspace:Raycast(mouseRay.Origin, mouseRay.Direction * 1000, raycastParams)
								if raycastResult then
									alignOrientation.CFrame = CFrame.lookAt(primaryPart.Position, raycastResult.Position)
								else
									alignOrientation.CFrame = CFrame.new(Vector3.new(0, 0, 0), mouseRay.Direction)
								end
								vectorForce.Force = (gravity - gravity / 25) * primaryPart.AssemblyMass
								local moveVector = Vector3.new(seatPart.SteerFloat, yAxis, -seatPart.ThrottleFloat)
								if moveVector.Magnitude > 0 then moveVector = moveVector.Unit end
								pilotForce.Force = moveVector * forceValue * primaryPart.AssemblyMass
								if primaryPart.AssemblyLinearVelocity.Magnitude > 0 then
									local dragVector = -primaryPart.AssemblyLinearVelocity.Unit * primaryPart.AssemblyLinearVelocity.Magnitude ^ 1.2
									vectorForce.Force += dragVector * drag * primaryPart.AssemblyMass
								end
							end)
						else
							vectorForce.Enabled = false
							pilotForce.Enabled = false
							alignOrientation.Enabled = false
							flightConnection:Disconnect()
							flightConnection = nil
						end
					end
				end)
			end
		elseif seatPart.Name == "TurretSeat" then
			local vehicle = seatPart.Parent
			alignOrientation = vehicle.TurretBarrel.AlignOrientation
			alignOrientation.Enabled = true
			
			local seats = getPassengerSeats(vehicle:GetChildren())

			inputStart = UIS.InputBegan:Connect(function(input, GPE)
				if GPE then return end
				if input.UserInputType == Enum.UserInputType.MouseButton1 then
					firing = true
					while firing do
						task.wait()
						fire:FireServer(vehicle)
					end
				end
			end)

			inputEnd = UIS.InputEnded:Connect(function(input, GPE)
				if GPE then return end
				if input.UserInputType == Enum.UserInputType.MouseButton1 then
					firing = false
				end
			end)

			sitConnection = run.RenderStepped:Connect(function()
				local mousePosition = UIS:GetMouseLocation()
				local mouseRay = workspace.Camera:ViewportPointToRay(mousePosition.X, mousePosition.Y)
				local raycastParams = RaycastParams.new()
				raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
				local passengers = {}
				for _, passenger in pairs(seats) do
					if passenger.Occupant then
						table.insert(passengers, passenger.Occupant.Parent)
					end
				end
				raycastParams.FilterDescendantsInstances = {vehicle, character, passengers}
				local raycastResult = workspace:Raycast(mouseRay.Origin, mouseRay.Direction * 1000, raycastParams)
				if raycastResult then
					alignOrientation.CFrame = CFrame.lookAt(alignOrientation.Parent.Position, raycastResult.Position)
				else
					alignOrientation.CFrame = CFrame.new(Vector3.new(0, 0, 0), mouseRay.Direction)
				end
			end)
		end
	end
end)

Server Script - Located in ServerScriptService:

local CS = game:GetService("CollectionService")
local RS = game:GetService("ReplicatedStorage")
local SS = game:GetService("ServerStorage")
local players = game:GetService("Players")
local debris = game:GetService("Debris")
local run = game:GetService("RunService")

local events = RS:WaitForChild("Events")
local boost = events:WaitForChild("Boost")
local BD = events:WaitForChild("BoostDepleted")
local drift = events:WaitForChild("Drift")
local fire = events:WaitForChild("FireTurret")

local vehicles = SS:WaitForChild("Vehicles")
local config = vehicles:WaitForChild("Config")

local projectiles = SS:WaitForChild("Projectiles")
local turretBullet = projectiles:WaitForChild("Turret")

local landVehicles = CS:GetTagged("Land Vehicle")
local airVehicles = CS:GetTagged("Air Vehicle")
local ATVs = CS:GetTagged("ATV")
local wheelers = CS:GetTagged("Wheeler")
local flyers = CS:GetTagged("Flyer")
local carriers = CS:GetTagged("Carrier")
local seats = CS:GetTagged("Vehicle Seat")

local healths = {}
local boostConnections = {}
local cooldowns = {}
local lastFired = {}

function getBodyParts(array)
	local result = {}
	for _, child in ipairs(array) do
		if child.ClassName == "Part" or child.ClassName == "MeshPart" or child.ClassName == "UnionOperation" then
			table.insert(result, child)
		end
	end
	return result
end
function getDriftWheels(array)
	local result = {}
	for _, child in ipairs(array) do
		if child.ClassName == "Part" then
			if child.Shape == Enum.PartType.Cylinder and not (child.Name == "WheelFL" or child.Name == "WheelFR") then
				table.insert(result, child)	
			end
		end
	end
	return result
end
function getSeats(array)
	local result = {}
	for _, child in ipairs(array) do
		if child.ClassName == "VehicleSeat" or child.ClassName == "Seat" then
			table.insert(result, child)
		end
	end
	return result
end
function getOccupants(array)
	local result = {}
	for _, child in ipairs(array) do
		table.insert(result, child.Occupant)
	end
	return result
end

function onLandVehicleAdded(vehicle)
	if vehicle.ClassName == "Model" then
		local body = vehicle.PrimaryPart
		local vehicleConfig = config:FindFirstChild(vehicle.Name)
		local maxHealth = vehicleConfig.Health
		local maxSpeed = vehicleConfig.Speed
		healths[vehicle] = maxHealth.Value

		local parts = getBodyParts(vehicle:GetChildren())
		local driftWheels = getDriftWheels(vehicle:GetChildren())
		local seats = getSeats(vehicle:GetChildren())
		local occupants = getOccupants(seats)

		local MAX_HEALTH = maxHealth.Value
		local MAX_SPEED = maxSpeed.Value
		local FRICTION = vehicle.WheelBR.CustomPhysicalProperties.Friction
		cooldowns[vehicle] = vehicleConfig.Duration.Value

		local healthDebounce = false
		local damageDebounce = false
		local connection = nil
		local timeframe = nil

		for _, part in pairs(parts) do
			part.Touched:Connect(function(otherPart)
				if otherPart.Name == "Projectile" and healthDebounce == false then
					healthDebounce = true
					healths[vehicle] -= 10
					if healths[vehicle] <= 0 then
						for _, attachment in pairs(vehicle:GetDescendants()) do
							if attachment.ClassName == "Attachment" or attachment.ClassName == "SpringConstraint" or attachment.ClassName == "CylindricalConstraint" or attachment.ClassName == "WeldConstraint" then
								attachment:Destroy()
							end
						end

						local boom = Instance.new("Explosion", body)
						boom.BlastRadius = body.Size.Z
						boom.ExplosionType = Enum.ExplosionType.NoCraters
						boom.DestroyJointRadiusPercent = 0
						boom.Position = body.Position
						boom.Hit:Connect(function(obj, dist)
							local humanoid = obj.Parent:FindFirstChild("Humanoid")
							if humanoid then
								humanoid.Health -= 150 / (dist / 2)
							end
						end)

						debris:AddItem(vehicle, 5)
						healths[vehicle] = nil
						lastFired[vehicle] = nil
						cooldowns[vehicle] = nil
						
					elseif healths[vehicle] <= MAX_HEALTH / 2 then
						body.ParticleAttachment.Smoke.Enabled = true
						if healths[vehicle] <= MAX_HEALTH / 4 then
							body.ParticleAttachment.Fire.Enabled = true
						end
					end
					task.wait()
					healthDebounce = false
				elseif damageDebounce == false and otherPart.Name ~= "Projectile" then
					damageDebounce = true
					local humanoid = otherPart.Parent:FindFirstChildWhichIsA("Humanoid")
					if humanoid then
						if humanoid.Sit == false and part.Velocity.Magnitude > 20 and not table.find(occupants, humanoid) then
							humanoid.Health -= part.Velocity.Magnitude
						end
					end
					task.wait()
					damageDebounce = false
				end
			end)
		end

		for _, seat in pairs(seats) do
			seat:GetPropertyChangedSignal("Occupant"):Connect(function()
				if seat.Occupant == nil then
					task.wait(1)
				end
				occupants = getOccupants(seats)

				if connection ~= nil then connection:Disconnect() connection = nil end
				if seat.Occupant ~= nil then
					connection = run.Heartbeat:Connect(function()
						if body.CFrame.UpVector.Y < 0 then
							for _, seat in pairs(seats) do
								seat.ProximityPrompt.Enabled = false
							end
							if timeframe and math.abs(timeframe - tick()) > 1 then
								for _, occupant in pairs(occupants) do
									occupant.Jump = true
								end
								timeframe = nil
								body.ProximityPrompt.Enabled = true
							elseif not timeframe then
								timeframe = tick()
							end
						else
							timeframe = nil
							for _, seat in pairs(seats) do
								if seat.Occupant == nil then
									seat.ProximityPrompt.Enabled = true
								end
							end
						end
					end)
				end
			end)
		end

		body.ProximityPrompt.Triggered:Connect(function()
			local flip = body.AlignOrientation
			flip.Enabled = true
			task.wait(0.5)
			flip.Enabled = false
			task.wait(0.25)
			if body.CFrame.UpVector.Y > 0 then
				for _, seat in pairs(seats) do
					seat.ProximityPrompt.Enabled = true
				end
				body.ProximityPrompt.Enabled = false
			end
		end)

		drift.OnServerEvent:Connect(function(player, drifting)
			if table.find(occupants, player.Character.Humanoid) then
				if drifting == true then
					for _, wheel in pairs(driftWheels) do
						wheel.CustomPhysicalProperties = PhysicalProperties.new(5, 0, 0.2, 1, 1)
					end
				else
					for _, wheel in pairs(driftWheels) do
						wheel.CustomPhysicalProperties = PhysicalProperties.new(5, FRICTION, 0.2, 1, 1)
					end
				end
			end
		end)
	end
end

function onAirVehicleAdded(vehicle)
	if vehicle.ClassName == "Model" then
		local body = vehicle.PrimaryPart
		local vehicleConfig = config:FindFirstChild(vehicle.Name)
		local maxHealth = vehicleConfig.Health
		local maxSpeed = vehicleConfig.Speed
		healths[vehicle] = maxHealth.Value

		local parts = getBodyParts(vehicle:GetChildren())
		local seats = getSeats(vehicle:GetChildren())
		local occupants = getOccupants(seats)

		local MAX_HEALTH = maxHealth.Value
		local MAX_SPEED = maxSpeed.Value
		cooldowns[vehicle] = vehicleConfig.Duration.Value

		local healthDebounce = false
		local damageDebounce = false

		for _, part in pairs(parts) do
			part.Touched:Connect(function(otherPart)
				if otherPart.Name == "Projectile" and healthDebounce == false then
					healthDebounce = true
					healths[vehicle] -= 10
					if healths[vehicle] <= 0 then

						for _, attachment in pairs(vehicle:GetDescendants()) do
							if attachment.ClassName == "Attachment" or attachment.ClassName == "SpringConstraint" or attachment.ClassName == "CylindricalConstraint" or attachment.ClassName == "WeldConstraint" then
								attachment:Destroy()
							end
						end

						local boom = Instance.new("Explosion", body)
						boom.BlastRadius = body.Size.Z
						boom.ExplosionType = Enum.ExplosionType.NoCraters
						boom.DestroyJointRadiusPercent = 0
						boom.Position = body.Position
						boom.Hit:Connect(function(obj, dist)
							local humanoid = obj.Parent:FindFirstChild("Humanoid")
							if humanoid then
								humanoid.Health -= 150 / (dist / 2)
							end
						end)

						debris:AddItem(vehicle, 5)
						healths[vehicle] = nil
						lastFired[vehicle] = nil
						cooldowns[vehicle] = nil

					elseif healths[vehicle] <= MAX_HEALTH / 2 then
						body.ParticleAttachment.Smoke.Enabled = true
						if healths[vehicle] <= MAX_HEALTH / 4 then
							body.ParticleAttachment.Fire.Enabled = true
						end
					end
					task.wait()
					healthDebounce = false
				elseif damageDebounce == false and otherPart.Name ~= "Projectile" then
					damageDebounce = true
					local humanoid = otherPart.Parent:FindFirstChildWhichIsA("Humanoid")
					if humanoid then
						if humanoid.Sit == false and part.Velocity.Magnitude > 20 and not table.find(occupants, humanoid) then
							humanoid.Health -= part.Velocity.Magnitude
						end
					end
					task.wait()
					damageDebounce = false
				end
			end)
		end
		for _, seat in pairs(seats) do
			seat:GetPropertyChangedSignal("Occupant"):Connect(function()
				if seat.Occupant == nil then
					task.wait(1)
				end
				occupants = getOccupants(seats)
			end)
		end
	end
end

function onSeatAdded(seat)
	if seat.ClassName == "Seat" or seat.ClassName == "VehicleSeat" then
		local body = seat.Parent.PrimaryPart

		seat:GetPropertyChangedSignal("Occupant"):Connect(function()
			if seat.Occupant == nil and body.CFrame.UpVector.Y > 0 then
				seat.ProximityPrompt.Enabled = true
			else
				seat.ProximityPrompt.Enabled = false
			end
		end)

		seat.ProximityPrompt.Triggered:Connect(function(player)
			seat:Sit(player.Character.Humanoid)
		end)
	end
end

players.PlayerAdded:Connect(function(player)
	player.CharacterAdded:Connect(function(character)
		character.Humanoid.Seated:Connect(function(active, seatPart)
			if seatPart == nil then return end
			if seatPart.ClassName ~= "VehicleSeat" or seatPart.Name ~= "TurretSeat" then return end
			seatPart:SetNetworkOwner(player)
			print("Owner:", player.Name)
		end)
	end)
end)

boost.OnServerEvent:Connect(function(player, boosting, vehicle)
	if table.find(getOccupants(getSeats(vehicle:GetChildren())), player.Character.Humanoid) then
		if boostConnections[vehicle] ~= nil then
			boostConnections[vehicle]:Disconnect()
			boostConnections[vehicle] = nil
		end
		local vehicleConfig = config:FindFirstChild(vehicle.Name)
		if not vehicleConfig then return end
		if boosting == true then
			boostConnections[vehicle] = run.Heartbeat:Connect(function(dt)
				if cooldowns[vehicle] <= 0 then
					vehicle.Drive.MaxSpeed = vehicleConfig.Speed.Value
					cooldowns[vehicle] = 0
					BD:FireClient(player)
					boostConnections[vehicle]:Disconnect()
					boostConnections[vehicle] = nil
				else
					vehicle.Drive.MaxSpeed = vehicleConfig.Speed.Value + vehicleConfig.Boost.Value
					cooldowns[vehicle] -= dt
				end
			end)
		else
			vehicle.Drive.MaxSpeed = vehicleConfig.Speed.Value
			boostConnections[vehicle] = run.Heartbeat:Connect(function(dt)
				if cooldowns[vehicle] >= vehicleConfig.Duration.Value then
					cooldowns[vehicle] = vehicleConfig.Duration.Value
					boostConnections[vehicle]:Disconnect()
					boostConnections[vehicle] = nil
				else
					cooldowns[vehicle] += dt
				end
			end)
		end
	end
end)

fire.OnServerEvent:Connect(function(player, vehicle)
	lastFired[vehicle] = lastFired[vehicle] or 0
	local vehicleConfig = config:FindFirstChild(vehicle.Name)
	if not vehicleConfig then return end
	
	if tick() - (1 / vehicleConfig.FireRate.Value) >= lastFired[vehicle] then
		lastFired[vehicle] = tick()
		local bullet = turretBullet:Clone()
		bullet.CFrame = vehicle.TurretBarrel.FiringPort.WorldCFrame
		bullet.Parent = vehicle
		local speed = vehicleConfig.FireSpeed.Value
		local params = RaycastParams.new()
		local iterations = math.round(vehicleConfig.Range.Value / speed)
		params.FilterDescendantsInstances = {vehicle, getOccupants(getSeats(vehicle:GetChildren()))}
		local bulletConnection
		bulletConnection = run.Heartbeat:Connect(function(deltaTime)
			local position,nextPosition = bullet.Position, bullet.CFrame.LookVector * deltaTime * speed
			local result = workspace:Raycast(position, nextPosition, params)
			if result then	
				bulletConnection:Disconnect()
				bullet:Destroy()
				local human = result.Instance.Parent:FindFirstChildWhichIsA("Humanoid")
				if human and human.Health > 0 and not players:GetPlayerFromCharacter(human.Parent) then
					human:TakeDamage(math.random(vehicleConfig.Damage.Value / 2, vehicleConfig.Damage.Value * 3/2))
				end
			else
				bullet.Position = position + nextPosition
			end

			iterations -= 1
			if iterations < 0 then
				bulletConnection:Disconnect()
				bullet:Destroy()
			end
		end)
	end
end)

CS:GetInstanceAddedSignal("Land Vehicle"):Connect(onLandVehicleAdded)
CS:GetInstanceAddedSignal("Air Vehicle"):Connect(onAirVehicleAdded)
CS:GetInstanceAddedSignal("Vehicle Seat"):Connect(onSeatAdded)

for _, vehicle in ipairs(landVehicles) do onLandVehicleAdded(vehicle) end
for _, vehicle in ipairs(airVehicles) do onAirVehicleAdded(vehicle) end
for _, seat in ipairs(seats) do onSeatAdded(seat) end
2 Likes

This might work but I can’t test it ATM:

The idea is to just give network ownership to the driver. Both players will need network ownership to use physics, but that’s clearly impossible when the vehicle is one big assembly. So create an entirely new “fake” assembly for the gunner that consists of the parts that they need to control using physics, but anchor the root of the assembly (whatever part normally attaches to the rest of the vehicle) and pivot (CFrame) it to the correct location every RenderStepped. This shoooould look smooth because physics information that gets sent out of sync with the render loop shoooould be interpolated but I don’t know for sure. That’s the reason for not just sending the CFrame of the vehicle every frame, you can’t do stuff faster than 30hz on the server and it wouldn’t be in sync with the client even if you could send it at the same rate as their render loop. Make the “real” assembly invisible and nonsolid locally for the gunner, and have them control the “fake”, local assembly using their physics. Send information to the server about the orientation of all the joints to the client every RenderStepped, update these on the server so it replicates to all players including the driver, and I think that should work. Just set this up when a gunner enters their seat and clean it up when they leave.

This is a bit complicated so if I were you I’d do some preliminary tests to see if this approach even works smoothly in the first place. Like, instead of a whole vehicle just have the driver control a box on a LinearConstraint and the gunner control a HingeConstraint that moves with the box.

1 Like

I’m afraid I’d need to see an example, sorry. Adding anchored parts to something that’s supposed to be moving freely around the game world using the physics engine sounds like a recipe for disaster. I know for a heartbeat loop, there are differences in timing and performance between the physics engine and manually-edited CFrame.

2 Likes

Yes. Or in other words, it feels like being a mad scientist. Try it and see if it works. I don’t see why it shouldn’t.

I can’t actually test it because I don’t have access to Studio. Here’s what I can come up with without testing anything :I


--[[
"MultiAssemblyManager".
Server script.
A multi-assembly is an assembly of physics-controlled Parts that need to be split into two or more subassemblies so that different physics-contexts can control each subassembly separately. In this case, for the purpose of having different clients control each subassembly locally, which is impossible otherwise.
]]
local MultiAssemblyManager = {}

function MultiAssemblyManager.SetupMultiAssembly(multiAssembly)

end

function MultiAssemblyManager.SetAssemblyOwner(assembly, player)

end

return MultiAssemblyManager

--[[
"BattleWagonManager".
Server script.
A battle wagon is controlled by up to two players simultaneously, a driver and a gunner. The gun is a subassembly of the whole vehicle. 
]]
local MultiAssemblyManager = require("the module")

local vehicleInfo = {}

function onDriversSeatEntered(vehicle, player)

end

function onDriversSeatExited(vehicle)

end

function onGunnersSeatEntered(vehicle, player)
    local gunnerControlScript = ServerStorage.BattleWagonAssets.BattleWagonGunnerControlScript:Clone()
    gunnerControlScript.VehicleReference.Value = vehicle
    gunnerControlScript.Parent = player.PlayerScripts --Not sure this is even possible from the server, otherwise signal to the player they must clone the control script.
    vehicleInfo[vehicle].Gunner = player
    vehicleInfo[vehicle].GunnerConnections = vehicle.GunnerRemoteEvent.OnServerEvent:Connect(function(eventPlayer, ...)
        if eventPlayer ~= player then return end
        handleGunnerRemoteEvent(vehicle, eventPlayer, ...)
    end)
end

function onGunnersSeatExited(vehicle)

end

--Honestly, sending which vehicle might be unnecessary when the RemoteEvent is parented to the vehicle in question. Oh well.
function handleGunnerRemoteEvent(vehicle, player, eventType, ...)
    local eventArgs = {...}
    if eventType == "GunnerControllerScriptReady" then
        vehicle.GunnerRemoteEvent:FireClient(player, vehicle, "AckGunnerControllerScriptReady") --"Ack" for Acknowledge, not sure if even needed.
    elseif eventType == "UpdateGunJoints" then
        local gunJointInfos = unpack(eventArgs)
        for gunJoint, gunJointInfo in ipairs(gunJointInfos) do
            if gunJoint:IsA("HingeConstraint") then
                if gunJoint.MotorType == Enum.MotorType.Servo then --Ehh something like that anyway
                    --Actually, it might be better to replace every physics constraint with a Motor6D and let the client *force* the CFrame based on their physics sim, but this gets the idea across.
                    gunJoint.TargetAngle = gunJointInfo.TargetAngle
                end
            end
        end
    end
end

--[[
"BattleWagonGunnerControlScript"
A LocalScript in ServerStorage, to be cloned into a Player's PlayerScripts.
]]

local vehicleReference = script:WaitForChild("VehicleReference")
local vehicle = vehicleReference.Value
local gunnerRemoteEvent = vehicle.GunnerRemoteEvent

local realGunAssembly = vehicle.GunAssembly
for _, p in ipairs(realGunAssembly:GetDescendants()) do
    if not p:IsA("BasePart") then continue end
    p.LocalTransparencyModifier = 1 --set to 0 when leaving the vehicle
end

local fakeGunAssembly = realGunAssembly:Clone()
fakeGunAssembly.VehicleWeld.Active = false
fakeGunAssembly.PrimaryPart.Anchored = true
fakeGunAssembly.Parent = vehicle

RunService.RenderStepped:Connect(function(dt)
    fakeGunAssembly:PivotTo(vehicle.GunAssemblyRootAttachment.WorldCFrame)

    fakeGunAssembly.PitchServo.TargetAngle += 90 * dt --lol just for testing, replace with proper controls
    gunnerRemoteEvent:FireServer("UpdateGunJoints", collectGunJoinInfos())
end)

gunnerRemoteEvent.OnClientEvent:Connect(function(eventVehicle, ...)
    --Probably react to the server telling the player that they're no longer the gunner, so clean up everything
end)

gunnerRemoteEvent:FireServer(vehicle, "GunnerControllerScriptReady")