C3. MonoBehaviour
Learning Outcomes
- Describe the role of
MonoBehaviour
as the foundation for Unity scripts. Before class, review its purpose and familiarize yourself with common lifecycle methods likeStart()
andUpdate()
.- Differentiate between initialization and setup methods in Unity. As preparation, study
Awake()
,Start()
,OnEnable()
,Reset()
, andOnValidate()
by creating a script that logs each method to observe their execution order.- Implement frame update loops for continuous behavior. Ahead of the session, write
Update()
,FixedUpdate()
, andLateUpdate()
methods to move or rotate a GameObject usingTime.deltaTime
and log when each method runs.- Respond to physics collisions and trigger events. For your pre-work, create GameObjects with colliders and write scripts that log messages when collisions or trigger entries occur.
- Detect visibility changes of GameObjects in the scene. In preparation, use
OnBecameVisible()
andOnBecameInvisible()
to log when an object enters or leaves the camera’s view.- Handle teardown events for cleanup in Unity. Before arriving, use
OnDisable()
andOnDestroy()
in a script to log when a GameObject is disabled or destroyed at runtime.
What Is MonoBehaviour?
MonoBehaviour is the foundational base class from which nearly all Unity scripts derive. It serves as the essential bridge between your custom C# code and Unity’s powerful engine, enabling your scripts to participate in the engine’s internal lifecycle, event system, and component-based architecture. MonoBehaviour
simplifies development by providing a set of built-in methods and message handlers—like Start()
, Update()
, and OnCollisionEnter()
—so you don’t have to build core behavior management from scratch. MonoBehaviour
connects your C# code to the GameObject system in Unity. When you create a script and attach it to a GameObject in your scene:
- Unity treats the script as a Component—just like a
Rigidbody
,Collider
, orAudio Source
. - Your script gains access to Unity’s game loop and lifecycle.
- You can define behaviors and interactions that run automatically without needing to call them manually.
It is important to remember that
MonoBehaviour
scripts are always Components. Therefore, they must be attached to an active GameObject to work. Sometimes, it is better to attach custom scripts to an empty GameObject—such as a “Manager” (e.g., TeleportationManager or InteractionManager)—to keep your scene clean, organized, and easier to maintain. Review this Unity documentation to learn more aboutMonoBehaviour
.
The MonoBehaviour
Script
When you create a new script in Unity, it typically inherits from MonoBehaviour
by default. This inheritance grants access to Unity’s lifecycle messages—such as Start()
, Update()
, and many others—that are automatically called by the engine at specific times during the game’s execution. These hooks make it easy to define behavior that reacts to events like object initialization, frame updates, collisions, and visibility changes. You can attach a script to a GameObject by
dragging it onto a GameObject in the Hierarchy
, selecting the GameObject, then dragging the script into the Inspector
, or using the Component > Scripts
submenu. Key features of MonoBehaviour
script include:
-
Lifecycle Integration and Event Handling:
MonoBehaviour
provides built-in lifecycle methods (Awake
,Start
,Update
,FixedUpdate
, etc.) that Unity automatically calls at the correct time. This eliminates manual polling or boilerplate initialization logic and ensures your code runs when the GameObject and its components are ready. For example, in XFactory’s logistics station, you can useStart()
to initialize drone pathfinding logic, whileUpdate()
continuously monitors their battery level or navigational state:public class DroneController : MonoBehaviour { void Start() { Debug.Log("Drone initialized and ready for pathfinding."); } void Update() { Debug.Log("Monitoring drone navigation and battery status..."); } }
-
Component-Based Architecture: Unity’s design is centered on GameObjects and their components. A
MonoBehaviour
script is a custom component that defines behavior and can be attached to any GameObject. This modular structure promotes reusability, scalability, and easier debugging. For example, attach aForkliftController
script to a forklift GameObject in the logistics station to give it autonomous movement behavior during package transport.using UnityEngine; public class ForkliftController : MonoBehaviour { [SerializeField] private float moveSpeed = 5f; // Editable in the Inspector but not directly from other scripts void Update() { if (Input.GetKey(KeyCode.M)) { transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime); } } }
The class is called
MonoBehaviour
because it represents a behaviour (a script component that defines how a GameObject acts) and it was originally powered by the Mono runtime, which Unity used to run C# across platforms. In other words, aMonoBehaviour
is the foundational way you give GameObjects custom behaviours in Unity, while the “Mono” part reflects Unity’s early adoption of the Mono framework for scripting.
Inherited Members
The MonoBehaviour class inherits a wide range of useful properties and methods from its parent classes (Behaviour
, Component
, and Object
). These inherited members give your scripts direct access to the GameObject they are attached to, making it easier to interact with components, control object behavior, and respond to gameplay events. Key inherited members include:
gameObject
: Grants direct access to the GameObject this script is attached to. You can use it to enable/disable the object, access or modify components, or manage its hierarchy and state.transform
: Provides access to the GameObject’sposition
,rotation
, andscale
in the scene. Crucial for movement, orientation, and spatial logic.name
: Thename
of the GameObject. Useful for identification, debugging, or dynamic naming during runtime.tag
: A string identifier used to categorize or group GameObjects. Useful for filtering objects in logic or triggering behavior based on tags.CompareTag(string tag)
: Efficiently compares the GameObject’s tag to a givenstring
. Preferred over string comparison for performance.GetComponent<T>()
: Retrieves a component of typeT
attached to the same GameObject. Used to interact with other components (e.g.,Rigidbody
,Collider
, or scripts).GetComponents<T>()
: Retrieves all components of typeT
attached to the GameObject.TryGetComponent<T>(out T component)
: Safely attempts to retrieve a component of typeT
, returning a boolean indicating success or failure. Avoids exceptions.GetComponentInChildren<T>()
: Finds a component of typeT
in the GameObject or its child objects.GetComponentInParent<T>()
: Finds a component of typeT
in the GameObject or its parent objects.
Example
Now let’s utilize everything we have discussed so far, from the basics of MonoBehaviour
to inherited members, specifically GetComponent<T>()
, to control the Drone
GameObject in the XFactory logistics station.
- Create a
MonoBehaviour
Script:- Right-click on the
Scripts
folder in theProject
window and selectCreate > MonoBehaviour Script
. Name itDroneController.cs
. - Paste the following script into it.
- It uses inherited members like
name
,transform
, andgameObject
directly fromMonoBehaviour
. - It uses
GetComponent<Animator>()
to access and control the drone’s main flight animation. - It uses
transform.Find()
to access child objects likeEye
and theFans
container. - It uses
GetComponent<Animator>()
on the childEye
to enable/disable its scanner’s pulse animation at runtime.
using UnityEngine; public class DroneController : MonoBehaviour { private Animator droneAnimator; // Controls drone flight animations private Animator eyeAnimator; // Controls Eye scanner animation private Transform[] fanTransforms; // References to the 4 fan objects private bool isFlying = false; // Tracks drone flight state private bool isEyeActive = true; // Tracks if Eye animator is active [SerializeField] private float idleFanSpeed = 1000f; // Fan speed while hovering [SerializeField] private float activeFanSpeed = 2000f; // Fan speed while flying void Start() { // Get the Animator on the main Drone droneAnimator = GetComponent<Animator>(); // Find Eye and get its Animator Transform eye = transform.Find("Eye"); if (eye != null) { eyeAnimator = eye.GetComponent<Animator>(); } // Get references to four fan objects under "Fans" Transform fansParent = transform.Find("Fans"); if (fansParent != null) { fanTransforms = new Transform[4]; for (int i = 0; i < 4; i++) { Transform fan = fansParent.Find($"fan.00{i + 1}"); if (fan != null) { fanTransforms[i] = fan; } } } Debug.Log($"{name} initialized with DroneController"); } void Update() { // Toggle flying state with 'F' key if (Input.GetKeyDown(KeyCode.F) && droneAnimator != null) { if (isFlying) { droneAnimator.SetTrigger("Return"); isFlying = false; Debug.Log("Drone is returning to hover."); } else { droneAnimator.SetTrigger("Move"); isFlying = true; Debug.Log("Drone is flying."); } } // Toggle Eye animator on/off with 'T' if (Input.GetKeyDown(KeyCode.T) && eyeAnimator != null) { isEyeActive = !isEyeActive; eyeAnimator.enabled = isEyeActive; // Enable/disable the Animator component Debug.Log("Eye animation is now " + (isEyeActive ? "ON" : "OFF")); } // Rotate fans based on drone state float currentSpeed = isFlying ? activeFanSpeed : idleFanSpeed; if (fanTransforms != null) { foreach (var fan in fanTransforms) { if (fan != null) { fan.Rotate(Vector3.forward * currentSpeed * Time.deltaTime); } } } } }
- Right-click on the
- Configure the Script:
- Attach this script to the
Drone
GameObject in theHierarchy
. - Adjust
Idle Fan Speed
andActive Fan Speed
as necessary.
- Attach this script to the
- Play and Observe the Behavior:
- Enter Play mode.
- Pressing
Space
toggles between flying and hovering by triggering animation transitions. - Pressing
P
enables or disables the Eye’s pulsing animation entirely (viaAnimator.enabled
). - Fan GameObjects are dynamically rotated around their
Z
-axis, with speed adjusted based on flight state.
Messages
Messages are special event functions in MonoBehaviour
. Unlike methods you call manually, messages are triggered automatically by Unity’s internal systems based on events in the game environment. This design makes it simple to respond to changes in game state, physics, input, rendering, and more without having to constantly check for those events yourself. Messages are crucial for three main reasons:
- Event-Driven Programming: Messages allow your scripts to follow an event-driven model. Instead of writing loops to monitor for conditions (like user input or collision events), you simply implement a message method (e.g.,
OnCollisionEnter()
) and Unity calls it when the event happens. - Separation of Concerns: Each message is responsible for handling a specific type of event. This modularity makes it easier to organize code—one method per event—improving readability and maintainability.
- Simplicity: Unity takes care of detecting events (such as collisions or input) and calling your methods. You only need to implement the logic, which accelerates development.
Message | Definition |
---|---|
Awake() |
Called when a script instance is loaded; used for early initialization. |
Start() |
Invoked just before the first frame update; ideal for initialization after Awake() . |
FixedUpdate() |
Called at fixed intervals; used for physics updates. |
Update() |
Called once per frame; used for general updates and handling real-time actions. |
LateUpdate() |
Called after Update() ; ideal for operations that depend on prior updates. |
OnValidate() |
Called in the Editor when a script is loaded or a value changes in the Inspector; useful for auto-updates. |
Reset() |
Called in the Editor when you click the Reset button in the Inspector; resets fields to default values. |
OnCollisionEnter() |
Called when a collider/rigidbody starts touching another collider/rigidbody. |
OnCollisionStay() |
Called every frame while another collider/rigidbody remains in contact. |
OnCollisionExit() |
Called when another collider/rigidbody stops touching this object. |
OnTriggerEnter() |
Invoked when another object enters a trigger collider on this object. |
OnTriggerStay() |
Called every frame while another object stays within the trigger collider. |
OnTriggerExit() |
Called when another object exits the trigger collider. |
OnBecameVisible() |
Called when the object becomes visible to any camera. |
OnBecameInvisible() |
Called when the object is no longer visible by any camera. |
OnDisable() |
Called when the script or its GameObject is disabled; used for cleanup or stopping actions. |
OnEnable() |
Called when the script or its GameObject becomes enabled and active. |
OnDestroy() |
Called just before the object is destroyed; used for final cleanup. |
For a full list of Unity event messages, see Unity’s MonoBehaviour documentation.
Initialization and Setup
These are methods that Unity calls during the lifecycle of a GameObject to set up and manage runtime behavior. While some occur only once, others enable real-time updates throughout gameplay. They are essential for driving dynamic behavior in XFactory—such as robotic movements, sensor monitoring, or user interaction with AR/VR panels. Key use cases include:
- Polling for input (e.g., sensor/user data).
- Moving objects (e.g., drone, quadruped).
- Checking conditions (e.g., machine state, energy level, assembly progress).
- Updating UI or animations frame-by-frame.
Reset()
The Reset()
method is called when the user selects the Reset()
option from the component’s context menu in the Inspector
, or when a component is added to a GameObject for the first time. It is used to assign sensible default values or references and does not run in Play Mode. It is ideal for setting up default values or basic references to reduce manual configuration. Key features include:
- Editor-Only: The
Reset
method is only executed in editor mode, not in the final build. - Default Initialization: It provides a mechanism to set up default values for public fields or properties, enhancing usability.
- Ease of Use: Offers a quick way for developers to restore a component’s configuration without manually adjusting properties.
Example: Example Description In XFactory, you might use Reset()
to auto-link the logistics drone to its navigation target when the component is first added in the Editor or manually reset in the Inspector
(not during Play Mode). Attach the following script to the Drone
GameObject prefab. Ensure there is a GameObject in the scene tagged as "Waypoint"
so that Reset()
can automatically assign it at edit time.
using UnityEngine;
public class DroneSetup : MonoBehaviour
{
public GameObject navigationTarget;
void Reset()
{
Debug.Log("Reset: Assigning default navigation target.");
// Try to auto-assign a default navigation waypoint
if (!navigationTarget)
{
navigationTarget = GameObject.FindWithTag("Waypoint");
}
}
}
OnValidate()
OnValidate()
is an editor-only callback that is invoked when a script is loaded or when a value is changed in the Inspector
. It is commonly used to enforce constraints, correct mistakes, or auto-update related properties without waiting for runtime. Key features include:
- Editor-Only: Executes only within the Unity Editor and not in runtime builds.
- Data Validation: Ideal for checking and clamping values to keep data consistent (e.g., ensuring numerical values remain within a specified range).
- Frequent Calls: Can be triggered often during normal Editor operations such as loading scenes, building a player, or entering Play Mode.
Example: Use OnValidate()
to ensure the forklift remains within the defined X
–Z
boundaries near the logistics area. OnValidate()
runs in the Editor, so it enforces boundary limits immediately when the forklift is moved or adjusted in the Inspector
—no need to enter Play Mode to see it applied, while runtime clamping ensures the forklift never leaves the warehouse area during gameplay.
using UnityEngine;
public class ForkliftController : MonoBehaviour
{
[Header("Motion Settings")]
[SerializeField]
private float moveSpeed = 3f; // Editable in the Inspector but not directly from other scripts
[Header("Boundary Settings (World Space)")]
public Vector2 xBounds = new Vector2(-20f, -5f);
public Vector2 zBounds = new Vector2(0f, 9f);
void Update()
{
if (Input.GetKey(KeyCode.M))
{
transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
ClampPosition();
}
}
private void OnValidate()
{
// Ensure position is clamped in the Editor too
ClampPosition();
}
private void ClampPosition()
{
Vector3 pos = transform.position;
pos.x = Mathf.Clamp(pos.x, xBounds.x, xBounds.y);
pos.z = Mathf.Clamp(pos.z, zBounds.x, zBounds.y);
transform.position = pos;
}
}
OnEnable()
OnEnable()
is automatically called when a GameObject becomes enabled and active. This method is invoked every time the object enters Play Mode, provided it is enabled. Key features include:
- Automatic Invocation: Runs immediately when the GameObject is enabled, making it ideal for initialization tasks.
- Editor Support: When using the
[ExecuteInEditMode]
attribute,OnEnable()
is also called in the Unity Editor, not just during runtime. - Complementary Functionality: Works in tandem with
OnDisable()
(introduced later), which is called when the GameObject is disabled.
Example: In XFactory’s exhibit area, this script lets you enable or disable the quadruped robot (Spot
) with a key press and clearly shows when it becomes active or inactive. Attach the script to the Spot GameObject and assign its visual child or root part to the Spot Dog
field in the Inspector
. Pressing X in Play Mode toggles Spot on and off, while OnEnable()
and OnDisable()
log messages each time the robot is activated or deactivated.
using UnityEngine;
public class SpotActivator : MonoBehaviour
{
// Reference to the Spot Dog (can be the same GameObject or a child)
public GameObject spotDog;
void Update()
{
if (Input.GetKeyDown(KeyCode.X))
{
// Toggle the Spot Dog active state
bool newState = !spotDog.activeSelf;
spotDog.SetActive(newState);
}
}
void OnEnable()
{
Debug.Log("OnEnable: Spot Dog script ENABLED.");
}
void OnDisable()
{
Debug.Log("OnDisable: Spot Dog script DISABLED.");
}
}
Awake()
Awake()
is automatically called when an enabled script instance is loaded. It is primarily used to initialize variables or states before the game starts running. This method is invoked in scenarios such as when a GameObject is active on scene load, when an inactive GameObject is activated, or when a new GameObject is created via instantiation. Key features include:
- One-time Initialization: Called only once during the lifetime of a script instance. If the Scene is reloaded or loaded additively, each instance will have its own
Awake()
call. - Order of Execution: All active GameObjects have their
Awake()
called before anyStart()
methods. However, the order in whichAwake()
is called across different GameObjects is not deterministic. - Early Setup: Since
Awake()
is called before anyStart()
methods, it is ideal for establishing references or setting initial states. - Called on Disabled Scripts:
Awake()
is executed even if the script component is disabled, as long as its GameObject is active. - Exception Handling: If an exception occurs during
Awake()
, Unity will disable the component to prevent further errors.
Example: A GameObject like can cache its Rigidbody
and initialize its status as soon as the scene loads using Awake()
, then use that cached data in Start()
to confirm readiness. Attach this script to the robot GameObject (Spot
) to see how Awake()
runs first for internal setup, while Start()
runs afterward for higher-level initialization that may depend on other objects being ready.
using UnityEngine;
public class SpotAwake : MonoBehaviour
{
private Rigidbody rb;
private string status;
void Awake()
{
// Runs immediately when the object is loaded
rb = GetComponent<Rigidbody>();
status = "Initialized";
Debug.Log("Awake: Cached Rigidbody and set status to Initialized.");
}
void Start()
{
// Runs just before the first frame update
// Often used for setup that depends on other objects being ready
Debug.Log($"Start: Robot ready with mass = {rb.mass}, status = {status}");
}
}
Start()
Start()
is invoked on the frame when a script is enabled, just before any of the frame and update loops are called for the first time. It is executed exactly once in the lifetime of the script, which makes it ideal for initialization that relies on other objects being set up first. Key features include:
- One-Time Initialization: Called only once per script instance, ensuring that initialization occurs only when needed.
- Execution Timing: Runs after the
Awake()
method of all objects, allowing it to safely interact with objects that have already initialized. - Coroutine Support: Can be implemented as a Coroutine, enabling the suspension of execution (using yield) for tasks like waiting.
- Conditional Execution: May not run on the same frame as
Awake()
if the script is not enabled during initial scene loading.
Tutorial
Let’s extend the simple SpotWalker.cs
script to guide the quadruped robot in the exhibit station of XFactory to a specified point in the XFactory scene, while enforcing editor-time setup and runtime behavior using Unity’s lifecycle callbacks:
- Editor Setup with
Reset()
: Auto-assigntargetPoint
when the component is added or reset. Key line:targetPoint = GameObject.FindWithTag("Waypoint")?.transform;
- Validation in
OnValidate()
: Clamp the waypoint’s position withinxBounds
andzBounds
whenever it’s edited in theInspector
. Key pattern:Mathf.Clamp
ontp.x
andtp.z
. - Early Initialization in
Awake()
: Reset thehasArrived
flag before any other logic runs. - Activation Hook with
OnEnable()
: Log and reinitialize state each time the component is enabled in Play Mode or Editor. - First-Frame Setup in
Start()
: Confirm the robot is initialized and ready to move. - Continuous Movement in
Update()
: UsesVector3.MoveTowards
to smoothly move along both X and Z axes. Arrival check: When distance < 0.01 units, stop and log arrival.
Implementation:
-
Attach the script below to your quadruped GameObject (
Spot
). Make sure the quadruped robot GameObject has aRenderer
using UnityEngine; public class SpotWalker : MonoBehaviour { [Header("Movement Settings")] public float speed = 1.0f; public float rotationSpeed = 180f; // Degrees per second public Transform targetPoint; public Vector2 xBounds = new Vector2(-20f, 20f); public Vector2 zBounds = new Vector2(-10f, 10f); private bool hasArrived; private Animator animator; // Animator to disable on arrival // ————— Editor Setup ————— void Reset() { // Auto-assign a waypoint tagged "Waypoint" targetPoint = GameObject.FindWithTag("Waypoint")?.transform; } void OnValidate() { if (targetPoint) { // Clamp the target position within bounds whenever edited Vector3 tp = targetPoint.position; tp.x = Mathf.Clamp(tp.x, xBounds.x, xBounds.y); tp.z = Mathf.Clamp(tp.z, zBounds.x, zBounds.y); targetPoint.position = tp; } } // ————— Runtime Initialization ————— void Awake() { hasArrived = false; animator = GetComponent<Animator>(); // Cache Animator reference } void OnEnable() { hasArrived = false; if (animator != null) animator.enabled = true; // Ensure animations are active on enable Debug.Log($"SpotWalker enabled, heading to: {(targetPoint ? targetPoint.name : "none")}."); } void Start() { Debug.Log("Quadruped robot initialized, will turn and move to target point."); } // ————— Movement, Turning & Arrival Handling ————— void Update() { // Only run movement and turning in Play Mode if (!Application.isPlaying || targetPoint == null || hasArrived) return; // Compute direction on the XZ plane Vector3 direction = targetPoint.position - transform.position; direction.y = 0; if (direction.sqrMagnitude > 0.0001f) { // Determine the rotation needed Quaternion targetRotation = Quaternion.LookRotation(direction); // Rotate smoothly toward the target transform.rotation = Quaternion.RotateTowards( transform.rotation, targetRotation, rotationSpeed * Time.deltaTime ); // Move forward in the newly faced direction transform.position = Vector3.MoveTowards( transform.position, targetPoint.position, speed * Time.deltaTime ); } // Arrival check if (Vector3.Distance(transform.position, targetPoint.position) < 0.01f) { hasArrived = true; Debug.Log("Arrived at target point."); if (animator != null) { animator.enabled = false; // Deactivate Animator on arrival Debug.Log("Animator disabled upon arrival."); } } } }
- Create an empty GameObject and tag it object as
"Waypoint"
. Alternatively, drag it or anyTransform
intoTarget Point
. -
Adjust
Speed
,Rotation Speed
,xBounds
, andzBounds
in theInspector
. -
Play the scene—watch your robot walk to the target and stop on arrival.
You have leveraged Unity’s lifecycle methods, especially Initialization and Setup messages, to create a robust, self-validating movement controller that works seamlessly in both the Editor and at runtime.
Frame and Update Loops
These methods are called once per frame or at a consistent time interval by Unity to handle ongoing logic during simulation. In XFactory, these loops are crucial for managing everything from real-time robotic motion to drone navigation, user interaction, UI feedback, and camera control. Key use cases include:
- Polling for user or machine input.
- Moving factory robots, drones, or conveyor belts.
- Monitoring operational conditions (e.g., battery level, tool wear).
- Updating UI panels, visual indicators, or animations.
Update()
Update()
is called every frame when the script’s component is enabled. It is primarily used to implement frame-dependent behaviors and game logic. Key features include:
- Frame-Based Execution: Runs every frame, making it ideal for tasks like checking for input or updating game states.
- Time Management: Uses
Time.deltaTime
to calculate the elapsed time since the last frame, ensuring frame rate independent movement and actions. - Conditional Use: Not every script requires an
Update()
function; it should only be implemented when continuous frame-based processing is necessary. - Enabled Dependency: The function is called only if the
MonoBehaviour
is enabled.
Example: In XFactory’s logistics station, the forklift (Forklift
) can use Update()
to move forward whenever the operator presses the M key. Attach this script to a forklift GameObject in the warehouse. Because it uses transform.Translate
inside Update()
, the motion is tied to the rendering frame rate and does not interact with the physics engine—making it simple, but less realistic for collision handling.
using UnityEngine;
public class ForkliftUpdate : MonoBehaviour
{
[SerializeField]
private float moveSpeed = 3f;
void Update()
{
if (Input.GetKey(KeyCode.M))
{
// Frame-based movement (ignores physics)
transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
Debug.Log("Update: Forklift moved forward with Transform.Translate.");
}
}
}
FixedUpdate()
FixedUpdate()
is Unity’s method for consistent, physics-based updates. It is called at regular, fixed intervals—ideal for any logic involving Rigidbody
motion, collisions, or real-world physical simulations. Key features include:
- Fixed Time Step: Runs every 0.02 seconds by default (configurable in project settings).
- Physics Engine Sync: Designed for
Rigidbody
movement, collision response, and force application. - Frame Rate Independence: Unlike
Update()
,FixedUpdate()
provides consistent physics behavior regardless of graphical frame rates.
Example: In XFactory’s logistics station, the forklift (Forklift
) can use FixedUpdate()
with a Rigidbody
to move forward whenever the operator presses the N key. Attach this script to a forklift GameObject that has a Rigidbody
component. Because the motion is applied inside FixedUpdate()
, it runs in sync with Unity’s physics engine, ensuring consistent movement across frame rates and realistic collision handling with other objects in the warehouse.
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class ForkliftFixedUpdate : MonoBehaviour
{
[SerializeField]
private float moveSpeed = 3f;
private Rigidbody rb;
void Awake()
{
rb = GetComponent<Rigidbody>();
}
void FixedUpdate()
{
if (Input.GetKey(KeyCode.N))
{
// Physics-based movement (collides realistically with obstacles)
rb.MovePosition(rb.position + transform.forward * moveSpeed * Time.fixedDeltaTime);
Debug.Log("FixedUpdate: Forklift moved forward with Rigidbody.MovePosition.");
}
}
}
LateUpdate()
LateUpdate()
is called every frame after all Update()
methods have run. It’s ideal for tasks that depend on updated GameObject positions, such as camera tracking. Key features include:
- Execution Order: Runs after all
Update()
calls, ensuring motion is complete before responding. - Frame-Based Call: Used for frame-dependent tasks like camera smoothing or bone-based animation alignment.
- Ideal for Follow-Cameras: Prevents laggy camera behavior that can occur if updates run out of sync.
Example: In XFactory’s exhibit station, a camera can follow a quadruped robot using LateUpdate()
to ensure smooth tracking after the robot moves. Attach this script to the camera GameObject (e.g., Main Camera
) intended to follow the quadruped robot (Spot
) in the exhibit area. Use LateUpdate()
so the camera updates after the robot has moved each frame, ensuring smooth and consistent tracking behavior.
Tutorial
Let’s extend our previous SpotWalker.cs
example to demonstrate Unity’s special update loops:
-
Physics-Driven Movement with
FixedUpdate()
: Runs at a consistent timestep (default 0.02 s) in sync with the physics engine. UsesRigidbody.MoveRotation
andMovePosition
to turn and advance the robot toward its target. Checks for arrival and disables the Animator when the robot reaches the waypoint. -
Smooth Camera Follow with
LateUpdate()
: Executes every frame, after allUpdate()
calls. Repositions and orients the camera to trail the robot’s final pose without jitter.
Implementation:
-
Update your
SpotWalker.cs
so that all movement and turning runs inFixedUpdate()
using aRigidbody
. Replace the oldUpdate()
with this:using UnityEngine; [RequireComponent(typeof(Rigidbody))] public class SpotWalker : MonoBehaviour { [Header("Movement Settings")] public float speed = 1.0f; public float rotationSpeed = 180f; // Degrees per second public Transform targetPoint; public Vector2 xBounds = new Vector2(-10f, 10f); public Vector2 zBounds = new Vector2(-5f, 5f); private bool hasArrived; private Animator animator; // Animator to disable on arrival private Rigidbody rb; // Physics body // ———— Runtime Initialization ———— void Awake() { rb = GetComponent<Rigidbody>(); animator = GetComponent<Animator>(); // Freeze X/Z rotations so the robot stays upright rb.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ; hasArrived = false; } void OnEnable() { hasArrived = false; if (animator != null) animator.enabled = true; Debug.Log($"SpotWalker enabled, heading to: {(targetPoint ? targetPoint.name : "none")}."); } void Start() { Debug.Log("Quadruped robot initialized; using FixedUpdate for motion."); } // ———— Physics‐Timed Movement, Turning & Arrival ———— void FixedUpdate() { if (targetPoint == null || hasArrived) return; // 1. Compute horizontal direction Vector3 dir = targetPoint.position - rb.position; dir.y = 0; if (dir.sqrMagnitude > 0.0001f) { // 2. Smoothly rotate via Rigidbody Quaternion desired = Quaternion.LookRotation(dir); Quaternion nextRot = Quaternion.RotateTowards( rb.rotation, desired, rotationSpeed * Time.fixedDeltaTime ); rb.MoveRotation(nextRot); // 3. Move forward via Rigidbody Vector3 step = dir.normalized * speed * Time.fixedDeltaTime; rb.MovePosition(rb.position + step); } // 4. Arrival check & Animator disable if (Vector3.Distance(rb.position, targetPoint.position) < 0.01f) { hasArrived = true; Debug.Log("Arrived at target point (FixedUpdate)."); if (animator != null) { animator.enabled = false; Debug.Log("Animator disabled upon arrival."); } } } }
-
Add a
Rigidbody
component to your quadruped and freeze itsX
andZ
rotations. Ensure it also has anAnimator
. -
Create
FollowSpotCamera.cs
and attach to yourMain Camera
:using UnityEngine; public class FollowSpotCamera : MonoBehaviour { [Tooltip("Drag your robot's Transform here")] public Transform spot; [Tooltip("Offset in the robot's local space (x = right, y = up, z = forward)")] public Vector3 offset = new Vector3(0f, 2f, -5f); void LateUpdate() { if (spot == null) return; // Position is target + rotated offset (so it stays behind the robot) transform.position = spot.position + spot.rotation * offset; // Always look at the robot transform.LookAt(spot); } }
-
In the
Inspector
, drag your robot’sTransform
intoQuadruped
, tweakOffset
andSmooth Speed
. -
Play the Scene. The robot will now turn and move under fixed‐timestep physics control, disable its
Animator
on arrival, and the camera will smoothly follow inLateUpdate()
.
Physics Events
These are triggered automatically when the GameObject interacts with other objects through the Unity physics system (using Colliders
and Rigidbody
components). There are two types: Collision (solid object contact) and Trigger (non-physical overlap). Key use cases include:
- Detecting hits, landings, or bumps.
- Triggering effects or events when entering a zone.
- Managing physics-based reactions (e.g., bouncing, sliding).
OnCollisionEnter/Stay/Exit()
Unity provides three key callbacks—OnCollisionEnter
, OnCollisionStay
, and OnCollisionExit
—to handle different stages of collisions between GameObjects. These methods let you detect when collisions begin, persist, and end, providing detailed physics data through the Collision
object. They are essential for building responsive interactions, such as triggering effects on impact, monitoring sustained contact, or resetting states when collisions stop.
-
OnCollisionEnter(Collision)
: Called when a GameObject with a non-kinematicRigidbody
starts colliding with another collider. It passes aCollision
object containing rich information about the contact. Key features include collision data through aCollision
object (contact points, impact velocity, etc.), physics interaction that only triggers when one of the colliding objects has a non-kinematic rigidbody, performance optimization by omitting the parameter if details are not needed, and event propagation which allows collision events to be sent to disabledMonoBehaviour
scripts. -
OnCollisionStay(Collision)
: Invoked each frame while two GameObjects remain in contact. Useful for ongoing feedback or monitoring sustained pressure/contact. Key features include continuous collision monitoring (called every frame while the collision persists), detailed collision data (contact points and relative velocity), performance considerations (omit parameter to avoid extra computations), the requirement that one collider must have a non-kinematic rigidbody, and the fact that collision stay events are not triggered for rigidbodies that are sleeping. -
OnCollisionExit(Collision)
: Called when two colliders stop touching. Commonly used for cleanup, state reset, or to log the end of a collision event. Key features include collision end notification (triggered when contact ceases), optional detailed collision data (contact points and velocity, though often just used to signal the end), the requirement that one object must have a non-kinematic rigidbody, and performance considerations (omit parameter if detailed data is unnecessary to reduce computations).
Example: In XFactory’s training arena, the player can log when colliding with obstacles such as crates or walls. Attach this script to the player GameObject with a CharacterController
. To ensure OnCollisionEnter/Stay/Exit
fire, all objects the player collides with must have a Collider and a Rigidbody (set to Is Kinematic if you don’t want them to move).
using UnityEngine;
public class PlayerCollisionLogger : MonoBehaviour
{
void OnCollisionEnter(Collision collision)
{
Debug.Log($"[Player] ENTER collision with: {collision.gameObject.name}");
}
void OnCollisionStay(Collision collision)
{
Debug.Log($"[Player] STAY collision with: {collision.gameObject.name}");
}
void OnCollisionExit(Collision collision)
{
Debug.Log($"[Player] EXIT collision with: {collision.gameObject.name}");
}
}
OnTriggerEnter/Stay/Exit()
Unity provides three key trigger callbacks—OnTriggerEnter
, OnTriggerStay
, and OnTriggerExit
—to detect when objects interact with trigger zones. Unlike collisions, triggers don’t physically block movement but instead allow for non-physical interactions such as sensors, checkpoints, and area-based events. Together, these methods let you monitor when an object enters, remains inside, and exits a trigger zone for full lifecycle control.
-
OnTriggerEnter(Collider)
:OnTriggerEnter(Collider)
is called when a GameObject with a trigger collider overlaps another collider. It’s commonly used for non-physical detection such as zone-based events, pickups, or proximity-based interactions. One of the colliders must be marked as a trigger, and at least one object must have aRigidbody
component (typically the moving object), with both havingCollider
s. This method is executed during theFixedUpdate()
cycle, and theother
parameter refers to the collider of the object that entered the trigger. -
OnTriggerStay(Collider)
:OnTriggerStay(Collider)
is called every physics update while another object remains inside a trigger collider. It is ideal for monitoring ongoing presence within a zone, such as charging stations, loading bays, or weight sensors. It provides continuous detection and is called repeatedly while the other collider stays inside the trigger. At least one object must have aRigidbody
component, and this method is especially useful for checking ongoing conditions like object stability, valid position, or contact duration. -
OnTriggerExit(Collider)
:OnTriggerExit(Collider)
is called when another collider exits a trigger zone. It is commonly used to stop processes that began withOnTriggerEnter()
orOnTriggerStay()
. It provides exit detection, signaling that an object has left the trigger zone, and requires at least one of the two colliders to have aRigidbody
. It complements entry events for full lifecycle control, but note that if the object is destroyed or deactivated inside the trigger, no exit event will occur.
Example: This example shows how a player with a CharacterController can detect when it enters, stays inside, or exits a trigger zone using OnTriggerEnter/Stay/Exit
. Unlike collisions, triggers don’t physically block the player but still fire events when overlapped. This makes them ideal for sensors, checkpoints, or area-based gameplay mechanics.
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class PlayerTriggerLogger : MonoBehaviour
{
void OnTriggerEnter(Collider other)
{
Debug.Log($"[Player] ENTER trigger with: {other.gameObject.name}");
}
void OnTriggerStay(Collider other)
{
Debug.Log($"[Player] STAY trigger with: {other.gameObject.name}");
}
void OnTriggerExit(Collider other)
{
Debug.Log($"[Player] EXIT trigger with: {other.gameObject.name}");
}
}
Tutorial
Let’s use OnTriggerEnter(Collider)
to simulate a box scanning process in the logistic station using the drone. You will:
- Pilot a drone in Play Mode using W/A/S/D + Q/E keys.
- Scan storage boxes by bumping them with a trigger sphere and displaying their names on a UI text.
- Keep the fans spinning at idle or active speed based on movement input.
- Chase–style camera follows behind using
LateUpdate()
.
Implementation:
- Update the
DroneController.cs
Script:- Update the existing
DroneController.cs
script attached to yourDrone
(must have aRigidbody
) as follows. - This script handles movement in
FixedUpdate()
, fan rotation & scan‐UI inUpdate()
, and scanning viaOnTriggerEnter()
.
using UnityEngine; using TMPro; [RequireComponent(typeof(Rigidbody))] public class DroneController : MonoBehaviour { [Header("Movement Settings")] public float speed = 5f; // linear speed (m/s) public float rotationSpeed = 100f; // yaw speed (deg/s) [Tooltip("How quickly velocity matches target (1/seconds). Higher = snappier.")] public float velocityDamping = 12f; [Tooltip("How quickly yaw matches target (1/seconds). Higher = snappier.")] public float yawDamping = 20f; [Header("Fan Settings")] public float idleFanSpeed = 1000f; public float activeFanSpeed = 2000f; [Header("Scan UI (TextMeshPro)")] public TextMeshProUGUI scanText; public float displayDuration = 2f; private Rigidbody rb; private Transform[] fanTransforms; private bool isFlying; private float scanTimer; // ---- input cached per-frame ---- float forwardInput, strafeInput, turnInput, verticalInput; void Awake() { rb = GetComponent<Rigidbody>(); // Physics settings that help visual smoothness rb.interpolation = RigidbodyInterpolation.Interpolate; rb.collisionDetectionMode = CollisionDetectionMode.Continuous; rb.maxAngularVelocity = 100f; rb.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ; var fansParent = transform.Find("Fans"); if (fansParent != null) { fanTransforms = new Transform[fansParent.childCount]; for (int i = 0; i < fansParent.childCount; i++) fanTransforms[i] = fansParent.GetChild(i); } } void Update() { // ----- cache inputs here (per render frame) ----- forwardInput = (Input.GetKey(KeyCode.W) ? 1f : 0f) + (Input.GetKey(KeyCode.S) ? -1f : 0f); strafeInput = (Input.GetKey(KeyCode.D) ? 1f : 0f) + (Input.GetKey(KeyCode.A) ? -1f : 0f); turnInput = (Input.GetKey(KeyCode.E) ? 1f : 0f) + (Input.GetKey(KeyCode.Q) ? -1f : 0f); verticalInput= (Input.GetKey(KeyCode.Z) ? 1f : 0f) + (Input.GetKey(KeyCode.C) ? -1f : 0f); // normalize planar so diagonals aren't faster; keep vertical independent Vector2 planar = new Vector2(strafeInput, forwardInput); planar = planar.sqrMagnitude > 1f ? planar.normalized : planar; strafeInput = planar.x; forwardInput = planar.y; isFlying = Mathf.Abs(forwardInput) + Mathf.Abs(strafeInput) + Mathf.Abs(turnInput) + Mathf.Abs(verticalInput) > 0f; // Clear scan text after the display duration if (scanText && !string.IsNullOrEmpty(scanText.text)) { scanTimer += Time.deltaTime; if (scanTimer >= displayDuration) { scanText.text = ""; scanTimer = 0f; } } } void FixedUpdate() { // ----- linear velocity target ----- Vector3 targetVel = (transform.forward * forwardInput + transform.right * strafeInput + transform.up * verticalInput) * speed; // Exponential smoothing towards targetVel (frame-rate independent) float tv = 1f - Mathf.Exp(-velocityDamping * Time.fixedDeltaTime); rb.linearVelocity = Vector3.Lerp(rb.linearVelocity, targetVel, tv); // ----- yaw angular velocity target ----- // Convert deg/s to rad/s for angularVelocity float yawRadPerSec = rotationSpeed * Mathf.Deg2Rad * turnInput; float currentYaw = Vector3.Dot(rb.angularVelocity, Vector3.up); float targetYaw = yawRadPerSec; float ta = 1f - Mathf.Exp(-yawDamping * Time.fixedDeltaTime); float newYaw = Mathf.Lerp(currentYaw, targetYaw, ta); // preserve only yaw (keep X/Z zero because we froze them) rb.angularVelocity = new Vector3(0f, newYaw, 0f); } void LateUpdate() { // Rotate fans visually after physics for butter-smooth visuals float fanSpeed = isFlying ? activeFanSpeed : idleFanSpeed; if (fanTransforms != null) { float angle = fanSpeed * Time.deltaTime; // purely visual for (int i = 0; i < fanTransforms.Length; i++) fanTransforms[i].Rotate(Vector3.forward, angle, Space.Self); } } void OnTriggerEnter(Collider other) { if (other.CompareTag("StorageBox") && scanText != null) { scanText.text = $"Scanned: {other.name}"; scanTimer = 0f; Debug.Log($"[Drone] Scanned: {other.name}"); } } }
- Update the existing
- Add a Front–Mount Trigger Collider:
- Under your
Drone
GameObject, create an empty child namedScanner
and position it at the drone’s nose (where you want it to “scan” forward). Also, make sure to disable the drone’sAnimator
. - On the
Scanner
child, add aSphereCollider
component. - In the
SphereCollider
settings, checkIs Trigger
. - Adjust the
Radius
so it just encapsulates the front of the drone without overlapping too far. This trigger will fireOnTriggerEnter()
in yourDroneController
when it overlaps anyStorageBox
. - In the
Inspector
, set each rack box’sTag
toStorageBox
.
- Under your
- Add a Scan UI Text:
- Create a
Screen Space - Overlay
UICanvas
, addUI > Text - TextMeshPro
. - Position the text element (e.g., top-left of the screen) and clear its default content.
- Optionally style the font, size, and color for readability.
- Drag this Text component into the
Scan Text
field on yourDroneController
script in theInspector
.
- Create a
- Create a
FollowDroneCamera.cs
Script:- Create a new script named
FollowDroneCamera.cs
. - Attach it to your
Main Camera
. Remove the other script (FollowQuadrupedCamera.cs
) to avoid conflict.
using UnityEngine; public class FollowDroneCamera : MonoBehaviour { [Tooltip("Assign your Drone’s Transform here")] public Transform drone; [Tooltip("Local offset: (X right, Y up, Z back)")] public Vector3 localOffset = new Vector3(0f, 3f, -8f); [Tooltip("Smooth follow speed")] public float smoothSpeed = 5f; void LateUpdate() { if (drone == null) return; // 1. Compute world position behind the drone Vector3 desiredPos = drone.TransformPoint(localOffset); // 2. Smoothly interpolate camera position transform.position = Vector3.Lerp( transform.position, desiredPos, smoothSpeed * Time.deltaTime ); // 3. Always look at the drone’s center transform.LookAt(drone); } }
- Create a new script named
- Configure the Script:
- Drag your drone’s Transform into the
Drone
field on theFollowDroneCamera
component. - Tweak
Local Offset
(X
: right/left,Y
: height,Z
: distance) to position the camera, and adjustSmooth Speed
for responsiveness.
- Drag your drone’s Transform into the
- Play the Scene:
- W/A/S/D: Move and strafe the drone.
- Q/E: Rotate (yaw) the drone.
- Fans: Spin faster when movement input is detected.
- Scanning: When the Scanner trigger overlaps a StorageBox, its name appears in the UI text for the configured
displayDuration
. - Camera: Trails behind the drone at your configured offset, smoothly following in
LateUpdate()
.
Visibility & Rendering
These methods are tied to what Unity’s cameras can see. Unity automatically calls them when a GameObject’s renderer enters or exits the field of view of any camera. In XFactory, these callbacks are essential for performance tuning, debugging, and triggering visibility-based logic for dynamic machines, sensors, or display panels. Key use cases include:
- Optimizing performance by disabling logic or effects when off-screen.
- Triggering animations, sounds, or loading behavior when something appears/disappears.
- Debugging or managing visibility-dependent logic.
OnBecameVisible()
OnBecameVisible()
is called when the renderer attached to a GameObject becomes visible to any camera. This message is broadcast to all scripts on the same GameObject with a renderer component. Key features include:
- Visibility Detection: Triggered when an object comes into the camera’s view, allowing the execution of code only when the object is visible.
- Performance Optimization: Helps reduce unnecessary computations by activating behaviors only when required.
- Coroutine Capability: Can be used as a coroutine by incorporating the yield statement within the function.
- Editor Support: In the Unity Editor, the Scene view cameras can also trigger this function, aiding in debugging and scene management.
Example: In XFactory’s assembly station, the large display screen (Display GT
) is set up to smoothly power on and off based on camera visibility. By attaching the script to the Display GT
GameObject—which must include a Renderer
so OnBecameVisible()
and OnBecameInvisible()
are triggered—and ensuring it has a child Canvas
for UI elements, the screen will automatically activate its Canvas two seconds after entering the camera’s view and deactivate it two seconds after leaving. This creates a delayed “power-on” and “power-off” effect that makes the display feel more lifelike in the scene.
using UnityEngine;
public class DisplayScreenController : MonoBehaviour
{
[SerializeField] private Canvas displayCanvas; // Assign in Inspector
[SerializeField] private float showDelay = 2f;
[SerializeField] private float hideDelay = 2f;
private void Awake()
{
if (!displayCanvas)
displayCanvas = GetComponentInChildren<Canvas>(true);
if (displayCanvas)
displayCanvas.enabled = false;
}
private void OnBecameVisible()
{
Debug.Log("[DisplayScreen] Became visible — scheduling power-on.");
CancelInvoke(nameof(EnableCanvas)); // cancel pending hides
Invoke(nameof(EnableCanvas), showDelay);
}
private void OnBecameInvisible()
{
Debug.Log("[DisplayScreen] Became invisible — scheduling power-off.");
CancelInvoke(nameof(EnableCanvas)); // cancel pending shows
Invoke(nameof(DisableCanvas), hideDelay);
}
private void EnableCanvas()
{
if (displayCanvas)
{
displayCanvas.enabled = true;
Debug.Log("[DisplayScreen] Canvas enabled.");
}
}
private void DisableCanvas()
{
if (displayCanvas)
{
displayCanvas.enabled = false;
Debug.Log("[DisplayScreen] Canvas disabled.");
}
}
}
OnBecameInvisible()
OnBecameInvisible()
is invoked when the renderer attached to a GameObject is no longer visible by any camera. This function is called on all scripts associated with the renderer, allowing you to manage behaviors based on object visibility. Key features include:
- Visibility Monitoring: Triggered when an object is no longer seen by any camera, which can be used to halt unnecessary computations.
- Performance Optimization: Helps optimize performance by disabling behaviors when the object is not in view.
- Editor Support: In the Unity Editor, Scene view cameras can also trigger this function, aiding in debugging.
Tutorial
Let’s use OnBecameVisible()
and OnBecameInvisible()
to monitor when a StorageBox enters or leaves any camera’s view in the logistics station. You will:
- Update a text field to show “Box Visible: [Name]” when it appears on-screen.
- Clear that text when it goes off-screen.
- Log corresponding messages to the Console for each event.
Implementation:
-
Create a new script named
BoxVisibilityMonitor.cs
and attach it to each StorageBox prefab/instance (make sure the box has aRenderer
component).OnBecameVisible()
fires once when the box’sRenderer
becomes visible to any camera.OnBecameInvisible()
fires once when it leaves all camera views.using UnityEngine; using TMPro; public class BoxVisibilityMonitor : MonoBehaviour { [Tooltip("Drag your UI TextMeshProUGUI here")] public TextMeshProUGUI visibilityText; void OnBecameVisible() { if (visibilityText != null) { visibilityText.text = $"Box Visible: {gameObject.name}"; Debug.Log($"[BoxVisibility] Box Visible: {gameObject.name}"); } } void OnBecameInvisible() { if (visibilityText != null) { visibilityText.text = ""; // Clear when no longer visible Debug.Log($"[BoxVisibility] Box Invisible: {gameObject.name}"); } } }
- Attach
BoxVisibilityMonitor.cs
to a box GameObject. -
Select the box and in the
BoxVisibilityMonitor
component, drag theText (TMP)
UI element into theVisibility Text
field. -
Play the scene. Move the drone so that the box enters the view frustum. When the box becomes visible, the UI text will update to Box Visible: [Name of the Box]. When the box leaves view, the UI text will clear. Check the Console for corresponding log messages from
OnBecameVisible()
andOnBecameInvisible()
.
Teardown
These Unity event methods are called when a script or GameObject is disabled or destroyed. They are essential for cleaning up resources, unsubscribing from events, or halting behaviors that should not continue once the object is no longer active. In XFactory, teardown logic can be used when, robots shut down or are removed from the scene, temporary diagnostic UI panels are closed, or machines are powered off between simulation phases. Key use cases include:
- Unsubscribing from events (e.g., PLC triggers or telemetry).
- Stopping coroutines or timers.
- Saving state (e.g., storing part progress before shutdown).
- Disabling effects or UI.
OnDisable()
OnDisable()
is automatically invoked when a script or its GameObject is disabled. This method is useful for performing cleanup operations such as unsubscribing from events, releasing resources, or other shutdown tasks. It is also called when a GameObject is destroyed or when scripts are reloaded after compilation. Key features include:
- Automatic Invocation: Called by Unity when a GameObject or its component is disabled.
- Cleanup Operations: Ideal for releasing resources or stopping ongoing processes.
- Script Reload Handling: Invoked during script reloads, ensuring proper state management.
- Editor Support: Works in both runtime and edit mode, which is especially useful for testing in the Unity Editor.
Example: In XFactory’s manufacturing station, pressing a key (e.g., U
) toggles the CNC machine’s operating sound via the AudioSource
component attached to the machine (CNC_Mill_Set
). When the machine is deactivated or the script is disabled, OnDisable()
ensures the audio is stopped and a shutdown message is logged. Attach this script to the CNC machine GameObject (CNC_Mill_Set
) in the manufacturing station. In the Inspector
, assign the AudioSource
component of the CNC machine to the machineAudio
field. Update()
handles audio toggling on key press (L
), while OnDisable()
ensures the machine sound is turned off cleanly when disabled.
using UnityEngine;
public class CNCAudioController : MonoBehaviour
{
public AudioSource machineAudio; // Assign CNC machine AudioSource in Inspector
private bool audioOn = false;
void Update()
{
if (Input.GetKeyDown(KeyCode.U))
{
audioOn = !audioOn;
if (audioOn)
{
machineAudio.Play();
Debug.Log("CNC machine audio ON.");
}
else
{
machineAudio.Stop();
Debug.Log("CNC machine audio OFF.");
}
}
}
void OnDisable()
{
if (machineAudio != null && machineAudio.isPlaying)
{
machineAudio.Stop();
}
Debug.Log("CNC audio system disabled — sound stopped.");
}
}
OnDestroy()
OnDestroy()
is automatically invoked when a GameObject or its attached behavior is destroyed. This event typically occurs when a scene ends, a new scene is loaded, or the application is closed. Key features include:
- Scene Transitions and Application Shutdown: Called when switching scenes or shutting down the game, ensuring cleanup actions occur.
- Active Object Requirement: It is only invoked for game objects that were active at some point in the scene.
- Debug and Logging: Often used to log cleanup events or release resources as the application exits or transitions between scenes.
Example: In the manufacturing station, multiple finished CNC parts are placed next to the CNC machine and tagged as Finished Part
. A central manager (CNCPartManager
) controls their lifecycle. When the user presses the R
key, the manager randomly selects one tagged part, destroys it, and logs a cleanup message to simulate teardown. Attach this script to an empty GameObject named CNCPartManager
in the manufacturing station. In Play Mode, press R
to remove a random CNC part and watch the console log both the destruction action and the simulated cleanup via HandlePartDestroyed()
.
using UnityEngine;
public class CNCPartManager : MonoBehaviour
{
void Update()
{
// Press R to randomly destroy one CNC part
if (Input.GetKeyDown(KeyCode.R))
{
GameObject[] parts = GameObject.FindGameObjectsWithTag("Finished Part");
if (parts.Length > 0)
{
int index = Random.Range(0, parts.Length);
GameObject chosenPart = parts[index];
Debug.Log($"Manager: Destroying {chosenPart.name}");
Destroy(chosenPart);
// Simulate OnDestroy-like cleanup here
HandlePartDestroyed(chosenPart);
}
else
{
Debug.Log("Manager: No CNC parts left to destroy.");
}
}
}
private void HandlePartDestroyed(GameObject part)
{
// Any cleanup logic for destroyed parts goes here
Debug.Log($"Manager: {part.name} teardown complete.");
}
}
Tutorial
Let’s extend our drone flight simulation further by simulating a landing-shutdown-power on-takeoff cycle:
- Press L to descend gently to a landing height, then disable the
DroneController.cs
script. - Press T to enable
DroneController.cs
and ascend back to the original height. - Use
OnDisable()
inDroneController.cs
for cleanup/logging when controls are locked, andOnEnable()
to signal reactivation.
Implementation:
-
Add
OnDisable()
/OnEnable()
methods to your existingDroneController.cs
:public class DroneController : MonoBehaviour { // ... existing code ... void OnEnable() { Debug.Log("DroneController enabled - controls active."); } void OnDisable() { Debug.Log("DroneController disabled - controls locked."); } }
-
Create
DroneLifecycleManager.cs
and attach it to the sameDrone
GameObject:using UnityEngine; using System.Collections; [RequireComponent(typeof(DroneController))] public class DroneLifecycleManager : MonoBehaviour { [Tooltip("Reference to the DroneController component")] public DroneController droneController; [Header("Landing / Takeoff Settings")] public float landingHeight = 0.1f; // Y position to land at public float speed = 2f; // Ascent/Descent speed private float initialHeight; void Awake() { if (droneController == null) droneController = GetComponent<DroneController>(); initialHeight = transform.position.y; } void Update() { if (Input.GetKeyDown(KeyCode.L)) StartCoroutine(Land()); if (Input.GetKeyDown(KeyCode.T)) StartCoroutine(TakeOff()); } IEnumerator Land() { // Descend until landingHeight while (transform.position.y > landingHeight) { transform.position += Vector3.down * speed * Time.deltaTime; yield return null; } // Disable control if (droneController != null) droneController.enabled = false; Debug.Log("DroneLifecycleManager: Landed, DroneController disabled."); } IEnumerator TakeOff() { // Re-enable control if (droneController != null) droneController.enabled = true; // Ascend back to initialHeight while (transform.position.y < initialHeight) { transform.position += Vector3.up * speed * Time.deltaTime; yield return null; } Debug.Log("DroneLifecycleManager: Takeoff complete, DroneController enabled."); } }
- Attach the
DroneLifecycleManager
component to yourDrone
GameObject (alongsideDroneController
). - In the
DroneLifecycleManager
inspector, drag the existingDroneController
component into theDrone Controller
field (it should auto-detect if left blank). -
Adjust
Landing Height
andSpeed
to control how low/far and how fast your drone descends and ascends. - Press L in Play mode. The drone will smoothly descend to the configured
landingHeight
. Once reached,DroneController
is disabled, locking out movement controls. Check the Console for theOnDisable()
log fromDroneController
. -
Press T in Play mode.
DroneController
is re-enabled, restoring full hover controls. The drone will ascend back up to its original altitude (initialHeight
). Check the Console for theOnEnable()
log fromDroneController
.
Key Takeaways
MonoBehaviour
is the backbone of Unity scripting, connecting your custom C# code to the engine’s lifecycle, event system, and component model. By understanding its initialization hooks (Awake()
, Start()
, OnEnable()
), frame and physics loops (Update()
, LateUpdate()
, FixedUpdate()
), and event callbacks for collisions, triggers, visibility, and teardown, you can design responsive, efficient, and maintainable behaviors. Leveraging serialized fields and public methods lets you configure and reuse scripts without altering code, while inherited members like transform
and gameObject
simplify direct interaction with scene objects. In practice, mastering these patterns allows you to build modular, event-driven gameplay systems that react seamlessly to both the Unity engine’s timing and player or environment interactions.