You already know how to give an object a Rigidbody and a Collider and watch Unity make it fall, bounce, and stop against walls. This chapter is about the next step: wiring that physics simulation into actual gameplay -- picking up a coin, taking damage from lava, getting knocked back by an explosion, shooting a raycast down a hallway. Physics code that looks correct in isolation still causes some of the most confusing bugs in a Unity project -- jittery movement, missed pickups, frame-rate-dependent jump heights -- and almost all of them trace back to one of a handful of rules covered in this chapter.
Unity ships with a full physics engine (a separate system that simulates rigid bodies, gravity, and collisions -- Unity's default 3D engine is called PhysX) running underneath your scripts. It has its own internal clock and its own loop, completely separate from the rest of your game code. Your job is not to compute physics yourself -- it is to talk to the physics engine cleanly: tell it what you want (a force, a target position), and listen to what it reports back (a collision, an overlap, a new position).
Two components put a GameObject under the physics engine's control:
Rigidbody -- marks an object as something the physics engine should simulate (gravity, momentum, and collisions all apply to it).Collider -- defines the shape the physics engine uses for contact tests (a box, sphere, capsule, or mesh shape, separate from what the object looks like visually).A Rigidbody can be one of three kinds, and mixing them up is a common source of confusion:
transform.position directly.isKinematic = true) -- ignores forces and gravity completely. You move it yourself, usually with Rigidbody.MovePosition/MoveRotation, and the physics engine still reports collisions and triggers against it. Good for moving platforms and scripted doors.Rigidbody at all, just a Collider) -- never moves. The cheapest option for walls, floors, and terrain.The cleanest way to think about the relationship: gameplay code sends intent to the physics engine (a force, an impulse, a target position), and the physics engine sends facts back (where things ended up, what touched what). Fighting this -- writing directly to transform.position on a dynamic Rigidbody every frame -- breaks the simulation, because now two systems think they own the same value.
Here is the mistake in code form, and the fix:
public class BadMover : MonoBehaviour
{
public float speed = 5f;
// BAD: this object also has a non-kinematic Rigidbody attached.
// Overwriting transform.position every frame ignores the physics
// engine completely -- the object can clip straight through walls,
// and any collision that does happen looks jittery, because the
// physics engine and this script are fighting over the same value.
void Update()
{
transform.position += Vector3.forward * speed * Time.deltaTime;
}
}
public class GoodMover : MonoBehaviour
{
public float speed = 5f;
private Rigidbody rb;
void Awake()
{
rb = GetComponent<Rigidbody>();
}
// GOOD: MovePosition hands the move to the physics engine. It sweeps
// the body through space between steps, so collisions along the way
// are still detected correctly, and it belongs in FixedUpdate --
// more on that split in Section 4.
void FixedUpdate()
{
Vector3 target = rb.position + Vector3.forward * speed * Time.fixedDeltaTime;
rb.MovePosition(target);
}
}
Expected result: GoodMover moves forward at a steady speed and still stops correctly against a wall's collider, because the physics engine is the one performing the move and can detect the wall along the way. BadMover moves at the same speed on an empty floor, but drives straight through a thin wall if it is moving fast enough between frames, because it never asked the physics engine to check.
Rigidbody, never write to its transform.position or transform.rotation directly. Use AddForce, AddTorque, MovePosition, or linearVelocity instead, and let the physics engine own the Transform.Every Collider has a checkbox in the Inspector called Is Trigger. It changes what kind of collider you have:
isTrigger = false, the default) -- physically blocks other colliders. Two solid colliders touching push each other apart (or stop movement), and the physics engine calls OnCollisionEnter/OnCollisionStay/OnCollisionExit.isTrigger = true) -- detects overlap but does not physically block anything. Objects pass straight through it, and the physics engine calls OnTriggerEnter/OnTriggerStay/OnTriggerExit instead.This is the tool for exactly the cases you would expect: a coin, a health pickup, a damage zone, an invisible "you entered this room" volume -- all things that should know something touched them without physically stopping it.
For two colliders to generate any event at all (collision or trigger), at least one of the two objects needs a Rigidbody. A trigger volume that only has a Collider and no Rigidbody still detects a player who has a Rigidbody walking into it -- the reverse also works. Two plain colliders with no Rigidbody anywhere never send events to each other; the physics engine does not bother testing static-against-static pairs for events, since neither one can move.
Collider, placed against a player that also only has a Collider and no Rigidbody, never fires OnTriggerEnter. If nothing seems to detect the player, check that the player (or the trigger) actually has a Rigidbody attached.When to use a solid collider: walls, floors, characters, crates -- anything that should physically stop or push other objects. When to use a trigger: pickups, damage zones, checkpoints, level-transition volumes, "is the player near this thing" sensors -- anything that should only know, not block.
Unity calls three methods on a MonoBehaviour attached to a trigger collider (or to the other collider involved), automatically, whenever an overlap starts, continues, or ends:
OnTriggerEnter(Collider other) -- called once, the frame the two colliders start overlapping.OnTriggerStay(Collider other) -- called every physics step while they keep overlapping.OnTriggerExit(Collider other) -- called once, the frame they stop overlapping.A pickup only needs OnTriggerEnter:
using UnityEngine;
public class CoinPickup : MonoBehaviour
{
public int value = 10;
void OnTriggerEnter(Collider other)
{
if (other.TryGetComponent(out PlayerScore score))
{
score.Add(value);
Debug.Log("Coin collected, +" + value);
Destroy(gameObject);
}
}
}
Expected output: the instant the player's collider touches the coin's trigger collider, the console prints Coin collected, +10 and the coin disappears. If something without a PlayerScore component clips the trigger instead (a stray enemy, say), nothing happens -- the TryGetComponent check quietly skips it.
A damage zone needs continuous detection, so it uses OnTriggerStay instead:
using UnityEngine;
public class LavaPit : MonoBehaviour
{
public float damagePerSecond = 10f;
void OnTriggerStay(Collider other)
{
if (other.TryGetComponent(out Health health))
{
// OnTriggerStay fires once per physics step, so scale the
// damage by the fixed step's length to get a steady
// per-second rate no matter how often the step runs.
float damageThisStep = damagePerSecond * Time.fixedDeltaTime;
health.TakeDamage(damageThisStep);
}
}
void OnTriggerExit(Collider other)
{
if (other.TryGetComponent(out Health health))
{
Debug.Log(other.name + " left the lava");
}
}
}
Expected output: while the player stands in the lava, Health.TakeDamage is called on every physics step with a small fractional amount, adding up to damagePerSecond hit points lost per real second. The moment the player steps out, OnTriggerExit prints Player left the lava once, and the steady damage stops.
TryGetComponent is the safer habit over GetComponent followed by a null check -- it returns a plain bool and reads naturally inside an if, and it avoids the small overhead of comparing a returned reference against null separately.OnTriggerStay without noticing it fires every single physics step, potentially 50 times a second, for every object still overlapping. A small per-step calculation like the damage tick above is fine; instantiating a new particle system every step is not.Unity's game loop calls two very different kinds of "once per X" methods, and mixing them up is one of the most common physics bugs beginners write:
Update() -- runs once per rendered frame. How often that is depends on frame rate, which changes constantly (a busy scene might drop to 30 fps, an empty one might hit 300 fps).FixedUpdate() -- runs at a fixed rate, set by Time.fixedDeltaTime (0.02 seconds, or 50 times a second, by default -- changeable in Project Settings > Time). This rate never changes with frame rate; the physics engine only ever advances in these fixed-size steps.The rule that follows from this: read input in Update (a fast frame rate should never make you miss a key press -- Input.GetKeyDown is designed to be polled once per rendered frame), and apply forces, impulses, or velocity changes in FixedUpdate (so the physics engine always receives exactly the amount you intend, once per physics step, regardless of frame rate).
Here is the bug in its most common shape -- a thruster that pushes a ship forward while a key is held:
public class BuggyThruster : MonoBehaviour
{
public Rigidbody rb;
public float thrust = 20f;
// BUG: AddForce is called once per Update -- once per RENDERED
// frame. At 240 fps, Update runs about 5 times for every single
// FixedUpdate (at the default 50 Hz). That means roughly 5 times
// as much force gets queued up per physics step as at 48 fps,
// where Update barely keeps up with FixedUpdate. Thrust ends up
// depending on the player's frame rate, not on the thrust value.
void Update()
{
if (Input.GetKey(KeyCode.W))
{
rb.AddForce(transform.forward * thrust, ForceMode.Force);
}
}
}
public class FixedThruster : MonoBehaviour
{
public Rigidbody rb;
public float thrust = 20f;
private bool thrusting;
// Only READ input here. Polling once per rendered frame is exactly
// right for input -- it is cheap, and GetKey/GetKeyDown are built
// to be checked this often.
void Update()
{
thrusting = Input.GetKey(KeyCode.W);
}
// Only APPLY physics here. This runs at a fixed rate, so the force
// added per physics step is now the same no matter how fast the
// game is rendering.
void FixedUpdate()
{
if (thrusting)
{
rb.AddForce(transform.forward * thrust, ForceMode.Force);
}
}
}
Expected result: FixedThruster reaches the same top speed after the same number of real-world seconds whether the game runs at 30 fps or 300 fps. BuggyThruster visibly accelerates faster on a high-end machine than a low-end one, running the exact same script.
FixedUpdate instead of Update. At a low frame rate, FixedUpdate can run zero times in a rendered frame or several times in a row to catch up, so Input.GetKeyDown (true for exactly one call) can be checked on the wrong call and miss the press, or be checked twice and double-fire. Poll input in Update, store it in a field, and read that field in FixedUpdate.FixedUpdate, Time.deltaTime is automatically equal to Time.fixedDeltaTime -- Unity sets it for you during that call. Using Time.fixedDeltaTime explicitly, as in the lava example, is still good habit, since it makes the intent obvious to anyone reading the code later.Every GameObject has a layer (a numbered category, up to 32 of them, named in Project Settings > Tags and Layers -- things like Player, Enemy, EnemyBullet, Scenery). Layers alone do nothing by themselves, but two systems read them: rendering (which camera sees which layer) and physics.
Project Settings > Physics > Layer Collision Matrix is a big checkbox grid: one row and one column per layer. A checked box means colliders on those two layers are allowed to generate collision or trigger events with each other; an unchecked box means the physics engine skips testing that pair entirely -- not just ignoring the result, but never even doing the work.
Setting this up once in Project Settings is usually the right call -- it is a one-time, whole-project rule. Two extra tools help from code:
using UnityEngine;
public class SetupLayers : MonoBehaviour
{
void Awake()
{
// Same effect as unchecking a box in the matrix, done at runtime.
int enemyLayer = LayerMask.NameToLayer("Enemy");
int enemyBulletLayer = LayerMask.NameToLayer("EnemyBullet");
Physics.IgnoreLayerCollision(enemyLayer, enemyBulletLayer, true);
// Assigning a layer to a spawned object, in code.
gameObject.layer = LayerMask.NameToLayer("EnemyBullet");
}
}
When to use it: any time two categories of object should never physically interact, or should never even be checked for events -- friendly fire off, bullets ignoring their own shooter, ghosts walking through walls, a "detection only" layer used purely for raycasts. This also directly reduces the physics engine's workload (Section 12 comes back to this), since ignored pairs are never even tested.
OnCollisionEnter/OnTriggerEnter pairs. It does not automatically affect raycasts or overlap queries -- those need their own LayerMask parameter, covered next.Rigidbody.AddForce(Vector3 force, ForceMode mode) is the main tool for pushing a dynamic Rigidbody. The second argument, ForceMode, changes what the numbers mean -- mixing these up is a common source of "why is my force too weak or too strong" confusion:
"Uses mass" means a heavier Rigidbody (higher rb.mass) needs more force to move by the same amount -- matching real-world intuition (a shove barely moves a truck but sends a shopping cart flying). "Ignores mass" means every object gets the exact same acceleration or speed change, useful when you want predictable, tunable game feel rather than physical realism.
A continuous thruster uses ForceMode.Force, applied every FixedUpdate while active (the same pattern from Section 4):
void FixedUpdate()
{
if (thrusting)
{
rb.AddForce(transform.forward * thrust, ForceMode.Force);
}
}
Expected result: the ship accelerates smoothly, gaining speed the longer thrusting stays true, and a heavier ship (rb.mass increased) accelerates more slowly under the exact same thrust value, because ForceMode.Force respects mass -- just like a real rocket engine on a heavier rocket.
AddForce with no second argument defaults to ForceMode.Force. Writing it explicitly, as above, saves the next reader (often you, in six months) from having to look up the default.Not every physics question needs a Rigidbody moving through the world. Sometimes you just want to ask the physics engine a question right now: "what is directly in front of this gun?" or "what is standing near this explosion?" These one-shot questions are called queries, and they read the current state of every collider in the scene without simulating anything.
A raycast (firing an invisible, infinitely thin line through the scene and asking what it hits first) is the standard way to implement hitscan shooting -- a gun that hits instantly, with no visible bullet travel time:
using UnityEngine;
public class Gun : MonoBehaviour
{
public float range = 100f;
public int damage = 25;
public LayerMask hittableLayers; // set in the Inspector: Enemy + Scenery
public void Fire()
{
Ray ray = new Ray(transform.position, transform.forward);
// Physics.Raycast returns true the instant it finds something,
// and fills "hit" with details -- what it hit, where, how far.
if (Physics.Raycast(ray, out RaycastHit hit, range, hittableLayers))
{
Debug.Log("Hit " + hit.collider.name + " at distance " + hit.distance);
if (hit.collider.TryGetComponent(out Health targetHealth))
{
targetHealth.TakeDamage(damage);
}
}
else
{
Debug.Log("Shot missed everything");
}
}
}
Expected output: aiming at an enemy 12 units away and calling Fire() prints Hit Enemy at distance 12 and reduces that enemy's health by 25. Aiming at open sky prints Shot missed everything. Passing hittableLayers (excluding the Player layer) means the raycast physically cannot report hitting the player who fired it, no matter where the ray starts.
An overlap query asks a different question: "what colliders are currently inside this shape?" -- useful for area detection, like an alarm that notices the player nearby:
using UnityEngine;
public class ProximityAlarm : MonoBehaviour
{
public float radius = 5f;
public LayerMask playerLayer;
void FixedUpdate()
{
// Physics.OverlapSphere returns every collider touching the
// sphere -- this object does not even need a Collider itself.
Collider[] nearby = Physics.OverlapSphere(transform.position, radius, playerLayer);
if (nearby.Length > 0)
{
Debug.Log("Player detected, colliders in range: " + nearby.Length);
}
}
}
Expected output: the console prints Player detected, colliders in range: 1 every physics step the player stays within 5 units, and stops printing once they walk away.
LayerMask to raycasts and overlap queries when you know what you are looking for. Without one, they test against every collider in the scene, including ones you never meant to hit, and the results are noisier and slower to filter afterward.OnTriggerEnter/OnCollisionEnter and queries like Physics.Raycast answer different questions at different times. Trigger and collision events tell you about things that are about to happen or just happened during a physics step. A raycast tells you about the scene's state right now, this instant, whenever you call it -- useful for an action the player triggers on demand, like firing a gun.A single, instant push -- a melee hit, a bullet impact, a bounce pad -- uses ForceMode.Impulse instead of the continuous ForceMode.Force from Section 6, applied exactly once:
void ApplyKnockback(Rigidbody target, Vector3 direction, float strength)
{
// Impulse: an instant velocity change that still respects mass,
// so a heavy enemy flies back less than a light one for the
// same strength value -- feels right without extra tuning.
target.AddForce(direction.normalized * strength, ForceMode.Impulse);
}
An explosion needs to push every nearby Rigidbody at once, with force that fades by distance. Rigidbody.AddExplosionForce handles that falloff for you, combined with the Physics.OverlapSphere query from Section 7 to find everything in range:
using UnityEngine;
public class Grenade : MonoBehaviour
{
public float explosionForce = 700f;
public float explosionRadius = 5f;
public float upwardsModifier = 0.5f;
public LayerMask affectedLayers;
public void Explode()
{
Collider[] hits = Physics.OverlapSphere(transform.position, explosionRadius, affectedLayers);
foreach (Collider hit in hits)
{
if (hit.attachedRigidbody != null)
{
hit.attachedRigidbody.AddExplosionForce(
explosionForce,
transform.position,
explosionRadius,
upwardsModifier,
ForceMode.Impulse);
}
}
Destroy(gameObject);
}
}
The parameters, in order: explosionForce (the strength at the very center of the blast), explosionPosition (where the blast happens -- objects farther from this point get less force, down to zero at explosionRadius), explosionRadius (how far the blast reaches), and upwardsModifier (an artificial extra push straight up, which makes debris and ragdolls fly up and outward instead of skimming flat along the ground -- real explosions do this too, but the parameter lets you exaggerate it for a better-looking effect).
Expected result: calling Explode() near three crates finds all three with OverlapSphere, and each one flies outward and slightly upward, with a crate right next to the grenade flying much farther than one near the edge of explosionRadius -- the falloff happens automatically inside AddExplosionForce, you never compute distance yourself.
affectedLayers to exclude Scenery if walls and terrain have Rigidbodies you never want flung around by a nearby explosion, or to exclude Player if explosions should only ever damage enemies. This is the Layer Collision Matrix idea from Section 5 applied to a query instead of a collision pair.You now have two different tools that can make something move: adding a force or impulse, or setting rb.linearVelocity directly (overwriting the object's current velocity outright -- this property was called velocity in older Unity versions). A jump is the clearest example of when each one fits, because both look "correct" in a quick test and only reveal their difference in an edge case.
public class JumpWithForce : MonoBehaviour
{
public Rigidbody rb;
public float jumpForce = 8f;
public void Jump()
{
// Adds ON TOP of whatever vertical velocity already exists.
rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
}
}
public class JumpWithVelocity : MonoBehaviour
{
public Rigidbody rb;
public float jumpSpeed = 8f;
public void Jump()
{
// OVERWRITES vertical velocity outright.
Vector3 v = rb.linearVelocity;
v.y = jumpSpeed;
rb.linearVelocity = v;
}
}
Try both while the character is already falling off a ledge (moving downward, rb.linearVelocity.y is negative). JumpWithForce adds the jump impulse to that negative value, so the jump height depends on how fast the character was already falling -- sometimes barely leaving the ground, sometimes launching unexpectedly high. JumpWithVelocity ignores whatever the character was doing and always sets y to exactly jumpSpeed -- the jump reaches the same height every single time.
When to add force or impulse: anything that should look and feel physically real, where varying with mass and existing motion is exactly the point -- explosions, knockback, ragdolls, environmental pushes, a boulder rolling downhill and picking up speed.
When to set velocity directly: tightly controlled, player-facing actions where consistency matters more than realism -- a platformer jump that must reach the same height every time, a dash move with an exact fixed speed, clamping a Rigidbody so it never exceeds a max speed. Most responsive, "tight-feeling" arcade-style character controllers lean heavily on setting velocity directly rather than accumulating forces.
AddForce for player movement and then wondering why acceleration feels sluggish or inconsistent, especially at low mass or while already moving. If the goal is "move at exactly this speed, right now," set linearVelocity. Save AddForce for cases where gradual, mass-aware acceleration is the actual goal.Recall from Section 4 that FixedUpdate runs at a fixed rate (50 Hz by default) while rendering can run much faster (144 Hz, 240 Hz, or uncapped). A Rigidbody's Transform only actually changes on a physics step. If the screen redraws more often than that, the same position gets drawn several frames in a row, then jumps to the next physics position -- visible as a slight stutter, especially on fast-moving objects.
The fix is the Rigidbody.interpolation setting, either in the Inspector or in code:
void Awake()
{
Rigidbody rb = GetComponent<Rigidbody>();
rb.interpolation = RigidbodyInterpolation.Interpolate;
}
RigidbodyInterpolation.None (default) -- no smoothing. Cheapest, but can look stuttery at high frame rates relative to the fixed timestep.RigidbodyInterpolation.Interpolate -- smooths by rendering slightly behind the true physics state, blending between the last two known positions. Always looks smooth, adds a tiny, usually unnoticeable delay. The right default for player-controlled and camera-followed objects.RigidbodyInterpolation.Extrapolate -- guesses ahead using current velocity, instead of blending behind. No added delay, but the guess can be visibly wrong for a frame when velocity changes suddenly (a bounce, a sharp turn). Use sparingly, and never on something the player needs pixel-precise feedback from.Expected result: a ball rolling across the floor with Interpolate set looks perfectly smooth at 144 fps even though it is still only physically simulated 50 times a second. The same ball with None set visibly steps between positions on a very fast monitor, especially noticeable on slow camera pans.
None) on background clutter, debris, or anything off-screen -- interpolation costs a small amount of extra CPU per Rigidbody, and objects nobody is closely watching do not need it.A simulation is deterministic when the exact same starting state and the exact same sequence of inputs always produce the exact same result, every single time it runs. Two features depend on this directly:
Two requirements make this possible, and both connect directly to earlier sections:
Time.fixedDeltaTime, from Section 4), never a variable per-frame step. Floating-point math gives slightly different results for different step sizes, so a variable step makes the exact same inputs produce slightly different outcomes on different runs.Dictionary's iteration order is not guaranteed to stay the same across runs or platforms; looping over one to apply gameplay logic can silently change the outcome. Use an ordered collection (a List, or an array sorted by a stable ID) for anything that affects the simulation.
[System.Serializable]
public struct InputFrame
{
public int tick;
public float horizontal;
public bool jumpPressed;
}
public class ReplayRecorder : MonoBehaviour
{
private List<InputFrame> recordedFrames = new List<InputFrame>();
private int currentTick;
void FixedUpdate()
{
InputFrame frame = new InputFrame
{
tick = currentTick,
horizontal = Input.GetAxisRaw("Horizontal"),
jumpPressed = Input.GetButtonDown("Jump")
};
recordedFrames.Add(frame);
ApplyInput(frame);
currentTick++;
}
void ApplyInput(InputFrame frame)
{
// The SAME method drives live play and replay playback. As long
// as this only ever runs from FixedUpdate (fixed step, fixed
// order), replaying the same recordedFrames list reproduces the
// exact same run.
}
}
Random.Range with no fixed seed picks a different sequence every run, which breaks determinism immediately -- a replay or a lockstep peer that used a different random sequence produces a different result from the same inputs. Seed the RNG explicitly (Random.InitState(seed)) with a value that is itself part of the recorded or synced state.One honest limitation worth knowing: Unity's built-in PhysX engine is not guaranteed to produce bit-for-bit identical results across different CPU architectures or platforms, because floating-point rounding can differ at that level. A single-machine replay system (the case above) is reliable, since the same machine runs both the recording and the playback. True cross-platform lockstep multiplayer with full 3D physics is hard enough that many competitive RTS and fighting games either restrict lockstep to simpler, custom fixed-point simulations, or only rely on determinism within one platform, rather than trusting a general-purpose physics engine across every machine.
Every physics step, the engine has to do real work for every awake Rigidbody: find which pairs of colliders might be touching (broadphase), check those candidate pairs precisely (narrowphase), and resolve any actual contacts (the solver). FixedUpdate has a fixed amount of real time to finish its work before the next step is due -- too many active bodies and the physics step itself starts taking longer than the time budget allows, which drags the whole game down even if nothing looks visually complex.
A few concrete ways to keep the count and the cost down:
AddForce or setting velocity). Do not fight this by touching a Rigidbody every frame when nothing actually needs to change; that keeps waking it up for no reason.BoxCollider, SphereCollider, or CapsuleCollider is far cheaper to test than a MeshCollider, especially a non-convex one. Wrap detailed meshes in a simple primitive (or a small handful of them) instead of colliding against the exact visual geometry.
using System.Collections.Generic;
using UnityEngine;
public class DebrisSpawner : MonoBehaviour
{
public GameObject debrisPrefab;
public int maxActiveDebris = 30;
private readonly Queue<GameObject> activeDebris = new Queue<GameObject>();
public void SpawnDebris(Vector3 position)
{
if (activeDebris.Count >= maxActiveDebris)
{
// Cap the number of active physics bodies instead of letting
// every explosion add more debris forever.
GameObject oldest = activeDebris.Dequeue();
Destroy(oldest);
}
GameObject piece = Instantiate(debrisPrefab, position, Quaternion.identity);
activeDebris.Enqueue(piece);
}
}
Expected result: repeated explosions keep spawning debris chunks, but the active Rigidbody count never grows past maxActiveDebris -- the oldest piece is removed before a new one is added, keeping the physics step's workload bounded no matter how long the player keeps blowing things up.
Solver Iterations (how many passes the solver makes resolving contacts -- higher is more accurate and more expensive) and Max Allowed Timestep (a safety cap so a huge frame-rate stall does not force physics to try to simulate a giant leap in time all at once). Both are worth knowing about, but only worth tuning after you have actually measured a physics-related slowdown with the Profiler.isTrigger = false; physically blocks other colliders and raises OnCollisionEnter/Stay/Exit.isTrigger = true; detects overlap without blocking, and raises OnTriggerEnter/Stay/Exit instead.Time.fixedDeltaTime), independent of frame rate; the correct place to apply forces, impulses, and velocity changes.AddForce that decides whether the push is continuous or instant, and whether it respects the target's mass.ForceMode.Impulse, that respects mass.W is held. It "works" in a quick test, but its top speed after 5 seconds turns out different on a fast machine than on a slow one. Explain why, using what Section 4 taught about Update and FixedUpdate, then rewrite it correctly.
public class Hovercraft : MonoBehaviour
{
public Rigidbody rb;
public float thrust = 15f;
void Update()
{
if (Input.GetKey(KeyCode.W))
{
rb.AddForce(transform.forward * thrust, ForceMode.Force);
}
}
}
AddForce is called from Update, which runs once per rendered frame -- a variable rate. On a fast machine, Update runs many more times per second than the fixed physics rate (50 Hz by default), so far more force gets queued up per physics step than on a slow machine, where Update barely outpaces FixedUpdate. The hovercraft ends up accelerating faster purely because the game is rendering faster, which has nothing to do with the thrust value. The fix is to only read input in Update and only touch the Rigidbody in FixedUpdate:
public class Hovercraft : MonoBehaviour
{
public Rigidbody rb;
public float thrust = 15f;
private bool thrustHeld;
void Update()
{
thrustHeld = Input.GetKey(KeyCode.W);
}
void FixedUpdate()
{
if (thrustHeld)
{
rb.AddForce(transform.forward * thrust, ForceMode.Force);
}
}
}
Now AddForce is called at most once per fixed physics step, no matter how many times Update runs in between, so the hovercraft reaches the same speed after the same number of real-world seconds regardless of frame rate.
Is Trigger checked, and explain why in one sentence. Then write the HealingPotion script: it should heal the player 20 HP, log a message, and destroy itself the moment the player touches it.The wall should be a solid collider (isTrigger = false) -- it needs to physically stop the player from walking through it. The healing potion should be a trigger (isTrigger = true) -- it needs to detect the player without blocking their movement. The lava pit should also be a trigger (isTrigger = true) -- the player needs to be able to walk into it (even if that is a bad idea) so the game can detect the overlap and apply damage; a solid collider there would just act like an invisible wall and never let the player experience the lava at all.
using UnityEngine;
public class HealingPotion : MonoBehaviour
{
public int healAmount = 20;
void OnTriggerEnter(Collider other)
{
if (other.TryGetComponent(out Health health))
{
health.Heal(healAmount);
Debug.Log("Healing potion used, +" + healAmount + " HP");
Destroy(gameObject);
}
}
}
This follows the same shape as the CoinPickup example from Section 3: check for the right component with TryGetComponent, apply the effect, then destroy the pickup so it cannot be collected twice.
Grenade script whose Explode() method finds every Rigidbody within a 6-unit radius using an overlap query, but should only affect objects on an Enemies layer (never the player or scenery), knocking each one back with AddExplosionForce using a force of 500 and an upwards modifier of 0.3. Expose the affected layer as a LayerMask field so it can be set in the Inspector.
using UnityEngine;
public class Grenade : MonoBehaviour
{
public float explosionForce = 500f;
public float explosionRadius = 6f;
public float upwardsModifier = 0.3f;
public LayerMask enemyLayer; // set to "Enemies" only in the Inspector
public void Explode()
{
Collider[] hits = Physics.OverlapSphere(transform.position, explosionRadius, enemyLayer);
foreach (Collider hit in hits)
{
if (hit.attachedRigidbody != null)
{
hit.attachedRigidbody.AddExplosionForce(
explosionForce,
transform.position,
explosionRadius,
upwardsModifier,
ForceMode.Impulse);
}
}
Destroy(gameObject);
}
}
The enemyLayer mask does the filtering work: Physics.OverlapSphere only returns colliders on that layer in the first place, so the player and scenery are never even in the hits array -- there is no need for an extra tag check afterward. This is the same technique from Section 8, combined with the layer filtering from Section 5, applied together.