- Home /
Determining thruster activation on 3D spaceship
Hello. I'm currently working on a small 3D space-station building game prototype, and I've hit a bit of a snag. Basically, I'm not that good with 3D vector math and stuff, so I need some help.
Right now, I can do some very simple stuff to determine which thrusters to fire when trying to translate or rotate in a single direction, with a single rigidbody. However, I'm not accounting for distance from the COM, which would result in undesired rotation when translating or translation when rotating if the thrusters are not placed symmetrically. This is part one of my problem -- what kind of math do I need, in 3D, to solve for the amount of thrust the thrusters have to put out not to cause undesired movement?
The second part involves combining two or more rigidbodies together to form a larger 'vessel'. As it stands, I can get the COM of the entire structure, but calculating which thrusters to fire, even if they're placed symmetrically, becomes a nightmare if one of the pieces are rotated before being docked.
Imagine two cylinders, facing along the Z axis. Individually, I can figure out which thrusters to fire to produce the desired effect. Indeed, when they are pushed together without any rotation, it mostly works as well.
Now, one of the cylinders is rotated say 90° along the Z axis. Both are still facing the same direction, except one is rotated. Now, everything goes wonky. What's the best way to solve this? Currently I have a 'Vessel' construct, which is basically a collection of Rigidbodies as well as the COM of the entire structure.
I forgot to mention, I'm using physics (AddForceAtPosition) to do all of this, not transformations.
If it helps, I've attached my current code below. It's quite a huge code dump, sorry :/ Thanks for your help!
int index = 0;
foreach(Transform trans in thrs)
{
var t = new RCSThruster();
// get the y direction, this is the thruster position.
Vector3 pos = trans.TransformPoint(trans.localPosition); // make into world-space (abs coord)
Vector3 com = this.baseModule.parentVessel.centreOfMass; // already in world space
Transform refTrans = this.baseModule.parentVessel.referenceTransform;
// check com position
if(pos.z < com.z)
t.offsetCOM |= Direction.Back;
else if(pos.z >= com.z)
t.offsetCOM |= Direction.Forward;
if(pos.x < com.x)
t.offsetCOM |= Direction.Left;
else if(pos.x >= com.x)
t.offsetCOM |= Direction.Right;
if(pos.y < com.y)
t.offsetCOM |= Direction.Down;
else if(pos.y >= com.y)
t.offsetCOM |= Direction.Up;
// check pointing direction
if(trans.up == refTrans.up)
{
t.pointing = Direction.Up;
}
else if(trans.up == -refTrans.up)
{
t.pointing = Direction.Down;
}
else if(trans.up == refTrans.right)
{
t.pointing = Direction.Right;
}
else if(trans.up == -refTrans.right)
{
t.pointing = Direction.Left;
}
else if(trans.up == refTrans.forward)
{
t.pointing = Direction.Forward;
}
else if(trans.up == -refTrans.forward)
{
t.pointing = Direction.Back;
}
t.trans = trans;
var ps = Instantiate(this.thrusterEffect, t.trans.position, t.trans.rotation) as GameObject;
ps.transform.parent = t.trans;
ps.transform.localRotation = Quaternion.identity;
ps.GetComponent<ParticleSystem>().Stop();
t.thrusterEffect = ps;
// Debug.Log(trans.name + ", offsetCOM: " + t.offsetCOM + ", pointing: " + t.pointing);
this.thrusters[index] = t;
index++;
}
void CheckTranslation(Vector3 force, float cutoff)
{
foreach(RCSThruster rcs in this.thrusters)
{
if(Mathf.Abs(force.x) > cutoff)
{
if(force.x > 0 && rcs.pointing == Direction.Left)
rcs.fire = true;
else if(force.x < 0 && rcs.pointing == Direction.Right)
rcs.fire = true;
}
if(Mathf.Abs(force.y) > cutoff)
{
if(force.y > 0 && rcs.pointing == Direction.Down)
rcs.fire = true;
else if(force.y < 0 && rcs.pointing == Direction.Up)
rcs.fire = true;
}
if(Mathf.Abs(force.z) > cutoff)
{
if(force.z > 0 && rcs.pointing == Direction.Back)
rcs.fire = true;
else if(force.z < 0 && rcs.pointing == Direction.Forward)
rcs.fire = true;
}
}
}
void CheckRotation(Vector3 force, float cutoff)
{
foreach(RCSThruster rcs in this.thrusters)
{
if(Mathf.Abs(force.x) > cutoff)
{
if(force.x > 0)
{
if((rcs.offsetCOM & Direction.Back) != 0 && rcs.pointing == Direction.Down)
rcs.fire = true;
else if((rcs.offsetCOM & Direction.Forward) != 0 && rcs.pointing == Direction.Up)
rcs.fire = true;
}
else if(force.x < 0)
{
if((rcs.offsetCOM & Direction.Back) != 0 && rcs.pointing == Direction.Up)
rcs.fire = true;
else if((rcs.offsetCOM & Direction.Forward) != 0 && rcs.pointing == Direction.Down)
rcs.fire = true;
}
}
if(Mathf.Abs(force.y) > cutoff)
{
if(force.y < 0)
{
if((rcs.offsetCOM & Direction.Back) != 0 && rcs.pointing == Direction.Right)
rcs.fire = true;
else if((rcs.offsetCOM & Direction.Forward) != 0 && rcs.pointing == Direction.Left)
rcs.fire = true;
}
else if(force.y > 0)
{
if((rcs.offsetCOM & Direction.Back) != 0 && rcs.pointing == Direction.Left)
rcs.fire = true;
else if((rcs.offsetCOM & Direction.Forward) != 0 && rcs.pointing == Direction.Right)
rcs.fire = true;
}
}
if(Mathf.Abs(force.z) > cutoff)
{
if(force.z < 0)
{
if((rcs.offsetCOM & (Direction.Down | Direction.Right)) == (Direction.Down | Direction.Right)
&& rcs.pointing == Direction.Down)
{
rcs.fire = true;
}
else if((rcs.offsetCOM & (Direction.Down | Direction.Left)) == (Direction.Down | Direction.Left)
&& rcs.pointing == Direction.Left)
{
rcs.fire = true;
}
else if((rcs.offsetCOM & (Direction.Up | Direction.Left)) == (Direction.Up | Direction.Left)
&& rcs.pointing == Direction.Up)
{
rcs.fire = true;
}
else if((rcs.offsetCOM & (Direction.Up | Direction.Right)) == (Direction.Up | Direction.Right)
&& rcs.pointing == Direction.Right)
{
rcs.fire = true;
}
}
else if(force.z > 0)
{
if((rcs.offsetCOM & (Direction.Down | Direction.Left)) == (Direction.Down | Direction.Left)
&& rcs.pointing == Direction.Down)
{
rcs.fire = true;
}
else if((rcs.offsetCOM & (Direction.Up | Direction.Left)) == (Direction.Up | Direction.Left)
&& rcs.pointing == Direction.Left)
{
rcs.fire = true;
}
else if((rcs.offsetCOM & (Direction.Up | Direction.Right)) == (Direction.Up | Direction.Right)
&& rcs.pointing == Direction.Up)
{
rcs.fire = true;
}
else if((rcs.offsetCOM & (Direction.Down | Direction.Right)) == (Direction.Down | Direction.Right)
&& rcs.pointing == Direction.Right)
{
rcs.fire = true;
}
}
}
}
}
Also note this type of open-ended question really belongs in the forums. I hope it's copacetic with everyone that I passed it through, I thought it was well-written.
Answer by AlwaysSunny · Mar 14, 2015 at 09:19 PM
I didn't proof your code, but having glanced over it and heard your proposal, depending on the specificity of your needs, you may be going about this the long way around.
I assume you want the following behaviors:
Thrusters are physically represented objects partnered with one or more rigidbodies to drive body motion. Your vessels respond to input or commands which suggest a desired change in rotation and/or location, and all the thrusters on the vessel are considered and activated if they help achieve the desired rot / loc. Perhaps their force contribution (and visual intensities) are scaled based on contribution, rather than simply on-or-off.
Although all of this is doable, unless your vessels will involve independently controlled flexible joints of some kind, none of this is "necessarily necessary©", and will force PhysX and the CPU to work exponentially harder than a more simplified model. Here's how I'd approach this, if I had the option:
Any time the vessel receives a move command, for-each thruster, if the dot-product of the thruster's forward direction and the requested movement direction is within a certain threshold of 1, it will participate in the simulation this frame. The degree to which it participates is related to the dot-product divided by the threshold. The threshold could mimic the visible representation of the "envelope" of the thruster, but I imagine between 30 and 60 degrees ought to work nicely.
The degree to which each thruster participates informs the intensity of the thruster's visual effects, but here comes the trickery: Each thruster's "forward" direction is multiplied by the degree-of-participation times the thruster's maximumPossibleThrust value, which is then summed with the results of its sibling thrusters to create a single call to vessel.AddForce per frame.
This satisfies all lateral movement. It does not account for the COM, (which might be a very good thing, depending on how aggravating you want piloting ever-so-slightly-unbalanced ships to be), but it does guarantee that if, for instance, we have no "forawrd-facing" thrusters, we cannot move "backward". Rotational movement is somewhat trickier, perhaps involving some simple torque equations to account for COM. Ultimately though, you could likewise create a scenario where only a single call to vessel.AddTorque is required per vessel per frame.
If this just isn't going to cut it, the dot-product methodology I described can still be used to inform each individual thruster's output. Have a look at the documentation article for dot product.
Regardless of the approach you choose, to ensure physically accurate behavior and non-migraine-inducing code, you must follow the prescribed practices for rigidbodies in Unity. This means no parenting bodies to other bodies without making the children kinematic. In other words, either use joints to connect bodies or merge bodies together when they're linked.
I may be overlooking some things, but certainly it's worth your while to read up on rigidbody do's and dont's. If you have any further desire for advice or brainstorming, I'll try to watch this thread. Good question!
Thanks for your in-depth answer (:
To clarify, the thrusters aren't actually Rigidbodies, they're just null objects with a specific orientation that I use to simulate the positions and rotations of the thrusters. I am using FixedJoints to connect rigidbodies (I'm not connecting them just yet, trying to figure out one-body stuff first)
I'm experimenting with what you told me, right now I have this:
foreach(RCSThruster rcs in this.thrusters)
{
float dot = Vector3.Dot(rcs.trans.up, wantedDirection);
rcs.fire = (dot >= 0.5 && dot <= 1.0);
}
It's very rudimentary, and right now I'm just trying to see whether it can even deter$$anonymous$$e which thrusters to fire, disregarding force. As of now it kinda works, but not really. It works along the z-axis, but the Y-axis movement is reversed, and X-axis thrusters just don't fire at all.
I'm a bit confused though -- what do you mean "a threshold of 30 to 60 degrees"? Well I kinda understand the meaning, but how would I apply this threshold? I get back values between 0 and 1 for the dot product, so...
Anyway, what you said about applying a single force makes much more sense than trying to simulate each thruster individually, thanks! I hope you can clarify the rest of the long body of text, though, :P
Oh dear... @AlwaysSunny, I think you have misunderstood my question, perhaps I wasn't explicit enough about my goals.
Looking at your solution that you offered, it seems that it maps inputs along the World X/Y/Z axes, where some input (say H) would always move the craft in the Z+ direction, regardless of its rotation (and hence the calculation to find out which thrusters to activate)
Rather, what I was looking for was kind of the opposite effect: for some input (say H again), the craft should always move forward along the Z+ axis of itself, not the World. Your solution did help though -- in this case I just had to make sure the arguments to Vector3.Dot() were all in local space.
One final thing though, how would I go about making CO$$anonymous$$ affect how the thrusters behave? I want translations to cause rotations and vice versa if the thrusters aren't placed properly or are damaged. I know in the original question I wanted not to have this, but upon further consideration, I think it would be better if the thrusters only behaved smartly in the presence of some kind of upgrade/system, so... how do I enable the realistically frustrating behaviour?
Thanks for all your help so far! (I hope you see this, though -- should I be editing my question ins$$anonymous$$d of posting comments like this for follow-ups?)
EDIT: You don't really need to explain everything in detail (and I don't really wanna waste your time), maybe just direct me to some material that explains it?
I'm glad this was helpful!
Posting comments to follow up is generally more useful than editing the question, because I don't believe subscribers are notified about edits. Doing both is fine.
I've attempted to attach a diagram to illustrate the envelope and participation factor concepts I mentioned, hopefully it'll show up.
Regarding any miscommunications, I know it's tough to describe certain concepts we deal with here. I made some assumptions about what you wanted and offered what I felt was the most flexible solution. I'm glad you're finding a way to leverage this concept in your situation; I think it makes for a neat exploration of vector math.
Regarding respecting CO$$anonymous$$, and your most recent followup requests: This is where this simplified model breaks down, I'm afraid. To achieve the not-so-pilot-friendly "realistic" style of locomotion you're asking for, you should ditch my idea and return to your original assumption, where every thruster represents a call to AddForce.
The good thing about this is, it will always respect CO$$anonymous$$ and behave in a physically plausible manner. And of course, you (or your players) get to solve all the fun engineering issues that come with that. ;)
The dot-and-envelope stuff can still be used to deter$$anonymous$$e which thrusters to fire (e.g. desired direction is vessel.forward) or drive your thruster effects animations, if any. Note that with the non-simplified method, each thruster and body attached to the vessel represents a greater CPU expense than the simple model, but perhaps not by that much.
If you plan on having dozens of thrusters and bodies working together on a single vessel, and many vessels alive simultaneously, I honestly don't know how it will perform.
In such a case, if you find performance or stability become issues, I'd recommend temporarily merging any linked bodies into a single body, so the simulation isn't doing extra work. You can write a tidy method on your Vessel class which can AddPart() and RemovePart() and build temporary compoundcolliders / adjust the mass and CO$$anonymous$$ of the vessel.
Best of luck moving forward,
Edit: pun accidental ^
Your answer
![](https://koobas.hobune.stream/wayback/20220613190120im_/https://answers.unity.com/themes/thub/images/avi.jpg)
Follow this Question
Related Questions
Need help with the physics and angles of gerbils in a plastic ball.. 2 Answers
ForceMode.Acceleration estimated dst != covered dst with a single Addforce in effect 1 Answer
Need a math geeks help. point at which a line hit a plane 1 Answer
In what way can I calculate a predicted target destination based on force to be added? 1 Answer
Drawing projectile trajectory 5 Answers