Node

Unit of the Graph

In a game engine's scene graph, a Node is the fundamental building block used to organize and represent everything within the game's world.

Every Node maintains ownership of its child Nodes (if any) and, unless they are root Node, always has a parent Node, realizing a tree-like structure.

Node Basics

#pragma once

#include <cstddef>
#include <memory>
#include <string>
#include <vector>

#include "derived.h"

namespace ng {

class App;

/// @brief The base class for all entities in the game world, forming a scene graph.
///        Manages local and global transformations, parent-child relationships, and rendering layers.
class Node {
 public:
  /// @brief Constructs a Node associated with a specific App instance.
  /// @param app A pointer to the App instance this node belongs to. This pointer must not be null.
  explicit Node(App* app);
  virtual ~Node() = default;

  Node(const Node& other) = delete;
  Node& operator=(const Node& other) = delete;
  Node(Node&& other) = delete;
  Node& operator=(Node&& other) = delete;

  /// @brief Returns the name of the node.
  /// @return A constant reference to the node's name.
  [[nodiscard]] const std::string& GetName() const;

  /// @brief Sets the name of the node.
  /// @param name The new name for the node.
  void SetName(std::string name);

  /// @brief Returns the App instance associated with this node.
  /// @return A pointer to the App instance. This pointer is never null after construction.
  [[nodiscard]] App* GetApp() const;

  /// @brief Returns the parent node in the scene graph. Can be null if this is the root node or not yet added as a child.
  /// @return A pointer to the parent Node, or null if there is no parent.
  [[nodiscard]] Node* GetParent() const;

  /// @brief Adds a new child node to this node. Ownership of the child is transferred.
  /// @param new_child A unique pointer to the Node to be added. This pointer must not be null.
  void AddChild(std::unique_ptr<Node> new_child);

  /// @brief Creates and adds a new child node of the specified type to this node.
  /// @tparam T The type of the Node to create, must derive from Node.
  /// @tparam Args The constructor arguments for the Node type T.
  /// @param args The arguments to forward to the constructor of T.
  /// @return A reference to the newly created and added Node.
  template <Derived<Node> T, typename... Args>
  T& MakeChild(Args&&... args) {
    auto child = std::make_unique<T>(app_, std::forward<Args>(args)...);
    T& ref = *child;
    AddChild(std::move(child));
    return ref;
  }

  /// @brief Schedules a child node for destruction. The actual removal happens at the beginning of the next frame.
  /// @param child_to_destroy A constant reference to the child Node to be destroyed.
  void DestroyChild(const Node& child_to_destroy);

  /// @brief Schedules this node for destruction. The actual removal happens at the beginning of the next frame by its parent.
  void Destroy();

  /// @brief Returns the first child node of a specific type.
  /// @tparam T The type of the child Node to retrieve, must derive from Node.
  /// @return A pointer to the first child of type T, or nullptr if no such child exists.
  template <Derived<Node> T>
  [[nodiscard]] T* GetChild() {
    for (const auto& child : children_) {
      T* c = dynamic_cast<T*>(child.get());
      if (c != nullptr) {
        return c;
      }
    }
    return nullptr;
  }

private:
  // The name of the node.
  std::string name_;
  
  // Pointer to the App instance. Never null after construction.
  App* app_ = nullptr;

  // Pointer to the parent node in the scene graph. Can be null for the root.
  Node* parent_ = nullptr;

  // Vector of child nodes. Ownership is managed by this node.
  std::vector<std::unique_ptr<Node>> children_;
  // Indices of children to be erased in the next update cycle.
  std::vector<size_t> children_to_erase_;
  // Vector of children to be added in the next update cycle.
  std::vector<std::unique_ptr<Node>> children_to_add_;
};

}  // namespace ng

Node creation involves either manually creating a unique pointer to the desired type and then attaching it as a child to a parent Node, or using the parent Node's MakeChild function, which streamlines this process.

A crucial aspect of the Node system is its handling of destruction: when a Node is destroyed, it triggers a recursive destruction of all its children, ensuring that the entire hierarchical branch originating from the destroyed Node is properly cleaned up.

Derived Concept

The Derived C++ Concept is employed to restrict the allowable types for the MakeChild and GetChild methods. This enforcement of stricter static type checking in these templated methods helps catch type errors at compile time.

Local and Global Transforms

A node's local transform defines its position, rotation, and scale relative to its own origin. In contrast, the global transform describes its position, rotation, and scale within the world coordinate system. For example, if node A is positioned at world coordinates (0, 10) and has a child node B with a local position of (2, 5), node A's local and global transforms are identical because it has no parent influencing its world position. However, node B's local position (2, 5) differs from its global position, which would be (2, 15) (assuming no rotation or scaling). This is because B's global position is calculated by adding its local position to its parent's global position. To optimize performance, the global transform is computed on demand, only when its value is explicitly requested. This lazy computation avoids unnecessary updates after every translation, rotation, or scale operation.

Node Lifecycle

Nodes within the scene often have specific functions (methods) that the engine calls automatically at different points in their lifecycle. The exact names vary greatly between engines (e.g., Unity uses Awake, Start, Update, OnDestroy; Godot uses _ready, _process, _draw, _exit_tree), but the concepts are similar:

  1. OnAdd (Initialization):

    • When: Called shortly after the object is added to the active scene tree and is ready. It runs once per object lifecycle.

    • Purpose: Used for one-time setup tasks like initializing variables, getting references to other objects or components, etc.

  2. Update (Logic):

    • When: Called every single tick while the object is active in the scene.

    • Purpose: This is where most ongoing game logic happens: handling input, moving the object, checking collisions, running AI logic, updating animations, etc.

  3. Draw (Rendering):

    • When: Called during the rendering phase of each frame for objects that need to be drawn.

    • Purpose: Responsible for issuing commands to draw the object's visual representation (sprite, model, UI element) on the screen.

  4. OnDestroy (Cleanup):

    • When: Called just before the object is permanently removed from the scene (either destroyed individually or because the whole scene is being unloaded).

    • Purpose: Used for cleanup tasks: releasing resources, saving data, disconnecting from event systems, and notifying other objects if necessary.

Scheduled Add and Destroy

You may have noticed that in the AddChild and DestroyChild methods, the node being added/destroyed is not processed right away, but rather scheduled to be processed. If you were to add or remove a Node directly within the Update method, it could happen while the engine (or another part of your code) is currently iterating over the list or tree structure containing that parent node.

Adding and removing an item from a collection while iterating over it can invalidate the iterator. The loop might crash, skip the next element, or exhibit other unpredictable (undefined) behavior because the structure it's traversing has changed unexpectedly underneath it.

To avoid these problems, node additions and removals are scheduled for the next tick of execution. When you call AddChild and DestroyChild, the engine doesn't modify the scene graph immediately. Instead, it puts the request into a queue.

Only after all the main processing and potentially dangerous iterations for the current frame are complete, the engine processes these queued requests and actually modifies the scene graph structure.

Last updated