Chapter 8: Trigonometry — Sine, Cosine, and the Turret Controller

Game Development Math for Unity3D June 2018 40 min read

Introduction

Trigonometry is one of those topics that sounds intimidating but unlocks an enormous range of gameplay mechanics the moment you understand it. Orbital motion, wave-based animations, bobbing effects, aiming systems, circular UI indicators, clock hands, pendulums, procedural planet paths all of these are a few lines of trigonometry code. Once you have a genuine intuition for sine and cosine, you will find yourself reaching for them constantly.

In this chapter we will build that intuition from the ground up, always grounding abstract concepts in practical Unity code. The capstone is a complete turret controller that uses every technique in this chapter: Atan2 to find the angle from a vector, cosine and sine to compute circular target positions, plane projection to work in 3D space, and Quaternion methods to smooth the final rotation.

Trigonometry Review Radians vs Degrees

There are two ways to measure angles: degrees and radians. A full circle is 360° in degrees, and 2π radians in radians.

  • 0° = 0 radians
  • 90° = π/2 radians ≈ 1.5708
  • 180° = π radians ≈ 3.1416
  • 270° = 3π/2 radians ≈ 4.7124
  • 360° = 2π radians ≈ 6.2832

Computers including Unity's Mathf functions always use radians internally.

Converting Between Degrees and Radians

// Degrees to radians: multiply by Mathf.Deg2Rad
float radians = degrees * Mathf.Deg2Rad;   // Deg2Rad = π / 180 ≈ 0.01745

// Radians to degrees: multiply by Mathf.Rad2Deg
float degrees = radians * Mathf.Rad2Deg;   // Rad2Deg = 180 / π ≈ 57.2958

float pi    = Mathf.PI;          // 3.14159265...
float twoPi = 2f * Mathf.PI;     // 6.28318... (full circle)
float halfPi = Mathf.PI / 2f;    // 1.5708...  (quarter circle / 90°)

In practice the workflow is:

  1. Think and store angles in degrees
  2. Multiply by Mathf.Deg2Rad when calling trig functions
  3. Multiply results of Mathf.Atan2() by Mathf.Rad2Deg to get degrees back

The Cosine Function

Mathf.Cos(angleInRadians) returns a value between -1 and 1 that describes the horizontal position on the unit circle at the given angle.

  • cos(0°) = 1 rightmost point of the unit circle
  • cos(90°) = 0 top of the unit circle (no horizontal offset)
  • cos(180°) = -1 leftmost point
  • cos(270°) = 0 bottom of the unit circle
  • cos(360°) = 1 back to start
using UnityEngine;

public class CosineOscillate : MonoBehaviour
{
    [SerializeField] private float amplitude = 3f;
    [SerializeField] private float frequency = 1f;
    [SerializeField] private float phase     = 0f;

    private Vector3 startPosition;

    private void Start()
    {
        startPosition = transform.position;
    }

    private void Update()
    {
        float angle = (Time.time * frequency * 360f + phase) * Mathf.Deg2Rad;
        float offset = Mathf.Cos(angle) * amplitude;
        transform.position = startPosition + Vector3.right * offset;
    }
}

The Sine Function

Mathf.Sin(angleInRadians) also returns a value between -1 and 1, but it represents the vertical position on the unit circle.

  • sin(0°) = 0 on the horizontal axis
  • sin(90°) = 1 top of the unit circle
  • sin(180°) = 0 back on the horizontal axis
  • sin(270°) = -1 bottom of the unit circle
// Bobbing object moves up and down using sine
public class SineBob : MonoBehaviour
{
    [SerializeField] private float bobHeight = 0.5f;
    [SerializeField] private float bobSpeed  = 2f;

    private Vector3 originPosition;

    private void Start() { originPosition = transform.position; }

    private void Update()
    {
        float yOffset = Mathf.Sin(Time.time * bobSpeed) * bobHeight;
        transform.position = originPosition + Vector3.up * yOffset;
    }
}

// Pulsing alpha using sine (for a UI glow effect)
public class SinePulse : MonoBehaviour
{
    [SerializeField] private CanvasGroup canvasGroup;
    [SerializeField] private float minAlpha = 0.3f;
    [SerializeField] private float maxAlpha = 1.0f;
    [SerializeField] private float pulseSpeed = 1.5f;

    private void Update()
    {
        // Remap -1..1 range of sine to 0..1
        float t = (Mathf.Sin(Time.time * pulseSpeed) + 1f) * 0.5f;
        canvasGroup.alpha = Mathf.Lerp(minAlpha, maxAlpha, t);
    }
}

The remapping trick (Mathf.Sin(x) + 1f) * 0.5f converts the -1..1 range of sine to 0..1, which is much easier to work with for most visual effects.

Finding a Point on a Circle from an Angle

For any angle θ, the point on the unit circle at that angle has coordinates X = cos(θ) and Y = sin(θ). Scale these by the radius to place an object on a circle:

using UnityEngine;

public class OrbitAround : MonoBehaviour
{
    [SerializeField] private Transform center;
    [SerializeField] private float orbitRadius = 5f;
    [SerializeField] private float orbitSpeed  = 45f;  // degrees per second
    [SerializeField] private float orbitHeight = 0f;

    private float currentAngle = 0f;

    private void Update()
    {
        currentAngle += orbitSpeed * Time.deltaTime;

        float radians = currentAngle * Mathf.Deg2Rad;

        float x = center.position.x + orbitRadius * Mathf.Cos(radians);
        float z = center.position.z + orbitRadius * Mathf.Sin(radians);
        float y = center.position.y + orbitHeight;

        transform.position = new Vector3(x, y, z);

        transform.LookAt(center.position);
    }
}
// Evenly distribute N items around a circle
void ArrangeInCircle(GameObject[] items, float radius)
{
    float angleStep = 360f / items.Length;
    for (int i = 0; i < items.Length; i++)
    {
        float angle = i * angleStep * Mathf.Deg2Rad;
        float x     = Mathf.Cos(angle) * radius;
        float z     = Mathf.Sin(angle) * radius;
        items[i].transform.localPosition = new Vector3(x, 0f, z);
    }
}

Finding an Angle from a Point Atan2

The inverse question "given a point, what is the angle?" is answered by Mathf.Atan2(y, x).

// Returns angle in RADIANS from -π to π (-180° to 180°)
float angleRadians = Mathf.Atan2(y, x);

// Convert to degrees
float angleDegrees = Mathf.Atan2(y, x) * Mathf.Rad2Deg;

Critical note on argument order: It is Atan2(y, x) Y first, X second. This is a famous source of bugs. Always use Atan2 in game code regular Mathf.Atan(y/x) only covers -90° to 90° and crashes when x = 0.

3D: Working in the Horizontal Plane

// angle = 0 when pointing along +Z (forward), increases clockwise
float horizontalAngle = Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg;

Example: Display an Object's Rotation Angle

using UnityEngine;
using UnityEngine.UI;

public class AngleDisplay : MonoBehaviour
{
    [SerializeField] private Text angleText;

    private void Update()
    {
        Vector3 forward = transform.forward;
        forward.y = 0f;

        float angle = Mathf.Atan2(forward.x, forward.z) * Mathf.Rad2Deg;

        angleText.text = "Angle: " + angle.ToString("F1") + "°";
    }
}

Vector Projection onto a Plane

When working in 3D, we often have a 3D vector that we want to "flatten" onto a plane. Projecting vector V onto a plane with normal N:

// Manual calculation
Vector3 projected = V - Vector3.Dot(V, N) * N;

// Unity built-in shortcut (equivalent, cleaner)
Vector3 projected = Vector3.ProjectOnPlane(V, N);
// Example: flatten a direction onto the XZ horizontal plane
Vector3 direction3D   = someTarget.position - transform.position;
Vector3 flatDirection = Vector3.ProjectOnPlane(direction3D, Vector3.up);
Vector3 flatNormalized = flatDirection.normalized;

Building the Turret Controller

The turret controller is a 3D turret that rotates horizontally to aim at the point where the mouse cursor intersects the game world. It uses a raycast from the camera, plane projection, Atan2, and smooth quaternion rotation.

Step 1: Convert 2D Mouse Position to a 3D World Point

private bool GetMouseWorldPosition(out Vector3 worldPosition)
{
    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    Plane groundPlane = new Plane(Vector3.up, transform.position);

    float distance;
    if (groundPlane.Raycast(ray, out distance))
    {
        worldPosition = ray.GetPoint(distance);
        return true;
    }
    worldPosition = Vector3.zero;
    return false;
}

Step 2: Calculate and Project the Aim Vector

Vector3 turretToMouse = mouseWorldPosition - transform.position;
Vector3 flatAimVector = Vector3.ProjectOnPlane(turretToMouse, Vector3.up);

Debug.DrawRay(transform.position, flatAimVector, Color.cyan);

Step 3: Calculate the Angle with Atan2

float targetAngle = Mathf.Atan2(flatAimVector.x, flatAimVector.z) * Mathf.Rad2Deg;

Step 4: Rotate the Turret Smoothly

Quaternion targetRotation = Quaternion.LookRotation(flatAimVector.normalized);

float rotationSpeed = 180f;
transform.rotation = Quaternion.RotateTowards(
    transform.rotation,
    targetRotation,
    rotationSpeed * Time.deltaTime
);

Complete Turret Script

using UnityEngine;

public class TurretController : MonoBehaviour
{
    [Header("Rotation")]
    [SerializeField] private float rotationSpeed   = 180f;
    [SerializeField] private Transform barrel;

    [Header("Shooting")]
    [SerializeField] private GameObject bulletPrefab;
    [SerializeField] private Transform  muzzlePoint;
    [SerializeField] private float      bulletSpeed   = 20f;
    [SerializeField] private float      fireRate      = 0.2f;

    [Header("Debug")]
    [SerializeField] private bool showDebugGizmos = true;
    [SerializeField] private float debugCircleRadius = 2f;

    private float lastFireTime;
    private float currentAngle;

    private void Update()
    {
        Vector3 mouseWorld;
        if (!GetMouseWorldPosition(out mouseWorld)) return;

        Vector3 turretToMouse = mouseWorld - transform.position;
        Vector3 flatAim       = Vector3.ProjectOnPlane(turretToMouse, Vector3.up);

        if (flatAim.sqrMagnitude < 0.01f) return;

        currentAngle = Mathf.Atan2(flatAim.x, flatAim.z) * Mathf.Rad2Deg;

        Quaternion targetRotation = Quaternion.LookRotation(flatAim.normalized, Vector3.up);
        transform.rotation = Quaternion.RotateTowards(
            transform.rotation,
            targetRotation,
            rotationSpeed * Time.deltaTime
        );

        if (showDebugGizmos)
        {
            float rad      = currentAngle * Mathf.Deg2Rad;
            Vector3 circPt = transform.position + new Vector3(
                Mathf.Sin(rad) * debugCircleRadius,
                0f,
                Mathf.Cos(rad) * debugCircleRadius
            );
            Debug.DrawRay(transform.position, flatAim, Color.cyan);
            Debug.DrawLine(transform.position, circPt, Color.magenta);
        }

        if (Input.GetMouseButton(0) && Time.time >= lastFireTime + fireRate)
        {
            Fire();
        }
    }

    private void Fire()
    {
        lastFireTime = Time.time;

        if (bulletPrefab == null || muzzlePoint == null) return;

        GameObject bullet = Instantiate(bulletPrefab, muzzlePoint.position, muzzlePoint.rotation);
        Rigidbody bulletRb = bullet.GetComponent<Rigidbody>();
        if (bulletRb != null)
        {
            bulletRb.velocity = muzzlePoint.forward * bulletSpeed;
        }

        Destroy(bullet, 5f);
    }

    private bool GetMouseWorldPosition(out Vector3 worldPosition)
    {
        Ray ray   = Camera.main.ScreenPointToRay(Input.mousePosition);
        Plane gp  = new Plane(Vector3.up, transform.position);
        float dist;

        if (gp.Raycast(ray, out dist))
        {
            worldPosition = ray.GetPoint(dist);
            return true;
        }
        worldPosition = Vector3.zero;
        return false;
    }

    private void OnDrawGizmosSelected()
    {
        Gizmos.color = new Color(0f, 1f, 1f, 0.3f);
        int segments = 32;
        Vector3 prevPoint = transform.position + Vector3.forward * debugCircleRadius;
        for (int i = 1; i <= segments; i++)
        {
            float angle = (float)i / segments * 360f * Mathf.Deg2Rad;
            Vector3 nextPoint = transform.position + new Vector3(
                Mathf.Sin(angle) * debugCircleRadius,
                0f,
                Mathf.Cos(angle) * debugCircleRadius
            );
            Gizmos.DrawLine(prevPoint, nextPoint);
            prevPoint = nextPoint;
        }
    }
}

Oscillation Patterns with Sine and Cosine Combined

// Figure-8 motion
void Update()
{
    float t = Time.time * speed;
    float x = Mathf.Sin(t) * amplitude;
    float z = Mathf.Sin(t * 2f) * amplitude * 0.5f;
    transform.position = origin + new Vector3(x, 0f, z);
}

// Spiral ascent circle on X-Z, rising on Y
void Update()
{
    float t = Time.time * orbitSpeed;
    float x = Mathf.Cos(t) * orbitRadius;
    float z = Mathf.Sin(t) * orbitRadius;
    float y = Time.time * riseSpeed;
    transform.position = new Vector3(x, y, z);
}

Exercise: Hungry Bird

A bird launches and curves through the air to catch seeds using a trigonometric trajectory. The horizontal movement is linear (Lerp), while the vertical movement follows a sine arc.

using UnityEngine;
using System.Collections;

public class HungryBird : MonoBehaviour
{
    [SerializeField] private float flyDuration   = 1.5f;
    [SerializeField] private float arcHeight     = 3f;
    [SerializeField] private GameObject seedPrefab;
    [SerializeField] private Transform  seedSpawnArea;
    [SerializeField] private float      spawnInterval   = 3f;

    private Vector3 perchPosition;
    private bool    isFlying = false;

    private void Start()
    {
        perchPosition = transform.position;
        StartCoroutine(SpawnSeeds());
    }

    private IEnumerator SpawnSeeds()
    {
        while (true)
        {
            yield return new WaitForSeconds(spawnInterval);
            if (!isFlying)
            {
                Vector3 spawnPos = seedSpawnArea.position
                    + new Vector3(Random.Range(-3f, 3f), 0f, 0f);
                GameObject seed = Instantiate(seedPrefab, spawnPos, Quaternion.identity);
                StartCoroutine(FlyToSeed(seed));
            }
        }
    }

    private IEnumerator FlyToSeed(GameObject seed)
    {
        isFlying = true;
        Vector3 startPos  = transform.position;
        Vector3 targetPos = seed.transform.position;
        float   elapsed   = 0f;

        while (elapsed < flyDuration)
        {
            elapsed += Time.deltaTime;
            float t = elapsed / flyDuration;

            float x = Mathf.Lerp(startPos.x, targetPos.x, t);
            float z = Mathf.Lerp(startPos.z, targetPos.z, t);

            // sin goes 0→1→0 over π, giving a smooth arch
            float arcT = Mathf.Sin(t * Mathf.PI) * arcHeight;
            float y    = Mathf.Lerp(startPos.y, targetPos.y, t) + arcT;

            transform.position = new Vector3(x, y, z);

            yield return null;
        }

        if (seed != null) Destroy(seed);

        yield return StartCoroutine(ReturnToPerch(targetPos));
        isFlying = false;
    }

    private IEnumerator ReturnToPerch(Vector3 fromPos)
    {
        float   elapsed  = 0f;
        float   duration = flyDuration * 0.8f;

        while (elapsed < duration)
        {
            elapsed += Time.deltaTime;
            float t  = elapsed / duration;
            float x  = Mathf.Lerp(fromPos.x, perchPosition.x, t);
            float z  = Mathf.Lerp(fromPos.z, perchPosition.z, t);
            float y  = Mathf.Lerp(fromPos.y, perchPosition.y, t)
                     + Mathf.Sin(t * Mathf.PI) * arcHeight * 0.5f;
            transform.position = new Vector3(x, y, z);
            yield return null;
        }

        transform.position = perchPosition;
    }
}

The key insight here is the line Mathf.Sin(t * Mathf.PI). As t goes from 0 to 1, the sine of that range starts at 0, rises to 1 at the midpoint, and returns to 0 at the end. This creates a perfect smooth arc.

Summary and Key Formulas

  • Point on circle: x = cx + r * Mathf.Cos(angle * Mathf.Deg2Rad), z = cz + r * Mathf.Sin(angle * Mathf.Deg2Rad)
  • Angle from direction (3D horizontal): Mathf.Atan2(dir.x, dir.z) * Mathf.Rad2Deg
  • Angle from 2D point: Mathf.Atan2(y, x) * Mathf.Rad2Deg
  • Project onto horizontal plane: Vector3.ProjectOnPlane(vector, Vector3.up)
  • Oscillation (0 to 1 to 0): Mathf.Sin(t * Mathf.PI) where t goes 0 to 1
  • Remap -1..1 to 0..1: (Mathf.Sin(x) + 1f) * 0.5f
  • Convert degrees to radians: degrees * Mathf.Deg2Rad
  • Convert radians to degrees: radians * Mathf.Rad2Deg
Unity3D Trigonometry Sine Cosine Atan2 Mathematics C# Turret

Need help with Unity3D development?

I'm a senior developer with 16+ years experience, including AAA projects at Ubisoft. Let's discuss how I can help with your game or interactive project.

Start a Conversation