TurretController - CFrame based Joint Instance mover

Hello there, it’s me again with a small resource that I see lacking on the forum with many,unsolved, or ones that have the general idea but incomplete scripting support questions.

Credits

@sleitnick, PID Controller from AeroGameFramework, works great :+1: but semi modified to accommodate Vector3 variables.

@ThanksRoBama, Great tutorial on PID Controllers which had excellent yet simple explanation along with great examples. Never knew it was possible to just work with P alone.

Summary

What is it? Basically, if you have a Motor6D rig you input the Motor6D joint into this module and it will create an object which will rotate the C0 of the joint until it looks at the desired position. Moreover, you can place it in a run service loop in order to interpolate it and have linear speed and such.

Why use CFrames over physics-based constraints? Well for one you don’t have to set up the constraint Instances within the model and that’s enough for me I guess. But mainly there is a possibility the constraint-based physics of moving the turret will affect the main chassis of a vehicle as I have tried with BallSocketConstraints and AlignOrientation although I may have been doing it wrong.

Showcase

Dummy test place provided with 2 joints:

Or you can be a little more complex with 3 joints with the example:

MechGIF

https://i.imgur.com/I9kjmi5.mp4

Now includes PID Controller! Great if you want a reliable physics like feel to your turrets without pesky stuff like collisions with the turret and the main body due to weird modeling collisions or angular momentum getting in the way.

Assets

And always a test place is provided in the Github below with the funny dummy with a gun, just download the rbxlx file.

How to use it?

You can mainly look at the test place file or here though I recommend experimentation always.

Initial Attachment setup:

Select the Motor6D you want to control:

Place a “TurretAttachment” into part 1 of the motor6d you want to control. This will control where your Turret will lookAt so place the attachment near the nozzle of the gun like such and also the orientation which the turret will look at as the model of the orientation of the parts will not exactly be (0,0,0) like in some meshes which this attachment solves by applying an offset rotation.


Use the right hand rule to figure out the lookVector the attachment is facing. Your turret will also face that way.

and If need be if you want to define the constraint axis place a “BaseAttachment” attachment in the part0 of the motor. The secondary axis will define the elevation angle or depression angle, and the primary axis will define the yaw left or the yaw right of the turret. Position of this attachment rn doesn’t matter. If no BaseAttachment is provided it’ll use the axis of the previous TurretAttachment or if there is no TurretAttachment then the axis of the part1.

Codewise:

Require the module

local TurretController = require(ReplicatedStorage:FindFirstChild("TurretController",true))

Set up the constraints according to the template, currently, it only has these 4 axially constraints, maybe I’ll add a ball socket like constraint later if needed.

local gunHeadConstraints = {
	["YawLeft"] = 89;
	["YawRight"] = 89;
	["ElevationAngle"] = 0;
	["DepressionAngle"] = 0;
}

Create the controller object with the joint of the motor6D

local GunHead = TurretController.new(ModelMotor6Ds["GunHead"],gunHeadConstraints)

Then in a runservice loop, use the LookAt function of the turret controller:

RunService.Heartbeat:Connect(function(step)
    local goalPos = Target.Position--some random part
	GunHead:LookAt(goalPos,step)
end)

Turret Settings

Currently, there is support for the turrets to use interpolate the movements with linear speed, some arbritrary lerp alpha value controlled by the bool settings of the object, by default it’s set the lerp with constant speed:

	self.LerpMotors = true
	self.ConstantSpeed = true
	--some arbritrary values feel free to change
	self.LerpAlpha = 1/4
	self.AngularSpeed = math.rad(180)

Now with PID variables:

self.Mass = 1 --makes it slower and feel bulkier
self.CurrentAngularVelocityClamp = nil --Clamps the angular velocity, number in radians per second
self.Restitution = 0.5 -- Value 0-1, 1 means elastic same amount of bounce, 0 means rigid no bounce

--[[
Constructs the PID controller with it's variables
maxAngularAcceleration is in radians per second squared.
]]
function TurretController:ConstructPIDController(maxAngularAcceleration,kP, kD, kI)
end

License

This project uses an MIT license, Feel free to use it but credit is very much appreciated.

Future Improvements

Bug fixes if I find any :stuck_out_tongue: I’ll be using this in my main game project so yeah I’ll see what additional features would be useful to add to the controller.

96 Likes

I have a question, the mech that you are controlling is from the same script?

good job I love it will be very useful :smile: :+1:

1 Like

Yes it is, well only the upper body turret portion is, the legs is from my other inverse kinematics module.

Edit: Sorry but it’s called mech not a robot just a pet peeve of mine :sweat_smile:.

1 Like

Hmm, even though the resource is called turret controller you can also use it for other stuff like making the character look at the mouse with constraints which I solved in this post though it may not be the best implementation. Also, no attachments are needed in this situation as the resource will automatically use part0 and part1 as an axis to lookAt.

So yeah currently I’m thinking if I should rename the resource so it covers a broader scope. You can’t really consider the character as a turret unless you go real abstract. IDK, so here is a poll with a few names I came up with I suck at naming :sob:. please reply to help out.

Rename resource?
  • TurretController (Fine as is)
  • JointLookAt
  • JointLookController
  • LookController
  • Other (please reply)

0 voters

4 Likes

You Know This is a Great little Feature. I should keep this in my notes to add for my game when Martian Mode comes out as a heat ray guide.

1 Like

Heyo, good news the bug is fixed :confetti_ball:

Bug quote

Videos:

Before:
After:

Yeah, it was really annoying to solve I spent hours guessing and checking solutions until I had the nerve to write down the issue and visualize what was actually happening but it’s solved now.

Whitebackground messy image+explanation

dios mios

Yeah, I had to detect the quadrant of the CFrame.lookAt starting from the jointPosition instead of the attachment position because at the attachment position the Z axis which determines the quadrants is very finicky during the scenario.

		--Detect quadrant of lookAt position
		local jointPosition = currentJointMotor6D.Part0.CFrame*originalC0Position
		local quadrantLookAtFromJointPosition = CFrame.lookAt(jointPosition,lookAtPosition,baseCFrame.UpVector)	
		local baseRelative = baseCFrame:ToObjectSpace(quadrantLookAtFromJointPosition)
		local _,y, _ = baseRelative:ToOrientation()
		constrainedY = math.abs(constrainedY)*math.sign(y)--use the quadrants of the lookAtFromJoint

Anyway, I’m currently hoping to give this controller more physics like oomph through the use of PID. Send help if possible in this scripting support post. But yeah hope to see your creations soon if you used this resource!

2 Likes

New update PID Controller thanks to @ThanksRoBama Thanks RoBama. Allows your turrets to behave physics like without using the physics engine for maximum reliability.

It even works for cases where the yaw or pitch constraint is zero:

1 Like

Hello!
It’s me again.
While using the module, how would one make it target the closest target if there are multiple ones?
I was thinking- creating a table in the workspace and putting all the targets there.
Then I would make the loop through the table and use magnitude to find the distance of each target compared to the origin. Then target each one based on proximity.

Do you think this is a good idea?

image
Here is something I cooked up,
It doesn’t seem to be working obviously.

The original intent was to loop through the workspace and target anything named Target within range.

I probably did something stupid or my method shouldn’t be used at all.

any advice?

Yeah the first issue is that you are not changing the goalPosition, even if it does detect the closest enemy correctly it’ll still point towards the Target part in the workspace and not v: part or kill : part.

But yeah your idea is on point though I believe you meant origin is the position of the turret and not the world origin at (0,0,0).

For a question like this, I suggest researching further on the topic, after some research I found that @Quasiduck has already written a function to find the closest part within a table of parts so it should work though you can organise it differently, perhaps using collection services instead to get table of all the parts labeled as the enemy.

Once you found the closest enemy/object/part you can then start using this turret controller to look at that part. I recommend doing this if you are using the turret controller with lerping or with a PID controller.

local closestEnemyPosition
local arbritraryDirection 
RunService.Heartbeat:Connect(function(step)
--Find the closest enemy.
closestEnemyPosition = findClosestEnemy() or Vector3.new(0,0,-1)-- You decide what to do if there are no enemy positions to look at and what position the turret will look at.
	GunHead:LookAt(closestEnemyPosition,step)
end)
1 Like

Is there a way to decipher how much elasticity is applied to the turrets? I’m trying to achieve a more smooth-tween transition from one orientation to another, without having it sway back and forth for a few seconds. Fantastic module though!

1 Like

I believe you are using the PID controller and for that unfortunately not really. People get paid to tune PID systems and guess those three numbers to make it go exactly the way you want. The best I think we can do for now is to search online for a PID tuner simulator and just guess those values.

Otherwise perhaps a future update will be to use a spring module like in this I made post and apply it to the turret system to do something cool, that way it’s easier to control the dampening as it’s just a value from 0-1.

Edit: also there is a rotation issue currently if you set yaw left and yaw right to more than 90 degrees where it’ll just swoosh past the supposed constraint deadzone region because of how CFrame lerping works taking the shortest path. I’ll see if I can fix it when I get the time to do.

1 Like

Yup, I’ll mess with these values until I find a smoother transition.

Also yeah, I think I ran into that rotation issue a few times; luckily it wasn’t too big of an issue. I also noticed another bug where the turrets randomly started to randomly wiggle and rotate around. I wonder if this is just a flaw in the way I set up the turrets though.

1 Like

Nah not really, I intentionally left this bug here in order to see if anyone would notice it :wink:. The issue was the constraint quadrant checking which caused it to teleport around. You can get the latest version from the tool box or the GitHub repository above.

For the PID Tuning a simple website should be this one with a car example written in javascript. Play around with the values and see if they fit what you want them to, pretty fun.

2 Likes

Thanks so much, this site helped a lot! Found a good setting to use for the PID. :slight_smile:

For clarification, is that “bug” solved in the latest versions or do we have to fix it ourselves? Thanks again for all the help, I really appreciate it!

Yeah to further clarify the teleporting turret should be fixed in the latest version, you can view what changed in the GitHub.

The only remaining bug is this one which I don’t know the fix to currently which kinda sucks because you can’t get a constraint range of > 180°

1 Like

For some reason my turret is only using the LerpAlpha value for the speed and ignores AngularSpeed. Am I doing something wrong with my turret or is it meant to act how it is?

Turret: CANNON.rbxm (107.1 KB)

1 Like

Wow, I have spent hours looking at this.

The issue is that the orientation of the base is (90,0,0) which messed up the EulerAngle clamping math somehow and forced the current look vector and current goal vector to be the same always.

image

This subsequently, messed up a lot of things mainly the CFrame lerping alpha value which made it equal to 1 so it always instantly looks at the target.

No idea what the fix is, but thanks for bringing it up.

1 Like

I re rigged it to only use 3 pieces like the example dummy and it works fine now. I’m guessing I rigged it wrong and it broke it or something

Hello, I am having some trouble making this work on a tool:

Code I am using:

local waistConstraints = {
	["YawLeft"] = 360;
	["YawRight"] = 360;
	["ElevationAngle"] = 360;
	["DepressionAngle"] = 360;
}

local TurretController = require(script.TurretController, true)

local waistController = TurretController.new(waist, waistConstraints)
waistController:LookAt(mouse.Hit.Position)