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

Game DevelopmentMath for Unity3DJune 201840 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. You are already comfortable with degrees — a full circle is 360°. Radians express the same angles in terms of arc length on a unit circle (a circle with radius 1). One full rotation equals 2π radians, because the circumference of a unit circle is 2π.

The key equivalences:

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

Why do radians exist? Because the mathematical definitions of sine and cosine (and their derivatives, series expansions, and relationships to complex numbers) are all defined in radians. The calculus only works out cleanly in radians. Computers — including Unity's Mathf functions — always use radians internally.

Converting Between Degrees and Radians

Unity provides built-in constants for the conversion factors, so you never have to remember them:

// 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

// Pi is available as a constant
float pi = Mathf.PI;  // 3.14159265...

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

In practice you will almost always work in degrees (they are human-readable) and convert to radians just before passing values to Mathf.Sin(), Mathf.Cos(), or Mathf.Tan(). 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. It is a periodic function with period 2π — after a full rotation it returns to exactly where it started.

Key values of cosine:

  • 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

Visualizing cosine over time reveals a smooth wave oscillating between 1 and -1. This oscillating behavior is immediately useful for animations:

using UnityEngine;

// Oscillate an object left and right using cosine
public class CosineOscillate : MonoBehaviour
{
    [SerializeField] private float amplitude = 3f;   // maximum displacement in meters
    [SerializeField] private float frequency = 1f;   // oscillations per second
    [SerializeField] private float phase     = 0f;   // starting offset in degrees

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

Note how multiplying Time.time by frequency * 360f turns the elapsed time into a continuously increasing angle in degrees per second, which we then convert to radians. Increasing frequency makes the oscillation faster; increasing amplitude makes it wider.

The Sine Function

Mathf.Sin(angleInRadians) is cosine's companion. It also returns a value between -1 and 1, but it represents the vertical position on the unit circle. Sine is identical to cosine but shifted 90° (π/2 radians) to the right — they are the same wave, just out of phase.

Key values of sine:

  • sin(0°) = 0 — on the horizontal axis (no vertical offset)
  • sin(90°) = 1 — top of the unit circle
  • sin(180°) = 0 — back on the horizontal axis
  • sin(270°) = -1 — bottom of the unit circle
  • sin(360°) = 0 — back to start

Sine's "starts at zero and rises" behavior makes it perfect for fade-in animations and bobbing:

// 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()
    {
        // Mathf.Sin returns -1 to 1; remap to 0..1 first
        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. It appears constantly in game development shaders and scripts.

What Are Cosine and Sine Used For?

The deeper reason cosine and sine are so useful is the unit circle definition: for any angle θ, the point on the unit circle at that angle has coordinates:

  • X = cos(θ)
  • Y = sin(θ)

This is the foundation of circular motion. If you want an object to orbit a center point at a given radius, you simply scale these unit-circle coordinates by the radius:

  • X = centerX + radius × cos(θ)
  • Y = centerY + radius × sin(θ)

As θ increases continuously (by adding to it every frame), the object traces a perfect circle. The speed of the orbit is determined by how fast θ increases.

Finding a Point on a Circle from an Angle

Let's make this concrete with a 3D orbital motion example. In Unity, orbital motion usually happens on the X-Z horizontal plane (Y is up), so we use cosine for X and sine for Z:

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;   // Y offset from center

    private float currentAngle = 0f;

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

        // Convert the current angle to radians for Mathf functions
        float radians = currentAngle * Mathf.Deg2Rad;

        // Calculate position on the circle
        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);

        // Optionally face the center at all times
        transform.LookAt(center.position);
    }
}

This produces perfectly smooth orbital motion with no drift or accumulation of floating-point error, because we are always computing the absolute position from the angle rather than incrementally moving the object.

Other Uses of the Circle Formula

  • Clock hands: minutes hand angle = -Time.time * 6f degrees (6° per second)
  • Radar scanner: rotating line that sweeps 360° using the same formula
  • Circular menus: place menu items evenly spaced around a circle
  • Planet paths: each planet has its own radius and speed constant
  • Enemy patrol: enemy patrols around a point at a fixed radius
// 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 previous section answered: "given an angle, where is the point on the circle?" The inverse question — "given a point, what is the angle?" — is answered by the arctangent function. Unity provides this as 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. The reason is mathematical convention: Atan2 computes the angle in the standard XY coordinate system where Y is vertical, so the vertical component (Y) comes first.

Why Atan2 and Not Atan?

The regular Mathf.Atan(y/x) function has two problems: it returns values only in the range -90° to 90° (it cannot distinguish between the left and right halves of the circle), and it crashes when x = 0 (division by zero). Atan2(y, x) handles both arguments separately, covers the full -180° to 180° range, and handles x = 0 correctly. Always use Atan2 in game code.

3D: Working in the Horizontal Plane

In 3D Unity projects, "horizontal" angles are typically measured in the X-Z plane (the ground plane). The conventional way to get the horizontal angle of a direction vector is:

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

Notice the argument swap: Atan2(x, z) instead of the standard Atan2(y, x). This is because we are working in the X-Z plane, and we want Z to be our "zero angle" reference (Unity's forward direction is +Z). Swapping the arguments rotates the reference frame by 90°.

Example: Display an Object's Rotation Angle

using UnityEngine;
using UnityEngine.UI;

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

    private void Update()
    {
        // Get the object's forward direction projected onto the ground plane
        Vector3 forward = transform.forward;
        forward.y = 0f;

        // Calculate the horizontal angle using Atan2
        float angle = Mathf.Atan2(forward.x, forward.z) * Mathf.Rad2Deg;

        // Display it
        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. For example: the vector from the turret to the mouse's 3D position includes a vertical component, but the turret only rotates horizontally — we need the horizontal part only.

Mathematically, projecting vector V onto a plane with normal N is:

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

// Unity built-in shortcut (equivalent, cleaner)
Vector3 projected = Vector3.ProjectOnPlane(V, N);

Vector3.ProjectOnPlane(vector, planeNormal) removes the component of the vector that is parallel to the normal, leaving only the component that lies in the plane. The result is always perpendicular to the normal.

Common uses:

  • Turret aiming: project the aim direction onto the horizontal plane so the turret doesn't tilt up/down
  • Slope movement: project the movement direction onto the slope's surface normal to move along it rather than through it
  • Camera: project the camera-to-target vector onto the horizontal plane to avoid camera tilting when target moves vertically
// Example: flatten a direction onto the XZ horizontal plane
Vector3 direction3D   = someTarget.position - transform.position;
Vector3 flatDirection = Vector3.ProjectOnPlane(direction3D, Vector3.up);

// flatDirection now has y = 0 and the same x,z proportions as direction3D
// (it is normalized to the plane, but NOT necessarily a unit vector —
//  normalize separately if needed)
Vector3 flatNormalized = flatDirection.normalized;

Building the Turret Controller

Now we bring everything together. 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 through the mouse position to find the 3D world point
  • A vector from the turret to that world point
  • Plane projection to work in the horizontal plane
  • Atan2 to convert the projected vector to an angle
  • Cosine and sine to verify and debug the target point on the circle
  • Quaternion.LookRotation and Quaternion.RotateTowards for smooth rotation

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

We need to know where in the 3D world the player's mouse cursor is pointing. We already saw this technique in the raycasting chapter: cast a ray from the camera through the mouse position and intersect it with the gameplay plane.

private bool GetMouseWorldPosition(out Vector3 worldPosition)
{
    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    // Cast against an infinite horizontal plane at turret height
    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;
}

Plane.Raycast() casts the ray against a mathematical plane (not a physics collider). This is more reliable than using a Physics.Raycast for the turret's aiming plane because it doesn't depend on having a collider of exactly the right size — it is always an infinite plane.

Step 2: Calculate and Project the Aim Vector

// Get vector from turret to the mouse world point
Vector3 turretToMouse = mouseWorldPosition - transform.position;

// Project onto the horizontal plane (remove vertical component)
Vector3 flatAimVector = Vector3.ProjectOnPlane(turretToMouse, Vector3.up);

// Debug visualization — always add this during development!
Debug.DrawRay(transform.position, flatAimVector, Color.cyan);
Debug.DrawRay(transform.position, turretToMouse, Color.yellow);

Step 3: Calculate the Angle with Atan2

// Get the horizontal angle toward the mouse (0 = forward/+Z, clockwise positive)
float targetAngle = Mathf.Atan2(flatAimVector.x, flatAimVector.z) * Mathf.Rad2Deg;

Step 4: Calculate a Debug Point on the Circle

To visually verify the angle is correct, compute the point on the turret's aiming circle at that angle using cosine and sine, and draw a debug sphere there:

float radius        = 2f; // debug circle radius
float angleRadians  = targetAngle * Mathf.Deg2Rad;
Vector3 debugPoint  = transform.position + new Vector3(
    Mathf.Sin(angleRadians) * radius,
    0f,
    Mathf.Cos(angleRadians) * radius
);
// In the Scene view: should appear where the mouse cursor is aimed
Debug.DrawLine(transform.position, debugPoint, Color.magenta);

Step 5: Rotate the Turret Smoothly

// Calculate target rotation from the flattened aim direction
Quaternion targetRotation = Quaternion.LookRotation(flatAimVector.normalized);

// Smoothly rotate toward the target
float rotationSpeed = 180f; // degrees per second
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;  // degrees per second
    [SerializeField] private Transform barrel;               // child turret barrel mesh

    [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()
    {
        // Step 1: Get the mouse world position
        Vector3 mouseWorld;
        if (!GetMouseWorldPosition(out mouseWorld)) return;

        // Step 2: Vector from turret to target
        Vector3 turretToMouse = mouseWorld - transform.position;
        Vector3 flatAim       = Vector3.ProjectOnPlane(turretToMouse, Vector3.up);

        if (flatAim.sqrMagnitude < 0.01f) return; // Too close — avoid instability

        // Step 3: Calculate angle with Atan2
        currentAngle = Mathf.Atan2(flatAim.x, flatAim.z) * Mathf.Rad2Deg;

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

        // Debug visualization
        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);
        }

        // Shooting
        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;
        }

        // Auto-destroy bullet after 5 seconds
        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;
    }

    // Draw gizmos in the Scene view for visual debugging
    private void OnDrawGizmosSelected()
    {
        // Draw the aiming circle
        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;
        }
    }
}

Turret Setup Instructions

  • Create an empty GameObject named "Turret Base" — this rotates horizontally.
  • Inside it, create a child "Turret Barrel" (a cylinder or imported mesh) oriented along +Z.
  • Add an empty child "Muzzle Point" at the end of the barrel.
  • Attach TurretController to the Turret Base. Assign the barrel and muzzle point.
  • Create a bullet prefab: a small sphere with a Rigidbody and a Collider. No gravity needed (uncheck Use Gravity).
  • Look at the camera from above — the turret should track your mouse cursor smoothly.

Oscillation Patterns with Sine and Cosine Combined

Using sine and cosine together opens up a rich space of interesting motion patterns. Because they are 90° out of phase, combining them produces circular and elliptical paths:

// Figure-8 / Lemniscate 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);
}

// Lissajous curve — two different frequencies
void Update()
{
    float t = Time.time;
    float x = Mathf.Sin(t * freqX + phaseX) * amplitudeX;
    float y = Mathf.Sin(t * freqY + phaseY) * amplitudeY;
    transform.localPosition = new Vector3(x, y, 0f);
}

Exercise: Hungry Bird

In this exercise you will build a "Hungry Bird" scene: a bird is perched at one side of the screen, and seeds fall from above. The bird launches and curves through the air to catch a seed using a trigonometric trajectory.

Concept

The bird's flight path is a parametric curve driven by sine and cosine. As the bird moves from its start position to the seed's position, the horizontal movement is linear (Lerp), while the vertical movement follows a sine arc — going up and then coming back down, creating a natural arc shape.

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;  // random spawn above
    [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;  // 0 to 1

            // Horizontal: linear interpolation
            float x = Mathf.Lerp(startPos.x, targetPos.x, t);
            float z = Mathf.Lerp(startPos.z, targetPos.z, t);

            // Vertical: sine arc — 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);

            // Rotate to face movement direction
            Vector3 nextPos = new Vector3(
                Mathf.Lerp(startPos.x, targetPos.x, t + 0.01f),
                Mathf.Lerp(startPos.y, targetPos.y, t + 0.01f)
                    + Mathf.Sin((t + 0.01f) * Mathf.PI) * arcHeight,
                Mathf.Lerp(startPos.z, targetPos.z, t + 0.01f)
            );
            Vector3 lookDir = nextPos - transform.position;
            if (lookDir.magnitude > 0.01f)
                transform.rotation = Quaternion.LookRotation(lookDir);

            yield return null;
        }

        // Catch the seed
        if (seed != null) Destroy(seed);

        // Return to perch
        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, t * Mathf.PI goes from 0 to π. The sine of that range starts at 0, rises to 1 at the midpoint (t = 0.5, angle = π/2), and returns to 0 at the end. This creates a perfect smooth arc — the bird rises and falls naturally without any additional code.

Summary and Key Formulas

Trigonometry in Unity boils down to a handful of core formulas that you will use again and again:

  • 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

The turret controller built in this chapter is a complete working example of how these pieces combine in production code: plane projection, Atan2, cosine/sine circle formula, and smooth quaternion rotation working together to create a polished gameplay mechanic.

In the next chapter we will look at easing and tweening — how to build smooth, non-linear animations in C# using mathematical easing curves, and how to combine them with the trigonometric motion patterns from this chapter to create truly organic-feeling movement.

Unity3DTrigonometrySineCosineAtan2MathematicsC#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