Chapter 6: Physics Programming — Forces, Torque, and Applied Math

Game Development Math for Unity3D June 2018 40 min read

Introduction

In the previous chapters you learned how Unity's physics components work: the Rigidbody, colliders, physics materials, and the PhysX simulation engine running underneath it all. Now it is time to connect that knowledge to mathematics. In this chapter you will learn how to apply forces, torques, and impulses programmatically in C#, understand exactly when and where to write physics code, and build two complete physics-based projects from scratch.

Physics programming sits at a fascinating intersection of math and game feel. The same jump can feel floaty or snappy depending purely on which ForceMode you choose and how much mass your Rigidbody has. Once you understand the math behind these choices, you will stop guessing and start designing exactly the feel you want.

The AddForce() Method

The primary way to move a physics object in Unity is Rigidbody.AddForce(). This method tells the physics engine to apply a force vector to the Rigidbody's center of mass at the next simulation step. Unlike setting transform.position directly, using AddForce() respects mass, drag, and all other physics properties the result is physically plausible motion.

The basic signature is:

rb.AddForce(Vector3 force, ForceMode mode = ForceMode.Force);

The first argument is the force vector in world space. If you want to apply force in the object's local space instead, use AddRelativeForce():

rb.AddRelativeForce(Vector3 localForce, ForceMode mode = ForceMode.Force);

This is useful for things like spaceship thrusters: you always want to push "forward" relative to the ship, regardless of which way the ship is pointing in world space.

3D vs 2D Physics

Unity has two separate physics engines: the 3D engine (PhysX) and the 2D engine (Box2D).

  • 3D: Rigidbody component + AddForce(Vector3, ForceMode)
  • 2D: Rigidbody2D component + AddForce(Vector2, ForceMode2D)

Note that ForceMode2D only has two values: ForceMode2D.Force and ForceMode2D.Impulse. The Acceleration and VelocityChange modes are not available in 2D.

Building a Physics-Based Movement Controller

using UnityEngine;

public class PhysicsMovement : MonoBehaviour
{
    [SerializeField] private float moveForce = 10f;
    [SerializeField] private float maxSpeed  = 5f;

    private Rigidbody rb;

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
    }

    private void FixedUpdate()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical   = Input.GetAxis("Vertical");

        Vector3 force = new Vector3(horizontal, 0f, vertical) * moveForce;

        if (rb.velocity.magnitude < maxSpeed)
        {
            rb.AddForce(force, ForceMode.Force);
        }
    }
}

A few things to notice. First, we cache the Rigidbody in Start() rather than calling GetComponent<Rigidbody>() every frame component lookups have a non-trivial cost and should always be cached. Second, the physics code is in FixedUpdate(), not Update(). We will explain exactly why this matters in the next section.

Update vs FixedUpdate vs LateUpdate

Unity calls three different update methods on MonoBehaviours, and choosing the right one is critical for physics programming. Getting this wrong is one of the most common sources of bugs in beginner Unity projects.

Update()

Update() is called once per rendered frame. The interval between calls is not constant it varies with the rendering workload. You can get the elapsed time since the last frame with Time.deltaTime.

Use Update() for: reading input, updating UI, moving non-physics objects (Transform-based movement), checking conditions, playing sounds in response to events.

FixedUpdate()

FixedUpdate() is called at a fixed time step, completely independent of the rendered frame rate. The default fixed timestep is 0.02 seconds (50 times per second). You can change this in Edit > Project Settings > Time > Fixed Timestep.

The key insight is that the physics engine advances its simulation in FixedUpdate() steps. If you apply a force in Update(), you are applying it at irregular intervals sometimes twice before a physics step, sometimes zero times. This produces inconsistent, framerate-dependent behavior. Always put physics code in FixedUpdate().

LateUpdate()

LateUpdate() is called once per frame, but after all Update() calls have finished. This guaranteed ordering makes it ideal for anything that needs to react to what happened in Update most commonly, camera movement.

The Rule in Plain English

  • Input readingUpdate()
  • Physics forces and velocity changesFixedUpdate()
  • Camera followingLateUpdate()

The Flickering Problem and How to Solve It

Suppose you have a ball controlled by physics (Rigidbody + AddForce) and a camera that follows it. You write something like this in your camera script:

// WRONG causes flickering!
private void Update()
{
    transform.position = target.position + offset;
}

You press Play and immediately notice a jitter or flickering in the movement. The physics engine moves the ball in discrete FixedUpdate steps (every 0.02 seconds). The camera moves every rendered frame. These two timelines do not align, producing a momentary mismatch.

The fix is to move the camera in LateUpdate():

using UnityEngine;

public class SmoothFollow : MonoBehaviour
{
    [SerializeField] private Transform target;
    [SerializeField] private Vector3   offset = new Vector3(0f, 5f, -10f);
    [SerializeField] private float     smoothSpeed = 8f;

    private void LateUpdate()
    {
        Vector3 desiredPosition = target.position + offset;
        transform.position = Vector3.Lerp(
            transform.position,
            desiredPosition,
            smoothSpeed * Time.deltaTime
        );

        transform.LookAt(target);
    }
}

Force vs Impulse: The Four ForceModes

ForceMode determines how the force vector you pass to AddForce() is applied. There are four options, each suited to different gameplay scenarios.

ForceMode.Force

This is the default mode. The force is applied continuously over time, scaled by the physics timestep and the object's mass. A heavier object accelerates more slowly under the same force.

// Sustained upward thrust heavier objects rise slower
rb.AddForce(Vector3.up * thrustPower, ForceMode.Force);

ForceMode.Impulse

The force is applied as an instantaneous velocity change, scaled by the object's mass. The entire effect is applied in a single physics step.

// Jump called once on a button press, not every frame
rb.AddForce(Vector3.up * jumpStrength, ForceMode.Impulse);

ForceMode.Acceleration

Like Force, but mass is ignored. All objects receive the same acceleration regardless of how heavy they are.

// All objects accelerate identically, regardless of mass
rb.AddForce(Vector3.forward * boostAcceleration, ForceMode.Acceleration);

ForceMode.VelocityChange

Like Impulse, but mass is ignored. Directly adds the given vector to the Rigidbody's velocity in a single step.

// Immediately adds 5 m/s forward regardless of mass
rb.AddForce(Vector3.forward * 5f, ForceMode.VelocityChange);

Choosing the Right ForceMode

  • Sustained thrust, engine, gravity-likeForceMode.Force (respects mass)
  • Jump, explosion, bullet hitForceMode.Impulse (respects mass)
  • Consistent acceleration ignoring massForceMode.Acceleration
  • Direct velocity addition ignoring massForceMode.VelocityChange

2D Physics with Rigidbody2D

using UnityEngine;

public class PlatformerMovement2D : MonoBehaviour
{
    [SerializeField] private float moveForce  = 8f;
    [SerializeField] private float jumpForce  = 12f;
    [SerializeField] private float maxSpeed   = 4f;

    private Rigidbody2D rb;
    private bool isGrounded = false;

    private void Start()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    private void FixedUpdate()
    {
        float horizontal = Input.GetAxis("Horizontal");
        Vector2 force = new Vector2(horizontal * moveForce, 0f);

        if (Mathf.Abs(rb.velocity.x) < maxSpeed)
        {
            rb.AddForce(force, ForceMode2D.Force);
        }
    }

    private void Update()
    {
        if (Input.GetButtonDown("Jump") && isGrounded)
        {
            rb.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
            isGrounded = false;
        }
    }

    private void OnCollisionEnter2D(Collision2D col)
    {
        if (col.gameObject.CompareTag("Ground"))
            isGrounded = true;
    }

    private void OnCollisionExit2D(Collision2D col)
    {
        if (col.gameObject.CompareTag("Ground"))
            isGrounded = false;
    }
}

Exercise: The Flying Saucer

A hovering saucer controlled with WASD, moving by applying forces and rotating toward its direction of movement.

using UnityEngine;

public class FlyingSaucer : MonoBehaviour
{
    [Header("Movement")]
    [SerializeField] private float thrustForce     = 15f;
    [SerializeField] private float maxSpeed        = 8f;
    [SerializeField] private float dragCoefficient = 2f;

    [Header("Rotation")]
    [SerializeField] private float rotationSpeed = 5f;

    private Rigidbody rb;
    private Vector3   inputDirection;

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
    }

    private void Update()
    {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        inputDirection = new Vector3(h, 0f, v);
    }

    private void FixedUpdate()
    {
        if (inputDirection.magnitude > 0.1f && rb.velocity.magnitude < maxSpeed)
        {
            rb.AddForce(inputDirection.normalized * thrustForce, ForceMode.Force);
        }

        Vector3 flatVelocity = new Vector3(rb.velocity.x, 0f, rb.velocity.z);
        rb.AddForce(-flatVelocity * dragCoefficient, ForceMode.Force);
    }

    private void LateUpdate()
    {
        if (inputDirection.magnitude > 0.1f)
        {
            Quaternion targetRotation = Quaternion.LookRotation(inputDirection.normalized);
            transform.rotation = Quaternion.RotateTowards(
                transform.rotation,
                targetRotation,
                rotationSpeed * Time.deltaTime * 100f
            );
        }
    }
}

The AddTorque() Method

Just as AddForce() applies a linear force to the center of mass, AddTorque() applies a rotational force a torque around an axis.

rb.AddTorque(Vector3 torque, ForceMode mode = ForceMode.Force);

AddRelativeTorque() works the same way but in local space:

rb.AddRelativeTorque(Vector3 localTorque, ForceMode mode = ForceMode.Force);

Example: Spinning a Wheel with Physics

using UnityEngine;

public class PhysicsWheel : MonoBehaviour
{
    [SerializeField] private float torqueStrength = 20f;
    [SerializeField] private float maxAngularVelocity = 15f;

    private Rigidbody rb;

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
        rb.maxAngularVelocity = maxAngularVelocity;
    }

    private void FixedUpdate()
    {
        float input = Input.GetAxis("Vertical");
        rb.AddRelativeTorque(Vector3.right * input * torqueStrength, ForceMode.Force);
    }
}

AddForceAtPosition()

// Push at the right edge causes translation AND rotation
Vector3 rightEdge = transform.position + transform.right * 2f;
rb.AddForceAtPosition(Vector3.forward * pushStrength, rightEdge, ForceMode.Force);

Exercise: Roll-A-Ball

Ball Movement Script

using UnityEngine;
using UnityEngine.UI;

public class RollABall : MonoBehaviour
{
    [Header("Physics")]
    [SerializeField] private float rollForce = 12f;
    [SerializeField] private float maxSpeed  = 6f;

    [Header("UI")]
    [SerializeField] private Text scoreText;

    private Rigidbody rb;
    private int score = 0;
    private Vector3 inputForce;

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
        UpdateScoreUI();
    }

    private void Update()
    {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        inputForce = new Vector3(h, 0f, v) * rollForce;
    }

    private void FixedUpdate()
    {
        if (rb.velocity.magnitude < maxSpeed)
        {
            rb.AddForce(inputForce, ForceMode.Force);
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Pickup"))
        {
            other.gameObject.SetActive(false);
            score++;
            UpdateScoreUI();

            if (score >= 12)
            {
                Debug.Log("You win!");
            }
        }
    }

    private void UpdateScoreUI()
    {
        scoreText.text = "Score: " + score;
    }
}

Rotating Pickup Script

using UnityEngine;

public class PickupRotate : MonoBehaviour
{
    [SerializeField] private float rotateSpeed = 90f;

    private void Update()
    {
        transform.Rotate(Vector3.up, rotateSpeed * Time.deltaTime);
    }
}

Putting It All Together: Physics Design Principles

  • Cache your Rigidbody. Always store the reference in Start(). Never call GetComponent<Rigidbody>() in Update or FixedUpdate.
  • Physics code goes in FixedUpdate. No exceptions. Even if it "works" in Update on your machine, it will break on machines with different frame rates.
  • Camera following goes in LateUpdate. This eliminates jitter when following physics objects.
  • Use ForceMode deliberately. Force and Impulse respect mass; Acceleration and VelocityChange ignore it. Match your mode to your gameplay intent.
  • AddTorque for rotational forces. Use AddRelativeTorque when the axis is in local space. Remember to raise maxAngularVelocity if needed.
  • AddForceAtPosition for off-center forces. This produces both translation and rotation, simulating realistic physical interactions.

In the next chapter we will look at raycasting casting invisible rays through the physics world to detect collisions, implement shooting, and solve ground-detection problems.

Unity3D Physics AddForce Rigidbody ForceMode FixedUpdate C#

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