25
\$\begingroup\$

I have a 3D model of a turret that con rotate around the Y-axis. This turret has a cannon that is significantly off the center of the object. I want the cannon, not the turret, to aim at a specified target. I can only rotate the turret, however, and thus I don't know what equation I need to apply in order to accomplish by objective.

The following image illustrates my problem:enter image description here

If I have the turret "LookAt()" the target, a laser originating from the cannon will completely miss said target.

If this were a completely top-down scenario, and the cannon were exactly parallel to the turret, then my logic tells me that the fake target should be located at a position that's equal to the actual target plus an offset that's equal to that between the turret and the cannon. However, in my actual scenario, my camera is angled 60º, and the cannon has a slight rotation.

The following image illustrates the scenario: Illustrative Scenario

I'm not sure exactly why, but if I apply that same offset, it only seems to work while aiming at certain distances from the turret.

Is my logic flawed? Am I missing something fundamental here?

Final Edit: the solution provided by @JohnHamilton latest update solves this problem with perfect precision. I have now removed the code and images that I used to illustrate my incorrect implementations.

\$\endgroup\$
  • \$\begingroup\$ From a perspective of weapons design, you could just fix your gun ;) \$\endgroup\$ – Wayne Werner Dec 6 '16 at 12:11
  • \$\begingroup\$ @WayneWerner this is not an option in my case. It's a design choice to have it be crooked, but functional. \$\endgroup\$ – Franconstein Dec 7 '16 at 23:08
  • 1
    \$\begingroup\$ I've added a working example to my answer. \$\endgroup\$ – ens Dec 10 '16 at 21:07
  • \$\begingroup\$ It seems answers are perfect... can you mention what detail do you need exactly? \$\endgroup\$ – Seyed Morteza Kamali Mar 4 '18 at 6:57
31
\$\begingroup\$

The answer is actually pretty easy if you do the math. You have a fixed distance of Y and a variable distance of X (See Picture 1). You need to find out the angle between Z and X and turn your turret that much more. enter image description here

Step 1 - Get distance between the turret line (V) and the gun line (W) which is Y (this is constant but doesn't hurt to calculate). Get distance from the turret to the target (which is X).

Step 2 - Divide Y by X and then get the Hyperbolic Sine of the value

double turnRadians = Mathf.Asin(Y/X);
double angle = Mathf.Rad2Deg * turnRadians;

//where B is the red dot, A is a point on the X line and C is a point on the Z line.

Step 3 - Turn the turret that much more (around the axis that goes from it's top to it's bottom, most likely up axis but only you can know that part).

gameObject.transform.Rotate(Vector3.up, turnAngle);

Of course in this case, you need it to turn counterclockwise so you might need to add a minus in front of the turnAngle there, as in -turnAngle.

Edited some parts. Thanks to @ens for pointing out the difference in distance.

The OP said his gun has an angle so here we go, image first, explanation later: enter image description here

We already know from the previous calculation where to aim the red line according to the blue line. So aiming for the blue line first:

float turnAngle = angleBetweenTurretAndTarget - angleBetweenTurretAndGun;
turret.transform.Rotate(Vector3.up, turnAngle);

The only calculation that differs here, is the calculation of "X Prime" (X') because the angle between the gun and the turret (angle "a") changed the distance between the lines.

//(this part had a mistake of using previous code inside new variable names, YPrime and Y are shown as X' and X in the 2nd picture.
float YPrime = Cos(a)*Y; //this part is what @ens is doing in his answer
double turnRadians = Mathf.Asin(YPrime/X);
double angle = Mathf.Rad2Deg * turnRadians;
turret.transform.Rotate(Vector3.up, angle);

This next part is ONLY necessary if you're doing the turret guns modular (i.e. user can change the guns on a turret and different guns have different angles). If you're doing this in editor, you can already see what the gun angle is according to the turret.

There are two methods for finding the angle "a" , one is the transform.up method:

float angleBetween = Vector3.Angle(turret.transform.up, gun.transform.up);

Above technique will calculate in 3D, so if you want a 2D result, you need to get rid of the Z axis (that's what I assume where gravity is, but if you changed nothing, in Unity it's Y axis that is up or down, i.e. gravity is on the Y axis, so you might have to change things up):

Vector2 turretVector = new Vector2(turret.transform.up.x, turret.transform.up.y);
Vector2 gunVector = new Vector2(gun.transform.up.x, gun.transform.up.y);
float angleBetween = Vector2.Angle(turretVector, gunVector);

Second way is the rotation method (I'm thinking in 2D in this case):

double angleRadians = Mathf.Asin(turret.transform.rotation.z - gun.transform.rotation.z);
double angle = 2 * Mathf.Rad2Deg * angleRadians;

Again, all these codes will give you values that are positive, so you might have to add or subtract the amount depending on the angle (there are calculations for that too, but I'm not going to go that in-depth). A good place to start on this would be the Vector2.Dotmethod in Unity.

Final block of code for additional explanation of what we're doing:

//turn turret towards target
turretTransform.up = targetTransform.position - turretTransform.position;
//adjust for gun angle
if (weaponTransform.localEulerAngles.z <180) //if the value is over 180 it's actually a negative for us
    turretTransform.Rotate(Vector3.forward, 90 - b - a);
else
    turretTransform.Rotate(Vector3.forward, 90 - b + a);

If you did everything right, you should get a scene like this (link for the unitypackage): enter image description here What I mean by always positive values:enter image description here

The Z method can give negative values:enter image description here

For an example scene, get the unitypackage from this link.

Here's the code I've used in the scene (on the turret):

public class TurretAimCorrection : MonoBehaviour
{
    public Transform targetTransform;
    public Transform turretTransform;
    public Transform weaponTransform;

    private float f, d, x, y, h, b, a, weaponAngle, turnAngle;
    private void Start()
    {
        TurnCorrection();
    }

    private void Update()
    {
        TurnCorrection();
    }
    void TurnCorrection()
    {
        //find distances and angles
        d = Vector2.Distance(new Vector2(targetTransform.position.x, targetTransform.position.y), new Vector2(turretTransform.position.x, turretTransform.position.y));
        x = Vector2.Distance(new Vector2(turretTransform.position.x, turretTransform.position.y), new Vector2(weaponTransform.position.x, weaponTransform.position.y));
        weaponAngle = weaponTransform.localEulerAngles.z;
        weaponAngle = weaponAngle * Mathf.Deg2Rad;
        y = Mathf.Abs(Mathf.Cos(weaponAngle) * x);
        b = Mathf.Rad2Deg * Mathf.Acos(y / d);
        a = Mathf.Rad2Deg * Mathf.Acos(y / x);
        //turn turret towards target
        turretTransform.up = targetTransform.position - turretTransform.position;
        //adjust for gun angle
        if (weaponTransform.localEulerAngles.z < 180)
            turretTransform.Rotate(Vector3.forward, 90 - b - a);
        else
            turretTransform.Rotate(Vector3.forward, 90 - b + a);
        //Please leave this comment in the code. This code was made by 
        //http://gamedev.stackexchange.com/users/93538/john-hamilton a.k.a. CrazyIvanTR. 
        //This code is provided as is, with no guarantees. It has worked in local tests on Unity 5.5.0f3.
    }
}

3D adapted code with X and Z as the 2D plane:

public class TurretAimCorrection : MonoBehaviour
{
    public Transform targetTransform; //drag target here
    public Transform turretTransform; //drag turret base or turret top part here
    public Transform weaponTransform; //drag the attached weapon here

    private float d, x, y, b, a, weaponAngle, turnAngle;
    private void Start()
    {
        TurnAdjustment();
    }

    private void Update()
    {
        TurnAdjustment();
    }
    void TurnAdjustment()
    {

        d = Vector2.Distance(new Vector2(targetTransform.position.x, targetTransform.position.z), new Vector2(turretTransform.position.x, turretTransform.position.z));
        x = Vector2.Distance(new Vector2(turretTransform.position.x, turretTransform.position.z), new Vector2(weaponTransform.position.x, weaponTransform.position.z));
        weaponAngle = weaponTransform.localEulerAngles.y;
        weaponAngle = weaponAngle * Mathf.Deg2Rad;
        y = Mathf.Abs(Mathf.Cos(weaponAngle) * x);
        b = Mathf.Rad2Deg * Mathf.Acos(y / d);
        a = Mathf.Rad2Deg * Mathf.Acos(y / x);
        //turn turret towards target
        turretTransform.forward = new Vector3(targetTransform.position.x, 0, targetTransform.position.z) - new Vector3(turretTransform.position.x, 0, turretTransform.position.z);
        //adjust for gun angle
        if (weaponTransform.localEulerAngles.y < 180)
            turretTransform.Rotate(Vector3.up, - a +b-90);
        else
            turretTransform.Rotate(Vector3.up, + a+ b - 90);
        //Please leave this comment in the code. This code was made by 
        //http://gamedev.stackexchange.com/users/93538/john-hamilton a.k.a. CrazyIvanTR. 
        //This code is provided as is, with no guarantees. It has worked in local tests on Unity 5.5.0f3.
    }
}
\$\endgroup\$
  • \$\begingroup\$ There's a slight flaw in the first image. Z is the length of the turret to the box. X is the length of the turret to the box after rotation... x = z. Therefore, unless y is the hypotenuse that isn't a right triangle and sin does not apply. \$\endgroup\$ – The Great Duck Dec 6 '16 at 7:56
  • \$\begingroup\$ @TheGreatDuck Z isn't the distance between turret and the box it's the Vector2.forward of that turret (it's just shown finite instead of having an arrow at the end). Even if Z was the distance, the picture has units and you can see that Z < X without even calculating. \$\endgroup\$ – John Hamilton Dec 6 '16 at 8:57
  • 2
    \$\begingroup\$ @Franconstein you don't have to first turn the turret, then apply these. You can first calculate these, then add the degree that you get from these equations to the turret's turn degree. (So instead of turning the turret 20 degrees to the object, then adjusting for the gun, you would be turning the turret by 20 degrees + adjustment for the gun). \$\endgroup\$ – John Hamilton Dec 6 '16 at 20:48
  • \$\begingroup\$ @Franconstein See the newly adjusted code. Since we changed planes the code was acting differently than it was in the other version. I have no idea why this happened but it's working perfectly on my end now. See: imgur.com/a/1scEH (removing your turrets was not necessary, those simple models acted the same way as yours did). \$\endgroup\$ – John Hamilton Dec 10 '16 at 18:39
  • 1
    \$\begingroup\$ @JohnHamilton You did it! It is finally resolved, and with laser-like precision, too! Thank you! Thank you! Thank you! I will now edit my post to how it was in the beginning, so it can be more easily understood for future reference! Once more, thank you! \$\endgroup\$ – Franconstein Dec 10 '16 at 19:49
3
\$\begingroup\$

You could also use a more general approach:

The math for your problem already exists in the form of the scalarproduct (or dot product). You only need to get the directions of your weapons forward axis and the direction from your weapon to the target.

Let W be your weapon's forward vector.

Let D be the direction from your weapon to your target. (Target.pos - Weapon.pos)

If you solve the dot product's formula

dot(A,B) = |A|*|B|*cos(alpha) with alpha = the angle between A and B

for alpha, you get:

              ( dot(W,D) )
alpha = arccos(--------- )
              ( |W|*|D|  )

You only have to convert radians to degrees and you got your angle to rotate your robot. (As you mentioned the weapon is at an angle to your robot, so you need to add the angle to alpha)

enter image description hereenter image description here

\$\endgroup\$
2
+100
\$\begingroup\$

All the answers posted so far are (more or less) wrong, so here's a quick correct solution:

enter image description here

To aim the gun towards the target, rotate the turret forward vector to the target and add the angle θ.

So let's find θ:

   d / sin(δ) = a / sin(α) # By the Law of Sines
=> α = asin(a * sin(δ) / d)

   β = 180° - α - δ
   θ = 90° - β
=> θ = α + δ - 90°
     = asin(a * sin(δ) / d) + δ - 90°
     = asin(a * cos(δ') / d) - δ' # Where (δ' = 90° - δ) is the angle between 
                                  # the gun and the turret forward vector.

When δ' = 0 this simplifies to θ = asin(a / d), which matches the first part of John Hamilton's answer.

Edit:

I've added a working example.

Open in JSFiddle or use the embedded snippet below:

var Degree = Math.PI / 180;
var showDebugOverlays = false;

var Turret = {
    gunAngle: -10 * Degree,
    maxGunAngle: 85 * Degree,
    adaptedGunXOffset: null,

    rotateToTarget: function (target) {
        var delta = Vec.subtract(this.position, target.position);
        var dist = Vec.length(delta);
        var angle = Vec.angle(delta);

        theta = Math.asin(this.adaptedGunXOffset / dist) + this.gunAngle;
        this.rotation = -(angle + theta);

        this.updateGunRay(target);
    },

    setGunAngle: function (angle) {
        var angle = this.clampGunAngle(angle);
        this.gunAngle = angle;
        this.gun.rotation = angle;
        // Account for the fact that the origin of the gun also has an y offset
        // relative to the turret origin
        var extraXOffset = this.gun.position.y * Math.tan(angle);
        var gunXOffset = this.gun.position.x + extraXOffset;
        // This equals "a * cos(δ')" in the angle formula
        this.adaptedGunXOffset = gunXOffset * Math.cos(-angle);

        if (showDebugOverlays) {
            // Show x offsets
            this.removeChild(this.xOffsetOverlay);
            this.removeChild(this.extraXOffsetOverlay);
            this.xOffsetOverlay = addRect(this, 0, 0, this.gun.position.x, 1, 0xf6ff00);
            this.extraXOffsetOverlay = addRect(this, this.gun.position.x, 0, extraXOffset, 1, 0xff00ae);
        }
    },

    rotateGun: function (angleDelta) {
        this.setGunAngle(this.gunAngle + angleDelta);
    },

    updateGunRay: function (target) {
        var delta = this.gun.toLocal(target.position);
        var dist = Vec.length(delta);
        this.gun.removeChild(this.gun.ray);
        this.gun.ray = makeLine(0, 0, 0, -dist);
        this.gun.addChildAt(this.gun.ray, 0);
    },

    clampGunAngle: function (angle) {
        if (angle > this.maxGunAngle) {
            return this.maxGunAngle;
        }
        if (angle < -this.maxGunAngle) {
            return -this.maxGunAngle;
        }
        return angle;
    }
}

function makeTurret() {
    var turret = new PIXI.Sprite.fromImage('http://i.imgur.com/gPtlPJh.png');
    var gunPos = new PIXI.Point(25, -25)

    turret.anchor.set(0.5, 0.5);

    var gun = new PIXI.Container();
    var gunImg = new PIXI.Sprite.fromImage('http://i.imgur.com/RE45GEY.png');
    gun.ray = makeLine(0, 0, 0, -250);
    gun.addChild(gun.ray);

    gunImg.anchor.set(0.5, 0.6);
    gun.addChild(gunImg);
    gun.position = gunPos;

    // Turret forward vector
    turret.addChild(makeLine(0, -38, 0, -90, 0x38ce2c));
    turret.addChild(gun);
    turret.gun = gun;

    Object.setPrototypeOf(Turret, Object.getPrototypeOf(turret));
    Object.setPrototypeOf(turret, Turret);

    turret.setGunAngle(turret.gunAngle);

    if (showDebugOverlays) {
        addRect(turret, 0, 0, 1, 1); // Show turret origin
        addRect(gun, -1, 1, 2, 2, 0xff0096); // Show gun origin
    }

    return turret;
}

function makeTarget() {
    var target = new PIXI.Graphics();
    target.beginFill(0xd92f8f);
    target.drawCircle(0, 0, 9);
    target.endFill();
    return target;
}

var CursorKeys = {
    map: { ArrowLeft: -1, ArrowRight: 1 },
    pressedKeyDirection: null,

    onKeyDown: function (keyEvent) {
        var key = this.map[keyEvent.key];
        if (key) {
            this.pressedKeyDirection = key;
        }
    },
    onKeyUp: function (keyEvent) {
        var key = this.map[keyEvent.key];
        if (key) {
            if (this.pressedKeyDirection == key) {
                this.pressedKeyDirection = null;
            }
        }
    }
}

document.body.addEventListener("keydown", CursorKeys.onKeyDown.bind(CursorKeys));
document.body.addEventListener("keyup", CursorKeys.onKeyUp.bind(CursorKeys));

function makeLine(x1, y1, x2, y2, color) {
    if (color == undefined) {
        color = 0x66CCFF;
    }
    var line = new PIXI.Graphics();
    line.lineStyle(1.5, color, 1);
    line.moveTo(x1, y1);
    line.lineTo(x2, y2);
    return line;
}

function addRect(parent, x, y, w, h, color) {
    if (color == undefined) {
        color = 0x66CCFF;
    }
    var rectangle = new PIXI.Graphics();
    rectangle.beginFill(color);
    rectangle.drawRect(x, y, w, h);
    rectangle.endFill();
    parent.addChild(rectangle);
    return rectangle;
}

var Vec = {
    subtract: function (a, b) {
        return { x: a.x - b.x,
                 y: a.y - b.y };
    },
    length: function (v) {
        return Math.sqrt(v.x * v.x + v.y * v.y);
    },
    angle: function (v) {
        return Math.atan2(v.x, v.y)
    }
}

Math.clamp = function(n, min, max) {
    return Math.max(min, Math.min(n, max));
}

var renderer;
var stage;
var turret;
var target;

function run() {
    renderer = PIXI.autoDetectRenderer(600, 300, { antialias: true });
    renderer.backgroundColor = 0x2a2f34;
    document.body.appendChild(renderer.view);
    stage = new PIXI.Container();
    stage.interactive = true;

    target = makeTarget();
    target.position = { x: renderer.width * 0.2, y: renderer.height * 0.3 };

    turret = makeTurret();
    turret.position = { x: renderer.width * 0.65, y: renderer.height * 0.3 };
    turret.rotateToTarget(target);

    stage.addChild(turret);
    stage.addChild(target);

    var message = new PIXI.Text(
        "Controls: Mouse, left/right cursor keys",
        {font: "18px Arial", fill: "#7c7c7c"}
    );
    message.position.set(10, 10);
    stage.addChild(message);

    stage.on('mousemove', function(e) {
        var pos = e.data.global;
        target.position.x = Math.clamp(pos.x, 0, renderer.width);
        target.position.y = Math.clamp(pos.y, 0, renderer.height);
        turret.rotateToTarget(target);
    })

    animate();
}

function animate() {
    requestAnimationFrame(animate);

    if (CursorKeys.pressedKeyDirection) {
        turret.rotateGun(3 * Degree * CursorKeys.pressedKeyDirection);
        turret.rotateToTarget(target);
    }

    renderer.render(stage);
}

run();
body {
    padding: 0;
    margin: 0;
}

#capture_focus {
  position: absolute;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.2.2/pixi.min.js"></script>
<div id="capture_focus" />

\$\endgroup\$
  • \$\begingroup\$ Thank you very much for this explanation. It was simple enough for me to understand, and it seems to take into account every situation. However, when I implemented it, the results I obtained where not favorable. I have edited my original post to include my code, an image visualizing my setup, and the results for each variable. My turret's forward vector is always looking at the target, but even if it's not, the results remain almost the same. Am I doing something wrong? Is it my code? \$\endgroup\$ – Franconstein Dec 8 '16 at 20:48
  • \$\begingroup\$ If the other answers "are more or less wrong", you do not understand / implement them correctly. I've used both alternate answers, previously, to create the desired behavior. @Franconstein, I even see your comments on atleast one to say you have verified that it works. If you've verified a solution, do you still have a problem? \$\endgroup\$ – Gnemlock Dec 8 '16 at 21:22
  • \$\begingroup\$ @Gnemlock, John Hamilton's solution was not wrong - I implemented it, and it worked, and thus I verified his solution as approved. But after implementing it, I started trying different non-static scenarios, and the solution did not hold up. I didn't want to prematurely discard it, though, so I went over it with a colleague. We ended up confirming it doesn't hold, but now ens posted another possible solution, and John edited his post to include it. As of this moment, I cannot confirm either of them work correctly, and am still attempting. I posted my code to see if it helps. Did I do wrong? \$\endgroup\$ – Franconstein Dec 8 '16 at 21:35
  • \$\begingroup\$ @Franconstein, in this form it is overly confusing. I would say this example is a fine example of what you would expect reading a Maths text book, but it is overbearingly confusing in relation to general game programming. The only important element is the angle (which the original answer John Hamilton posted did provide). I see what you mean by particular angles, ultimately you may have done this incorrectly. I find there is a lot of room, in this answer, to do it incorrectly. \$\endgroup\$ – Gnemlock Dec 8 '16 at 21:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.