F01


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 like Start() and Update().
  • Differentiate between initialization and setup methods in Unity. As preparation, study Awake(), Start(), OnEnable(), Reset(), and OnValidate() 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(), and LateUpdate() methods to move or rotate a GameObject using Time.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() and OnBecameInvisible() to log when an object enters or leaves the camera’s view.
  • Handle teardown events for cleanup in Unity. Before arriving, use OnDisable() and OnDestroy() 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, or Audio 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.

F02

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 about MonoBehaviour.


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 use Start() to initialize drone pathfinding logic, while Update() 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 a ForkliftController 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, a MonoBehaviour 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’s position, rotation, and scale in the scene. Crucial for movement, orientation, and spatial logic.
  • name: The name 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 given string. Preferred over string comparison for performance.
  • GetComponent<T>(): Retrieves a component of type T attached to the same GameObject. Used to interact with other components (e.g., Rigidbody, Collider, or scripts).
  • GetComponents<T>(): Retrieves all components of type T attached to the GameObject.
  • TryGetComponent<T>(out T component): Safely attempts to retrieve a component of type T, returning a boolean indicating success or failure. Avoids exceptions.
  • GetComponentInChildren<T>(): Finds a component of type T in the GameObject or its child objects.
  • GetComponentInParent<T>(): Finds a component of type T 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.

  1. Create a MonoBehaviour Script:
    • Right-click on the Scripts folder in the Project window and select Create > MonoBehaviour Script. Name it DroneController.cs.
    • Paste the following script into it.
    • It uses inherited members like name, transform, and gameObject directly from MonoBehaviour.
    • It uses GetComponent<Animator>() to access and control the drone’s main flight animation.
    • It uses transform.Find() to access child objects like Eye and the Fans container.
    • It uses GetComponent<Animator>() on the child Eye 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);
                     }
                 }
             }
         }
     }
    
  2. Configure the Script:
    • Attach this script to the Drone GameObject in the Hierarchy.
    • Adjust Idle Fan Speed and Active Fan Speed as necessary.

    01

  3. 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 (via Animator.enabled).
    • Fan GameObjects are dynamically rotated around their Z-axis, with speed adjusted based on flight state.

    F05


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.

F08


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 XZ 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 any Start() methods. However, the order in which Awake() is called across different GameObjects is not deterministic.
  • Early Setup: Since Awake() is called before any Start() 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-assign targetPoint when the component is added or reset. Key line: targetPoint = GameObject.FindWithTag("Waypoint")?.transform;
  • Validation in OnValidate(): Clamp the waypoint’s position within xBounds and zBounds whenever it’s edited in the Inspector. Key pattern: Mathf.Clamp on tp.x and tp.z.
  • Early Initialization in Awake(): Reset the hasArrived 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(): Uses Vector3.MoveTowards to smoothly move along both X and Z axes. Arrival check: When distance < 0.01 units, stop and log arrival.

Implementation:

  1. Attach the script below to your quadruped GameObject (Spot). Make sure the quadruped robot GameObject has a Renderer

     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.");
                 }
             }
         }
     }
    
  2. Create an empty GameObject and tag it object as "Waypoint". Alternatively, drag it or any Transform into Target Point.
  3. Adjust Speed, Rotation Speed, xBounds, and zBounds in the Inspector.

    F06

  4. Play the scene—watch your robot walk to the target and stop on arrival.

    F07

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.

F09


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. Uses Rigidbody.MoveRotation and MovePosition 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 all Update() calls. Repositions and orients the camera to trail the robot’s final pose without jitter.

Implementation:

  1. Update your SpotWalker.cs so that all movement and turning runs in FixedUpdate() using a Rigidbody. Replace the old Update() 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.");
                 }
             }
         }
     }
    
  2. Add a Rigidbody component to your quadruped and freeze its X and Z rotations. Ensure it also has an Animator.

    F13

  3. Create FollowSpotCamera.cs and attach to your Main 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);
         }
     }
    
  4. In the Inspector, drag your robot’s Transform into Quadruped, tweak Offset and Smooth Speed.

    F14

  5. 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 in LateUpdate().

    F15


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).

F10


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-kinematic Rigidbody starts colliding with another collider. It passes a Collision object containing rich information about the contact. Key features include collision data through a Collision 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 disabled MonoBehaviour 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 a Rigidbody component (typically the moving object), with both having Colliders. This method is executed during the FixedUpdate() cycle, and the other 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 a Rigidbody 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 with OnTriggerEnter() or OnTriggerStay(). It provides exit detection, signaling that an object has left the trigger zone, and requires at least one of the two colliders to have a Rigidbody. 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:

  1. Update the DroneController.cs Script:
    • Update the existing DroneController.cs script attached to your Drone (must have a Rigidbody) as follows.
    • This script handles movement in FixedUpdate(), fan rotation & scan‐UI in Update(), and scanning via OnTriggerEnter().
     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}");
             }
         }
     }
    
  2. Add a Front–Mount Trigger Collider:
    • Under your Drone GameObject, create an empty child named Scanner and position it at the drone’s nose (where you want it to “scan” forward). Also, make sure to disable the drone’s Animator.
    • On the Scanner child, add a SphereCollider component.
    • In the SphereCollider settings, check Is Trigger.
    • Adjust the Radius so it just encapsulates the front of the drone without overlapping too far. This trigger will fire OnTriggerEnter() in your DroneController when it overlaps any StorageBox.
    • In the Inspector, set each rack box’s Tag to StorageBox.

    F16

  3. Add a Scan UI Text:
    • Create a Screen Space - Overlay UI Canvas, add UI > 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 your DroneController script in the Inspector.

    F17

  4. 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);
         }
     }
    
  5. Configure the Script:
    • Drag your drone’s Transform into the Drone field on the FollowDroneCamera component.
    • Tweak Local Offset (X: right/left, Y: height, Z: distance) to position the camera, and adjust Smooth Speed for responsiveness.

    F18

  6. 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().

    F19


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.

F11


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:

  1. Create a new script named BoxVisibilityMonitor.cs and attach it to each StorageBox prefab/instance (make sure the box has a Renderer component). OnBecameVisible() fires once when the box’s Renderer 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}");
             }
         }
     }
    
  2. Attach BoxVisibilityMonitor.cs to a box GameObject.
  3. Select the box and in the BoxVisibilityMonitor component, drag the Text (TMP) UI element into the Visibility Text field.

    F20

  4. 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() and OnBecameInvisible().

    F21


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.

F12


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() in DroneController.cs for cleanup/logging when controls are locked, and OnEnable() to signal reactivation.

Implementation:

  1. Add OnDisable() / OnEnable() methods to your existing DroneController.cs:

     public class DroneController : MonoBehaviour
     {
         // ... existing code ...
    
         void OnEnable()
         {
             Debug.Log("DroneController enabled - controls active.");
         }
    
         void OnDisable()
         {
             Debug.Log("DroneController disabled - controls locked.");
         }
     }
    
  2. Create DroneLifecycleManager.cs and attach it to the same Drone 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.");
         }
     }
    
  3. Attach the DroneLifecycleManager component to your Drone GameObject (alongside DroneController).
  4. In the DroneLifecycleManager inspector, drag the existing DroneController component into the Drone Controller field (it should auto-detect if left blank).
  5. Adjust Landing Height and Speed to control how low/far and how fast your drone descends and ascends.

    F22

  6. 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 the OnDisable() log from DroneController.
  7. 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 the OnEnable() log from DroneController.

    F23


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.