# 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

``````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;
}
}
}
}
}
``````

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!