How to make tank turret rotate with mouse

yeah, now I see the problem. You have ManualWelds on every part, (don’t use them, they are deprecated, use Weld/Motor6D instead), and you also add Motor6Ds to every part in your script, but they can’t get Active because of the initial welds.
Here’s what you need to do

  1. remove all the manual welds from the tank model (you can select them fast by typing in explorer c:weld (searches by class name)) (make sure all parts are anchored so they don’t fall on start)
  2. think of Motor6Ds or Welds as joints that connect 2 parts together (the difference is Motor6D can be used in animations). When you add them, you should only modify the CFrame of these connected parts through the joints, otherwise it will break.

So anyways, if you want to rotate your turret and all its child parts, you need to

  1. connect all turret’s children with motor6D/weld to 1 main turret part that will be rotated (in your case that would be the “PrimPart”

this is how your code in the “Framework” script should look like:

 -- Welds everything to main part
 for _, BasePart in pairs(script.Parent.Parent.BodyKit:GetDescendants()) do
 	if BasePart:IsA("BasePart") and BasePart.Name ~= "InvisParts" then
 		BasePart.CanCollide = false
 		BasePart.Massless = true
 		local Weld = Instance.new("Motor6D")
 		Weld.Parent = BasePart
 		if BasePart.Parent.Name == "Turret" and BasePart.Name ~= "PrimPart" then
 			Weld.Part0 = BasePart.Parent.PrimPart
 		else
 			Weld.Part0 = script.Parent.Parent.MainPart
 		end
 		Weld.Part1 = BasePart
 		Weld.C0 = Weld.Part0.CFrame:Inverse() * BasePart.CFrame
 		BasePart.Anchored = false
 	end
 end
  1. connect the PrimPart to the main tank part. This is the joint that you’re going to modify. Your mistake was that you modified the part’s CFrame directly. Instead you should modify the joint’s C0 property. Here’s how your code in “LocalScript” should look like
tool.Equipped:Connect(function()
	Connection = game:GetService("RunService").RenderStepped:Connect(function(dt)
 		TurretUnion.Motor6D.C0 = CFrame.new(TurretUnion.Motor6D.C0.Position, mouse.Hit.Position)
	end)
 end)

the only remaining thing would be to make the mouse position be relative instead of absolute, because the C0 is a relative CFrame of your part. You can check out this thread for it: Making a motor6d point in direction of mouse

2 Likes

Hello. I’ve tried doing this as best as i can and maybe i did something wrong but it doesnt seem to work. Whenever i test play the tracks fall apart and the turret also falls apart even though I anchored it.

Here is the updated version of th rbx file.
tankhelp.rbxl (674.5 KB)

Ok i got you, here’s the project that is working for me:
tankhelp_fix.rbxl (673.2 KB)

Here I removed the manualWelds only from BodyKit parts, because your tank driving script was dependent on the other welds, so I kept them. Also there was an issue with unanchoring parts before adding the motor6Ds which was causing them to fall down, so I moved it to the end

--Welds everything to main part
for _, BasePart in pairs(script.Parent.Parent.BodyKit:GetDescendants()) do
	if BasePart:IsA("BasePart") and BasePart.Name ~= "InvisParts" then
		BasePart.CanCollide = false
		BasePart.Massless = true
		local Weld = Instance.new("Motor6D")
		Weld.Parent = BasePart
		if BasePart.Parent.Name == "Turret" and BasePart.Name ~= "PrimPart" then
			Weld.Part0 = BasePart.Parent.PrimPart
		else
			Weld.Part0 = script.Parent.Parent.MainPart
		end
		Weld.Part1 = BasePart
		Weld.C0 = Weld.Part0.CFrame:Inverse() * BasePart.CFrame
		BasePart.Anchored = false
	end
end

If you’re not planning to use animations on them, you can replace the motor6Ds with weld.

Hello. This is awesome. Thank you very much :smile:

Is there a way to disable rotation up and down so it only rotates like an actual tank would? Also the turret doesn’t seem to point to the mouse cursor.

Sorry if im asking for a lot :sweat_smile:

Hello. I attempted to make it so the turret only moves side to side because rn its moving up and down which isnt realistic but I couldn’t quite figure out how to do that.

I’ve also moved the turret rotation script into the serverscript so it rotates on all clients.

And also is it possible to have the turret actually aim to the position of the cursor? cuz its about 90 degrees to the left side when i point forward.

here is the updated file:
tankhelp_fix.rbxl (673.3 KB)

1 Like

yes, as I said, you need to make the CFrame of mouse relative to the joint C0, here’s a link on one way to do it: Making a motor6d point in direction of mouse - #14 by dthecoolest

and if you want it to only move on X axis, you should just change the Y position to be the same as the turret’s instead of the mouse. Here’s how your code should be:

local body = script.Tank.Value.BodyKit
local tool = script.Parent
local event = game.ReplicatedStorage.TankTurretRotation
local TurretUnion = script.Tank.Value.BodyKit.Turret.PrimaryPart

local function worldCFrameToC0ObjectSpace(motor6DJoint, worldCFrame)
	local part1CF = motor6DJoint.Part1.CFrame
	local c1Store = motor6DJoint.C1
	local c0Store = motor6DJoint.C0
	local relativeToPart1 =c0Store*c1Store:Inverse()*part1CF:Inverse()*worldCFrame*c1Store
	relativeToPart1 -= relativeToPart1.Position
	local goalC0CFrame = relativeToPart1+c0Store.Position--New orientation but keep old C0 joint position
	return goalC0CFrame
end

event.OnServerEvent:Connect(function(plr, mousePosition)
	local lookPos = Vector3.new(mousePosition.X, TurretUnion.CFrame.Position.Y, mousePosition.Z)
	local goalCF = CFrame.lookAt(TurretUnion.CFrame.Position, lookPos)
	TurretUnion.Motor6D.C0 = worldCFrameToC0ObjectSpace(TurretUnion.Motor6D, goalCF)
end)
1 Like

Oh my god this is awesome thank you so much :smile:. I’ve been trying to make this work for 2 weeks now and finally you’ve helped me. This is so helpful thank you very much.

Whilst we’re here is it also possible to make the barrel part of the turret go up down? And if you cant/dont want to then thats perfectly fine you’ve already helped so much. Thank you :smile:

ACtually scratch that. I dont need that. How do i make the tank actually work now. Like actually shoot a shell and explosion effect and all that?

Thanks for the help im so grateful :smile:

No problem :slight_smile: Barrel up and down can be done exactly the same way as the turret, the only difference would be to change the Y axis instead of X like in turret’s case. I’m sure you can figure it out.
For shooting, you can try google-ing or searching it on youtube, there are lots of videos & roblox forum topics explaining how it is done.
If you’ll have questions with them, you can just make a new topic like you made this one

Okay thank you very much for the help :smile:

2 Likes

you’re welcome :slight_smile:

here’s a good video I found on turret rotation and shooting, you can check it out. It also uses hingeConstraints for rotation, so you can try that way for rotation as well

1 Like

Hello. Thanks for this video. It was quite helpful.

I have 1 more request that i can’t quite figure out on my own and i figured you’d know why.
So the turret being locked is great but whenever i go down hill for example it stays level and doesnt like follow the tank if that makes sense.

Here is a video example:
robloxapp-20250212-1718524.wmv (1.5 MB)

Script

-- Function to calculate C0 from the world CFrame
local function worldCFrameToC0ObjectSpace(motor6DJoint, worldCFrame)
	local part1CF = motor6DJoint.Part1.CFrame
	local c1Store = motor6DJoint.C1
	local c0Store = motor6DJoint.C0
	local relativeToPart1 = c0Store * c1Store:Inverse() * part1CF:Inverse() * worldCFrame * c1Store
	relativeToPart1 -= relativeToPart1.Position
	local goalC0CFrame = relativeToPart1 + c0Store.Position
	return goalC0CFrame
end

local rotationSpeed = 0.05

event.OnServerEvent:Connect(function(plr, mousePosition)
	local turretPos = TurretUnion.CFrame.Position
	local lookPos = Vector3.new(mousePosition.X, turretPos.Y, mousePosition.Z)  -- Adjust Y to maintain current height

	local goalCF = CFrame.lookAt(turretPos, lookPos, Vector3.new(0, 1, 0))  -- Ensure up vector is vertical

	local currentCF = TurretUnion.Motor6D.C0
	local newCF = worldCFrameToC0ObjectSpace(TurretUnion.Motor6D, goalCF)
	TurretUnion.Motor6D.C0 = currentCF:Lerp(newCF, rotationSpeed)
end)

1 Like

oh, you’re correct, my bad. You should actually change it to only affect Y component after converting it to relative cframe. Here’s the fix:

event.OnServerEvent:Connect(function(plr, mousePosition)	
	local goalCF = CFrame.lookAt(TurretUnion.CFrame.Position, mousePosition)
	goalCF = worldCFrameToC0ObjectSpace(TurretUnion.Motor6D, goalCF)
	local _, y, _ = goalCF:ToOrientation() -- Get the X, Y, Z rotation components
	goalCF = CFrame.new(goalCF.Position) * CFrame.Angles(0, y, 0)
	TurretUnion.Motor6D.C0 = TurretUnion.Motor6D.C0:Lerp(goalCF, rotationSpeed)
end)

Thank you this works :smile:
Now the tank is complete :slight_smile:

Hello. Found another small issue that i also dont know how to fix. Sorry for asking so much :sweat_smile:

I’ve made a rotationspeed so its like an actual tank but wheever the players FPS is set to 240 for example the speed is insanely fast and vice versa.

How do i fix this?

-- Function to calculate C0 from the world CFrame
local function worldCFrameToC0ObjectSpace(motor6DJoint, worldCFrame)
	local part1CF = motor6DJoint.Part1.CFrame
	local c1Store = motor6DJoint.C1
	local c0Store = motor6DJoint.C0
	local relativeToPart1 = c0Store * c1Store:Inverse() * part1CF:Inverse() * worldCFrame * c1Store
	relativeToPart1 = relativeToPart1 - relativeToPart1.Position
	local goalC0CFrame = relativeToPart1 + c0Store.Position
	return goalC0CFrame
end

local rotationSpeed = 0.02 -- Adjust the rotation speed as needed

event.OnServerEvent:Connect(function(plr, mousePosition)
	local turretPosition = TurretUnion.CFrame.Position
	local lookAtDirection = (mousePosition - turretPosition).unit -- Direction to look at

	-- Calculate the goal orientation
	local goalLookAtCF = CFrame.lookAt(Vector3.new(), Vector3.new(lookAtDirection.X, 0, lookAtDirection.Z))

	-- Convert to local CFrame relative to Motor6D
	local goalCF = worldCFrameToC0ObjectSpace(TurretUnion.Motor6D, goalLookAtCF)

	-- Extract Y rotation to maintain turret's current Y rotation
	local _, y, _ = goalCF:ToOrientation()
	goalCF = CFrame.new(goalCF.Position) * CFrame.Angles(0, y, 0)

	-- Gradually rotate towards the goal
	TurretUnion.Motor6D.C0 = TurretUnion.Motor6D.C0:Lerp(goalCF, rotationSpeed)
end)

that’s because in functions like Heartbeat/RenderStepped that run every frame, if you have more frames per second, then it will run more times. That’s why you need to account the deltaTime (time passed between this and previous frame) property, which is the default argument for such functions. You need to pass the deltaTime to your function, and multiply by it.

tool.Equipped:Connect(function()
	Connection = game:GetService("RunService").RenderStepped:Connect(function(dt)
		event:FireServer(dt, mouse.Hit.Position)
	end)
end)

and in your server script:

event.OnServerEvent:Connect(function(plr, dt, mousePosition)
	local turretPosition = TurretUnion.CFrame.Position
	local lookAtDirection = (mousePosition - turretPosition).unit -- Direction to look at

	-- Calculate the goal orientation
	local goalLookAtCF = CFrame.lookAt(Vector3.new(), Vector3.new(lookAtDirection.X, 0, lookAtDirection.Z))

	-- Convert to local CFrame relative to Motor6D
	local goalCF = worldCFrameToC0ObjectSpace(TurretUnion.Motor6D, goalLookAtCF)

	-- Extract Y rotation to maintain turret's current Y rotation
	local _, y, _ = goalCF:ToOrientation()
	goalCF = CFrame.new(goalCF.Position) * CFrame.Angles(0, y, 0)

	-- Gradually rotate towards the goal
	TurretUnion.Motor6D.C0 = TurretUnion.Motor6D.C0:Lerp(goalCF, rotationSpeed * dt)
end)

Once again you save my life. Dude genuinely thank you so much for helping me out these last couple days it means the world to me :smile:

Now idk if this is the kind of stuff you’re also really good at but for some odd reason it deducts 2 from the TotalAmmo intvalue when you reload. Im assuming its because the reload function is fired twice but i cant seem to figure out why. Can you help here? If no, no sweat you’ve already helped PLENTY. Thank you so much :smile:

local body = script.Tank.Value.BodyKit
local tool = script.Parent
local event = game.ReplicatedStorage.TankSystem.RemoteEvents.TankTurretRotation
local TurretUnion = script.Tank.Value.BodyKit.Turret.PrimaryPart
local fireShell = event.Parent.FireShell
local debris = game:GetService("Debris")

local config = script.Tank.Value.Seat.Configuration

local ammoLeft = config.AmmoLeft
local totalAmmo = config.TotalAmmo
local reloading = false

local turret = script.Tank.Value.BodyKit.Turret  -- Assuming turret is a model with PrimaryPart set
local startPart = script.Tank.Value.BodyKit.Turret.FirePart
local muzzlePart = script.Tank.Value.BodyKit.Turret.MuzzlePart
local barrelPart = script.Tank.Value.BodyKit.Turret.BarrelPart

local function Shoot(mousePosition)
	if not turret.PrimaryPart then
		warn("Turret does not have a PrimaryPart set!")
		return
	end

	local turretPosition = startPart.Position

	-- Get direction from turret to mouse position
	local direction = (mousePosition - turretPosition).Unit

	-- Raycast to detect impact point
	local rayParams = RaycastParams.new()
	rayParams.FilterDescendantsInstances = {body}  -- Ignore the tank itself
	rayParams.FilterType = Enum.RaycastFilterType.Exclude

	local rayResult = game.Workspace:Raycast(turretPosition, direction * 1000, rayParams)

	-- Fire sound
	local fireSound = Instance.new("Sound")
	fireSound.SoundId = "rbxassetid://8213064126"
	fireSound.Volume = 0.75
	fireSound.Parent = barrelPart
	fireSound:Play()
	if not fireSound.Playing then
		fireSound:Play()
	end

	-- Explosion effect
	local function createExplosionAtImpact(hitPosition)
		local explosion = Instance.new("Explosion")
		explosion.Position = hitPosition
		explosion.BlastRadius = 100
		explosion.BlastPressure = 0  -- Increased explosion impact
		explosion.ExplosionType = Enum.ExplosionType.NoCraters
		explosion.Parent = game.Workspace
	end

	-- If the ray hits something, create explosion at the correct position
	if rayResult then
		local hitPosition = rayResult.Position
		local distance = (hitPosition - turretPosition).Magnitude

		-- Adjust delay based on realistic shell speed
		local shellSpeed = 500  -- Adjust shell velocity
		local delayTime = distance / shellSpeed  

		task.delay(delayTime, function()
			createExplosionAtImpact(hitPosition)
		end)
	end
end

local function reload()
	-- Prevent reloading if already in the process or if there's ammo left
	if reloading or ammoLeft.Value >= 1 or totalAmmo.Value <= 0 then
		return
	end

	reloading = true
	local reloadSound = Instance.new("Sound")
	reloadSound.SoundId = "rbxassetid://2721754456"
	reloadSound.Volume = 0.75
	reloadSound.Parent = barrelPart
	if not reloadSound.Playing then
		reloadSound:Play()
	end

	wait(3)  -- Wait for reload time

	-- Only reload if totalAmmo is available
	if totalAmmo.Value > 0 then
		ammoLeft.Value = 1  -- Refill one ammo
		totalAmmo.Value = totalAmmo.Value - 1  -- Decrease totalAmmo by 1
	end

	reloading = false
end

event.Parent.FireShell.OnServerEvent:Connect(function(plr, mouseHitPosition)
	if ammoLeft.Value > 0 and not reloading then
		ammoLeft.Value = ammoLeft.Value - 1
		Shoot(mouseHitPosition)
	end
end)

ammoLeft:GetPropertyChangedSignal("Value"):Connect(function()
	if ammoLeft.Value <= 0 and totalAmmo.Value >= 1 and not reloading then
		reload()
	end
end)

-- Function to calculate C0 from the world CFrame
local function worldCFrameToC0ObjectSpace(motor6DJoint, worldCFrame)
	local part1CF = motor6DJoint.Part1.CFrame
	local c1Store = motor6DJoint.C1
	local c0Store = motor6DJoint.C0
	local relativeToPart1 = c0Store * c1Store:Inverse() * part1CF:Inverse() * worldCFrame * c1Store
	relativeToPart1 = relativeToPart1 - relativeToPart1.Position
	local goalC0CFrame = relativeToPart1 + c0Store.Position
	return goalC0CFrame
end

local rotationSpeed = 1.3 -- Adjust the rotation speed as needed

event.OnServerEvent:Connect(function(plr, dt, mousePosition)
	local turretPosition = TurretUnion.CFrame.Position
	local lookAtDirection = (mousePosition - turretPosition).unit -- Direction to look at

	-- Calculate the goal orientation
	local goalLookAtCF = CFrame.lookAt(Vector3.new(), Vector3.new(lookAtDirection.X, 0, lookAtDirection.Z))

	-- Convert to local CFrame relative to Motor6D
	local goalCF = worldCFrameToC0ObjectSpace(TurretUnion.Motor6D, goalLookAtCF)

	-- Extract Y rotation to maintain turret's current Y rotation
	local _, y, _ = goalCF:ToOrientation()
	goalCF = CFrame.new(goalCF.Position) * CFrame.Angles(0, y, 0)

	-- Gradually rotate towards the goal
	TurretUnion.Motor6D.C0 = TurretUnion.Motor6D.C0:Lerp(goalCF, rotationSpeed * dt)
end)

it’s much easier for me if you just share the project file, so I can print out and see what functions are being called and when.

Here is the project file.

And i dont wanna sound needy so if u dont wanna help any more thats perfectly fine but do you know how to make it shoot a ray out? I had this feature but i was shooting a part out but it was messing up with the collission system and the explosions were happenening prematurely so i switched to this.

But absolutely no worries if u dont wanna help out with that :smile:

tankhelp_workingbest.rbxl (686.7 KB)

the GetPropertyChangedSignal is being called twice for some reason. But you don’t really need it, you should never use these unless no other choice. You can just move your reload code after shooting and it will work.

event.Parent.FireShell.OnServerEvent:Connect(function(plr, mouseHitPosition)
	if ammoLeft.Value > 0 and not reloading then
		ammoLeft.Value = ammoLeft.Value - 1
		Shoot(mouseHitPosition)
		if ammoLeft.Value <= 0 and totalAmmo.Value >= 1 then
			reload()
		end
	end
end)

as for collision issues, you should print what part is your bullet colliding with, and disable their collisions if not needed, or add NoCollisionConstraint between your bullet and parts that you want to not collide. If they don’t collide but the touched function is being called, you can also disable “CanTouch” on those parts and it should work.