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.

F02

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:

  • Integration with Unity’s Lifecycle: MonoBehaviour provides built-in event methods, such as Awake(), Start(), Update(), and FixedUpdate() that Unity automatically invokes at specific times. Unity handles when these are called, so you don’t need to write manual polling logic. 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.");
          }
    
          void Update()
          {
              Debug.Log("Monitoring drone behavior...");
          }
      }
    
  • Component-Based Architecture: Unity’s architecture is built around GameObjects and their attached components. A MonoBehaviour script is a custom component that defines behavior and can be attached to any GameObject in the scene. This modular design allows you to add, remove, or modify functionality without altering other parts of the system—perfect for building scalable simulations like XFactory. Once written, a MonoBehaviour script can be reused across multiple GameObjects. This promotes code reuse 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.

      public class ForkliftController : MonoBehaviour
      {
          public float moveSpeed = 5f;
    
          void Update()
          {
              // Move the forklift forward continuously
              transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
          }
      }
    
  • Simplified Initialization and Event Handling: Instead of traditional constructors, MonoBehaviour uses lifecycle methods (e.g., Awake() for setup and Start() for initialization) that are called at the appropriate time by Unity. This ensures that your code executes when the GameObject is fully set up and all components are loaded.

A script is just a blueprint for behavior. That behavior is not active until the script is attached to a GameObject in your scene. The script will appear in the Inspector, where its public fields can be viewed or edited directly, and Unity will begin calling its relevant methods automatically during runtime.

Properties

Properties (or serialized fields) represent variables that define the state or configuration of a MonoBehaviour component. These values are often made public or marked with [SerializeField] so they can be set directly in the Unity Inspector, without exposing them to other scripts. This approach allows designers and developers to tweak simulation behavior—like speed, capacity, or thresholds—without modifying code. For example, a forklift in the logistics station can move forward when triggered by user input or an automation script, while its speed is controllable through the Inspector but not from other scripts:

using UnityEngine;

public class ForkliftMotion : MonoBehaviour
{
    [SerializeField]
    private float moveSpeed = 3f; // Editable in the Inspector but not directly from other scripts

    void Update()
    {
        if (Input.GetKey(KeyCode.Space))
        {
            transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime);
        }
    }
}

F03

Public Methods

Public methods are functions declared within your MonoBehaviour that can be called by other scripts or through Unity’s event messaging system. They define the actions that your component can perform and are essential for implementing game logic. For example, a welding robot can use a StartWelding() method that begins its programmed sequence. Unity’s InvokeRepeating() might repeatedly call it based on a timing condition.

using UnityEngine;

public class WeldingRobot : MonoBehaviour
{
    // Method that logs when the welding sequence starts
    public void StartWelding()
    {
        Debug.Log("Starting welding sequence...");
    }

    // Unity method called once when the GameObject is enabled
    void Start()
    {
        // Calls StartWelding() after 2 seconds, then repeats every 4 seconds
        InvokeRepeating("StartWelding", 2f, 4f);
    }
}

F04

Static Methods

Static methods belong to the class itself, not to an instance. This means you can call them without attaching the script to a GameObject or creating an object. They are ideal for utility functions, like logging, formatting, or shared calculations used across multiple systems. Static methods are especially useful for creating centralized, reusable functionality that doesn’t depend on GameObject instances. In XFactory, a static utility class could be used to log important actions from any machine, robot, or station—ensuring consistent logging format across the simulation.

using UnityEngine;

public static class FactoryLogger
{
    // Logs a standardized message from any part of the simulation
    public static void LogAction(string source, string message)
    {
        Debug.Log($"[XFactory - {source}] {message}");
    }
}

// Usage examples:
FactoryLogger.LogAction("WeldingRobot", "Completed weld cycle.");
FactoryLogger.LogAction("Forklift", "Delivered package to Assembly Station.");

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.
    • 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 Space key
             if (Input.GetKeyDown(KeyCode.Space) && droneAnimator != null)
             {
                 if (isFlying)
                 {
                     droneAnimator.SetTrigger("ReturnToHover");
                     isFlying = false;
                 }
                 else
                 {
                     droneAnimator.SetTrigger("StartFlying");
                     isFlying = true;
                 }
             }
    
             // Toggle Eye animator on/off with 'P'
             if (Input.GetKeyDown(KeyCode.P) && 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 that Unity calls automatically when specific events occur. Unlike methods you call manually, messages are triggered 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 automatically invoked in the Unity Editor. It 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 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: In XFactory, you might use Reset() to auto-link the logistics drone to its navigation target when first added. 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 when the component is first added or reset in the Inspector.

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 mobile robot remains within the defined X-Z boundaries of the assembly station area. Attach this script to the mobile robot GameObject in the assembly station (AGV_Omniwheel). OnValidate() runs in the Editor, so it helps enforce boundary limits immediately when the robot is moved or adjusted in the Inspector—no need to run the game to see it applied.

using UnityEngine;

public class MobileRobotBounds : MonoBehaviour
{
    // Define the boundary limits for the assembly station (in world space)
    public Vector2 xBounds = new Vector2(-10f, 10f);
    public Vector2 zBounds = new Vector2(-5f, 5f);

    private void OnValidate()
    {
        Vector3 pos = transform.position;

        // Clamp position within defined bounds
        pos.x = Mathf.Clamp(pos.x, xBounds.x, xBounds.y);
        pos.z = Mathf.Clamp(pos.z, zBounds.x, zBounds.y);

        // Apply constrained position back to the transform
        transform.position = pos;
    }
}

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: In XFactory’s logistics station, a drone can initialize its status and visual material when the scene loads. Attach this script to the drone GameObject (Drone) that needs a visual status indicator. Make sure the drone has a Renderer component (e.g., MeshRenderer) on the same GameObject so GetComponent<Renderer>() works correctly during Awake().

public class DroneStatusIndicator : MonoBehaviour
{
    private Renderer droneRenderer;
    private string status;

    void Awake()
    {
        // Get the renderer to change the material later
        droneRenderer = GetComponent<Renderer>();
        
        // Initialize the default status
        status = "Idle";

        Debug.Log("Awake: Drone status set to Idle.");
    }
}

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 could activate the quadruped robot’s Spot_Controller animator when the GameObject becomes active. Attach this script to the quadruped robot GameObject (Spot) in the exhibit station that has the Animator component controlling the Spot_Controller state. OnEnable() ensures the animation starts whenever the robot is activated in the scene or re-enabled in play mode.

public class DemoRobotActivator : MonoBehaviour
{
    private Animator spotAnimator;

    void OnEnable()
    {
        spotAnimator = GetComponent<Animator>();
        if (spotAnimator != null)
        {
            spotAnimator.Play("Spot_Controller");
            Debug.Log("OnEnable: Quadruped animator activated.");
        }
    }
}

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.

Example: You can use Start() in the manufacturing station to launch a delayed CNC machine warm-up sequence followed by activating its operational sound. Attach this script to the CNC machine GameObject (CNC_Mill_Set) in the manufacturing station. Ensure the GameObject has an AudioSource component with the machine sound assigned—Start() will trigger a warm-up delay before playback begins automatically during runtime.

using UnityEngine;

public class CNCMachine : MonoBehaviour
{
    private AudioSource cncAudio;

    void Start()
    {
        Debug.Log("Start: Initializing CNC warm-up sequence.");

        // Get the AudioSource component on the CNC machine
        cncAudio = GetComponent<AudioSource>();

        // Begin warm-up coroutine
        StartCoroutine(WarmUpRoutine());
    }

    private System.Collections.IEnumerator WarmUpRoutine()
    {
        yield return new WaitForSeconds(5f);

        Debug.Log("CNC Machine ready. Starting operational sound.");

        // Start playing the machine sound
        if (cncAudio != null)
        {
            cncAudio.Play();
        }
    }
}

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(-10f, 10f);
         public Vector2 zBounds = new Vector2(-5f, 5f);
    
         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 script below can use a timer to track elapsed time between drone scan cycles. It prints a status message every second. Attach this script to a drone GameObject (Drone) in the logistics station. It should remain active during runtime, as Update() depends on real-time frame updates to track and log scan cycle intervals every second.

using UnityEngine;

public class DroneScannerStatus : MonoBehaviour
{
    private float timer = 0.0f;

    void Awake()
    {
        Debug.Log("Awake: Drone scanner initialized.");
    }

    void Start()
    {
        Debug.Log("Start: Beginning scan cycle monitoring.");
    }

    void Update()
    {
        timer += Time.deltaTime;

        if (timer > 1.0f)
        {
            timer = 0.0f;
            Debug.Log("Update: Drone scanner cycle complete.");
        }
    }
}

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.

using UnityEngine;

public class FollowQuadruped : MonoBehaviour
{
    public Transform quadruped;
    public Vector3 offset = new Vector3(0f, 2f, -6f);

    void LateUpdate()
    {
        if (quadruped != null)
        {
            transform.position = quadruped.position + offset;
            transform.LookAt(quadruped);
        }
    }
}

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 heavy box (Box_Large_01a_Prefab_01) on the scale (Industrial_Scale_01a) can use FixedUpdate() to continuously report weight (mass × gravity) to be projected on the scale’s UI display. Attach this script to the heavy box GameObject (Box_Large_01a_Prefab_01) that sits on the industrial scale. Ensure the box has a Rigidbody component, as FixedUpdate() is used for accurate physics-based calculations and real-time weight reporting synced with the physics engine.

using UnityEngine;

public class BoxWeightReporter : MonoBehaviour
{
    private Rigidbody rb;

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

    void FixedUpdate()
    {
        float simulatedWeight = rb.mass * Physics.gravity.magnitude;
        Debug.Log($"[Logistics Scale] Weight reading: {simulatedWeight} N");
    }
}

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 FollowQuadrupedCamera.cs and attach to your Main Camera:

     using UnityEngine;
    
     public class FollowQuadrupedCamera : MonoBehaviour
     {
         [Tooltip("Drag your robot’s Transform here")]
         public Transform quadruped;
         [Tooltip("Offset relative to robot position")]
         public Vector3 offset = new Vector3(0f, 2f, -6f);
    
         void LateUpdate()
         {
             if (quadruped == null) return;
             transform.position = quadruped.position + offset;
             transform.LookAt(quadruped);
         }
     }
    
  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(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: Receives a Collision object, which includes contact points, impact velocity, and other collision details.
  • Physics Interaction: Only triggers when one of the colliding objects has a non-kinematic rigidbody.
  • Performance Optimization: If collision details are not needed, omitting the parameter can avoid unnecessary calculations.
  • Event Propagation: Collision events are sent to disabled MonoBehaviour scripts, allowing for behavior activation in response to collisions.

Example: If the drone collides with storage boxes on the rack in the logistics station, an alert can be triggered. Attach this script to the drone GameObject (Drone) in the logistics station that can collide with storage boxes. Make sure the storage boxes are tagged as StorageBox and that both the drone and boxes have appropriate colliders and Rigidbody components (at least one non-kinematic) to enable collision detection.

using UnityEngine;

public class DroneCollisionHandler : MonoBehaviour
{
    void OnCollisionEnter(Collision collision)
    {
        // Check if the drone collided with a storage box
        if (collision.gameObject.CompareTag("StorageBox"))
        {
            Debug.Log($"[Drone] Collided with: {collision.gameObject.name}");

            // Visualize contact points in the Scene view
            foreach (ContactPoint contact in collision.contacts)
            {
                Debug.DrawRay(contact.point, contact.normal, Color.red, 2f);
            }
        }
    }
}

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, allowing continuous handling of collision events.
  • Detailed Collision Data: Provides a Collision object with information such as contact points and relative velocity.
  • Performance Consideration: If collision details are not needed, omitting the parameter avoids extra computations.
  • Non-Kinematic Requirement: Collision events are sent only if one of the colliding objects has a non-kinematic rigidbody.
  • Sleeping Rigidbodies: Collision stay events are not triggered for rigidbodies that are sleeping.

Example: If the mobile robot presses against a wall in the assembly station, an alarm can be generated. Attach this script to the mobile robot GameObject in the assembly station (AGV_Omniwheel). To detect sustained wall contact, ensure the robot and wall objects have colliders, and at least one has a Rigidbody. OnCollisionStay() will continuously monitor the collision, with a velocity threshold to trigger alerts for pressing incidents.

void OnCollisionStay(Collision collision)
{
    foreach (ContactPoint contact in collision.contacts)
    {
        Debug.DrawRay(contact.point, contact.normal, Color.yellow);
    }

    if (collision.relativeVelocity.magnitude > 1)
    {
        Debug.Log("Sustained contact with: " + collision.gameObject.name);
    }
}

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 the contact between two colliding objects ceases.
  • Detailed Collision Data: Provides a Collision object with information such as contact points and impact velocity, though it is often used simply to signal the end of a collision.
  • Non-Kinematic Requirement: Collision events, including exit events, are only sent if one of the colliding objects has a non-kinematic rigidbody.
  • Performance Consideration: As with other collision events, omit the collision parameter if detailed collision data is not needed to reduce extra computations.

Example: In XFactory’s logistics station, the drone can log when it has finished colliding with a storage box. Attach this script to the drone GameObject (Drone) in the logistics station. Ensure storage boxes are tagged as StorageBox, and that proper colliders and Rigidbody setup are in place so Unity can invoke OnCollisionExit() when the drone stops touching a box.

void OnCollisionExit(Collision collision)
{
    if (collision.gameObject.CompareTag("StorageBox"))
    {
        Debug.Log($"[Drone] Cleared collision with: {collision.gameObject.name}");
    }
}

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. Key features include:

  • Trigger-Based Interaction: One of the colliders must be marked as a trigger.
  • Physics Setup Required: At least one object must have a Rigidbody component (typically the moving object), and both must have Colliders.
  • Timing: Executed during the FixedUpdate() cycle.
  • Collider Reference: The other parameter refers to the collider of the object that entered the trigger.

Example: In XFactory’s logistics station, the forklift (Reach_Truck_01a_Prefab) detects when a large box has been loaded onto it. Attach this script to the forklift GameObject (Reach_Truck_01a_Prefab) and ensure it has a trigger collider (set isTrigger = true) to detect incoming boxes. The large box GameObjects must be tagged as LargeBox and have Collider (and optionally Rigidbody) for the OnTriggerEnter() event to register correctly.

void OnTriggerEnter(Collider other)
{
    if (other.CompareTag("LargeBox"))
    {
        Debug.Log("[Forklift] Large box loaded: " + other.name);
        // Optionally update UI, trigger animation, or start delivery logic
    }
}

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. Key features include:

  • Continuous Detection: Called repeatedly while the other collider stays inside the trigger.
  • Rigidbody Required: At least one object must have a Rigidbody component.
  • Useful for State Monitoring: Can be used to check ongoing conditions like object stability, valid position, or contact duration.

Example: In XFactory’s logistics station, a forklift trigger zone can detect whether it is still carrying a large box. Attach this script to a trigger collider on the forklift (Reach_Truck_01a_Prefab). Ensure the large boxes are tagged as LargeBox and have Collider (and optionally Rigidbody). OnTriggerStay() will continuously run while the box remains inside the trigger, allowing for persistent state checks or logic like box-locking or delivery countdowns.

void OnTriggerStay(Collider other)
{
    if (other.CompareTag("LargeBox"))
    {
        Debug.Log("[Forklift] Still carrying box: " + other.name);
        // Optional: Display status, lock box in position, or track 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(). Key features include:

  • Exit Detection: Signals that an object has left the trigger zone.
  • Requires Rigidbody: At least one of the two colliders must have a Rigidbody.
  • Complements Entry Events: Pairs well with OnTriggerEnter() and OnTriggerStay() for full lifecycle control.
  • Silent Exit: If the object is destroyed or deactivated inside the trigger, no exit event will occur.

Example: In XFactory’s logistics station, we can detect when the forklift offloads a box it was previously carrying. Attach this script to the forklift (Reach_Truck_01a_Prefab). Make sure the large boxes are tagged as LargeBox and have Collider (and optionally Rigidbody). OnTriggerExit() will detect when a box leaves the cargo zone, useful for tracking offloads or triggering inventory updates.

void OnTriggerExit(Collider other)
{
    if (other.CompareTag("LargeBox"))
    {
        Debug.Log("[Forklift] Box unloaded: " + other.name);
        // Optional: Update inventory system or trigger unloading animation
    }
}

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;
         public float rotationSpeed = 100f;
    
         [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;
    
         void Awake()
         {
             rb = GetComponent<Rigidbody>();
             rb.constraints = RigidbodyConstraints.FreezeRotationX
                         | RigidbodyConstraints.FreezeRotationZ;
    
             Transform 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()
         {
             // Clear scan text after the display duration
             if (!string.IsNullOrEmpty(scanText.text))
             {
                 scanTimer += Time.deltaTime;
                 if (scanTimer >= displayDuration)
                 {
                     scanText.text = "";
                     scanTimer = 0f;
                 }
             }
    
             // Rotate fans
             float fanSpeed = isFlying ? activeFanSpeed : idleFanSpeed;
             if (fanTransforms != null)
             {
                 foreach (var fan in fanTransforms)
                     fan.Rotate(Vector3.forward * fanSpeed * Time.deltaTime);
             }
         }
    
         void FixedUpdate()
         {
             // Read movement input
             float forward = Input.GetKey(KeyCode.W) ? 1f :
                             Input.GetKey(KeyCode.S) ? -1f : 0f;
             float strafe  = Input.GetKey(KeyCode.D) ? 1f :
                             Input.GetKey(KeyCode.A) ? -1f : 0f;
             float turn    = Input.GetKey(KeyCode.E) ? 1f :
                             Input.GetKey(KeyCode.Q) ? -1f : 0f;
    
             isFlying = Mathf.Abs(forward) + Mathf.Abs(strafe) + Mathf.Abs(turn) > 0f;
    
             // Yaw rotation
             Quaternion deltaRot = Quaternion.Euler(
                 0f,
                 turn * rotationSpeed * Time.fixedDeltaTime,
                 0f
             );
             rb.MoveRotation(rb.rotation * deltaRot);
    
             // Translation
             Vector3 move = (transform.forward * forward + transform.right * strafe).normalized;
             rb.MovePosition(rb.position + move * speed * Time.fixedDeltaTime);
         }
    
         void OnTriggerEnter(Collider other)
         {
             if (other.CompareTag("StorageBox") && scanText != null)
             {
                 string name = other.name;
                 scanText.text = $"Scanned: {name}";
                 scanTimer = 0f;
                 Debug.Log($"[Drone] Scanned: {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) powers on 2 seconds after becoming visible to the camera. Attach this script to the Display GT screen GameObject in the assembly station. Ensure the screen has a child Canvas (for UI elements) and a Renderer component so OnBecameVisible() is triggered correctly when the camera sees it. The Canvas will activate 2 seconds after the screen enters the camera’s view.

using UnityEngine;
using UnityEngine.UI;

public class DisplayScreenController : MonoBehaviour
{
    private Canvas displayCanvas;

    void Start()
    {
        // Find and disable the Canvas component at start
        displayCanvas = GetComponentInChildren<Canvas>(true);
        displayCanvas.enabled = false;
    }

    void OnBecameVisible()
    {
        Debug.Log("[DisplayScreen] Became visible — scheduling power-on.");
        Invoke(nameof(ActivateDisplay), 2f); // Delay activation by 2 seconds
    }

    void ActivateDisplay()
    {
        displayCanvas.enabled = true;
        Debug.Log("[DisplayScreen] Canvas activated.");
    }
}

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.

Example: In XFactory’s exhibit station, the quadruped robot uses a script called SpotWalker.cs. Its walking animation can be paused when it leaves the camera’s view, and resumed when it becomes visible again. Attach this script to the quadruped robot GameObject (Spot) in the exhibit station. It must have an Animator component and a Renderer (e.g., MeshRenderer), as OnBecameVisible() and OnBecameInvisible() are only triggered when the GameObject’s renderer enters or exits the camera’s view.

using UnityEngine;

public class SpotWalker : MonoBehaviour
{
    private Animator animator;

    void Start()
    {
        animator = GetComponent<Animator>();
    }

    void OnBecameVisible()
    {
        Debug.Log("[SpotWalker] Visible — animation resumed.");
        animator.enabled = true;
    }

    void OnBecameInvisible()
    {
        Debug.Log("[SpotWalker] Not visible — animation paused.");
        animator.enabled = false;
    }
}

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., L) can toggle the CNC machine’s work lights (Spot Light_01, …, Spot Light_04). When lights are disabled, OnDisable() ensures a message is logged and the system shuts down cleanly. Attach this script to the CNC machine GameObject (CNC_Mill_Set) in the manufacturing station. In the Inspector, assign all four spotlights (Spot Light_01, …, Spot Light_04) to the spotLights array. Update() handles the light toggling on key press (L), while OnDisable() ensures proper shutdown behavior when the script or GameObject is deactivated.

using UnityEngine;

public class CNCLightController : MonoBehaviour
{
    public GameObject[] spotLights; // Assign Spot Light_01 to Spot Light_04 in Inspector
    private bool lightsOn = false;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.L))
        {
            lightsOn = !lightsOn;
            foreach (GameObject light in spotLights)
            {
                light.SetActive(lightsOn);
            }
        }
    }

    void OnDisable()
    {
        Debug.Log("CNC light system disabled — all spotlights turned off.");
    }
}

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: Imagine the mobile robot in the assembly station is dynamically instantiated at runtime. When the user switches to another scene (e.g., from assembly to exhibit), OnDestroy() handles cleanup—saving progress or reporting its removal. Attach this script to the mobile robot GameObject in the assembly station. OnDestroy() ensures cleanup actions (like saving state or logging removal) occur when the robot is destroyed during scene transitions. Use the Space key in Update() to simulate scene changes for testing.

using UnityEngine;
using UnityEngine.SceneManagement;

public class RobotDestructionLogger : MonoBehaviour
{
    void OnEnable()
    {
        Debug.Log("Mobile robot initialized and ready.");
    }

    void OnDestroy()
    {
        Debug.Log("Mobile robot instance is being destroyed. Logging final state.");
        // Save robot progress, disconnect from navigation system, etc.
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Debug.Log("Scene change triggered. Mobile robot will be destroyed.");
            SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
        }
    }
}

Tutorial

Let’s extend oue drone 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.