Defining a Custom Controller

From C4 Engine Wiki
Revision as of 01:19, 8 December 2023 by Eric Lengyel (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

In the C4 Engine, a controller is attached to a node to give it some kind of dynamic behavior. A controller is represented by a subclass of the Controller class and contains all of the information needed to move or change an object in some way. It is often the case that a controller is assigned to a node and configured in the World Editor under the Controller tab in the Node Info window.

Defining a Controller Subclass

In this article, we will use the example of a controller called TwistController that simply twists a node around its z axis. Note that this particular functionality already exists as part of the more general built-in Spin Controller, so the example here is for illustrative purposes only.

A custom controller subclass needs to have a unique type identifier that is registered with the engine so that the World Editor knows about it. The type identifier is a 32-bit number normally represented by a four-character string as in the following code.

enum
{
    kControllerTwist = 'twst'
};

(Type identifiers consisting of only uppercase letters and numbers are reserved for use by the engine. Anything else is okay for an application to use.)

Next, the controller subclass needs to be defined. We declare the TwistController class to be a subclass of the Controller class as follows. We include some data members, one of which holds the rate at which the target node should twist. Several member functions are included to handle serialization and user interface—these are discussed below.

class TwistController : public Controller
{
   private:

        float         twistRate;             // In radians per millisecond.
        float         twistAngle;            // The current angle, in radians.
        Transform4D   originalTransform;     // The target's original transform.

        TwistController(const TwistController& twistController);

        Controller *Replicate(void) const;

   public:

        TwistController();
        TwistController(float rate);
        ~TwistController();

        float GetTwistRate(void) const
        {
            return (twistRate);
        }

        void SetTwistRate(float rate)
        {
            twistRate = rate;
        }

        static bool ValidNode(const Node *node);

        // Serialization functions.
        void Pack(Packer& data, uint32 packFlags) const override;
        void Unpack(Unpacker& data, uint32 unpackFlags) override;

        // User interface functions.
        void BuildSettingList(List<Setting> *settingList) const override;
        void CommitSetting(const Setting *setting) override;

        void PreprocessController(void);

        // The function that moves the target node.
        void MoveController(void);
};

The constructor and destructor for this example would typically be implemented as follows.

TwistController::TwistController() : Controller(kControllerTwist)
{
    // Set a default value for the twist rate of one revolution per second.
    twistRate = Math::two_pi / 1000.0F;
    twistAngle = 0.0F;
}

TwistController::TwistController(float rate) : Controller(kControllerTwist)
{
    twistRate = rate;
    twistAngle = 0.0F;
}

TwistController::~TwistController()
{
}

Notice that the controller's type kControllerTwist is passed to the base class's constructor.

The default constructor only needs to set a default value if the controller will be exposed in the World Editor. Otherwise, the default constructor will only be called right before the controller is deserialized, thus filling in the data members.

The copy constructor and the Replicate() function must be included for all controllers. For this example, the copy constructor would be implemented as follows.

TwistController::TwistController(const TwistController& twistController) : Controller(twistController)
{
    twistRate = twistController.twistRate;
    twistAngle = 0.0F;
}

It's important to observe that the reference to the copied TwistController object is passed to the base class constructor this time instead of the controller's type identifier.

The Replicate() function simply constructs a new instance of the controller using the copy constructor. This would usually be implemented as follows.

Controller *TwistController::Replicate(void) const
{
    return (new TwistController(*this));
}

Controller Registration

A custom controller type must be registered with the engine in order to be recognized by the World Editor. This is accomplished by creating a ControllerReg object. The controller registration contains information about the controller type and its name, and it's mere existence registers the controller type that it represents. A controller registration for the TwistController class would normally look like the following.

// Define the controller registration.
ControllerReg<TwistController> twistControllerReg;

The registration object is initialized with the following code.

twistControllerReg(kControllerTwist, "Twist");

The ValidNode() function declared in the TwistController class is used by the World Editor to determine what kind of node the controller was meant to be assigned to. If this function is not included in the class definition, then the controller can be assigned to any node. Otherwise, the ValidNode() function should return true when it's okay to assign the controller to the node passed to it, and false if it's not okay. As an example, if we only wanted TwistController objects to be assigned to geometry nodes, then we would implement the ValidNode() function as follows.

bool TwistController::ValidNode(const Node *node)
{
    return (node->GetNodeType() == kNodeGeometry);
}

Notice that the ValidNode() function is declared static. The World Editor calls this function automatically when it needs to know whether the controller can be used with a specific node.

Serialization

A custom controller must implement the Pack() and Unpack() functions so that its data can be written to a file and later restored. (These functions override the virtual functions in the Packable class.) Each of these functions needs to first call its counterpart in the Controller base class. For the TwistController example, these functions would typically be implemented as follows.

void TwistController::Pack(Packer& data, uint32 packFlags) const
{
    Controller::Pack(data, packFlags);

    // Write the twist rate.
    data << twistRate;

    // Write the current angle.
    data << twistAngle;

    // Write the original transform.
    data << originalTransform;
}

void TwistController::Unpack(Unpacker& data, uint32 unpackFlags)
{
    Controller::Unpack(data, unpackFlags);

    // Read the twist rate.
    data >> twistRate;

    // Read the current angle.
    data >> twistAngle;

    // Read the original transform.
    data >> originalTransform;
}

User Interface

The Controller class is a subclass of the Configurable class, which means it can expose a user interface that appears in the World Editor. The Controller object is queried by the World Editor for its configurable settings when the user opens the Node Info window for a node with a controller attached to it. The BuildSettingList() function is called when the editor needs all of the configurable settings to be built, and the CommitSetting() function is called for each setting when the user confirms the new configuration. For the TwistController example, there is one setting representing the twist rate stored in the controller object. The user interface to change this value would be implemented as follows.

void TwistController::BuildSettingList(List<Setting> *settingList) const
{
    // There is one setting, and it's an editable text field.
    settingList->AppendListElement(new TextSetting('rate', "Twist rate", Text::FloatToString(twistRate * 1000.0F / Math::two_pi)));
}

void TwistController::CommitSetting(const Setting *setting)
{
    // Are we setting the twist rate?
    if (setting->GetSettingIdentifier() == 'rate')
    {
        // Yes, grab the rate value from the setting.
        const char *text = static_cast<const TextSetting *>(setting)->GetText();
        twistRate = Text::StringToFloat(text) * Math::two_pi * 0.001F;
    }
}

The first parameter passed to the TextSetting constructor ('rate') is just an identifier that the controller uses to keep track of which setting is which—it can be anything you want. The second parameter is the title of the setting that, in this case, will be displayed next to the editable text box. The third parameter is the value that will initially be shown to the user.

Moving the Target Node

The PreprocessController() function is called once when the controller's target node is inserted into a world. In this function, we record the original transformation matrix for the target node so that we have a reference frame to which rotations are later going to be applied. We also make sure that the kGeometryDynamic flag is set for any geometry nodes in the subtree rooted at the target node. This tells the engine not to cache certain types of information for the objects that we're going to be moving. The body of our PreprocessController() function is shown below. Note that the base class's PreprocessController() function should always be called.

void TwistController::PreprocessController(void)
{
    PreprocessController::Preprocess();

    // Grab the original transform of the target node.
    const Node *target = GetTargetNode();
    originalTransform = target->GetNodeTransform();

    // Set the kGeometryDynamic flag for any geometry nodes.
    const Node *node = target;
    do
    {
        if (node->GetNodeType() == kNodeGeometry)
        {
            // Node is a geometry, so grab its object.
            GeometryObject *object = static_cast<const Geometry *>(node)->GetObject();

            // Set the kGeometryDynamic flag.
            object->SetGeometryFlags(object->GetGeometryFlags() | kGeometryDynamic);
        }

        // Iterate through entire subtree.
        node = target->GetNextTreeNode(node);
    } while (node);
}

The MoveController() function is called once per frame to let the controller move its target node. In the case of the TwistController class, we just want to update the twist angle and calculate a new transform for the target node. The body of our MoveController() function looks like the following. (We do not need to call the base class's MoveController() function.)

void TwistController::MoveController(void)
{
    // Calculate the new twist angle based on how much time has passed.
    float angle = twistAngle + twistRate * TheTimeMgr->GetFloatDeltaTime();

    // Make sure it's in the [-pi, pi] range.
    if (angle > Math::pi) angle -= Math::two_pi;
    else if (angle < -Math::pi) angle += Math::two_pi;

    twistAngle = angle;

    // Now make a 3x3 rotation matrix.
    Matrix3D rotator = Matrix3D::MakeRotationZ(angle);

    // We'll rotate about the object-space center of the target node's bounding box.
    Node *target = GetTargetNode();
    Point3D objectCenter = target->GetInverseWorldTransform() * target->GetBoundingBox()->GetCenter(); 

    // Make a 3x4 transform that rotates about the center point.
    Transform4D transform(rotator, objectCenter - rotator * objectCenter);

    // Apply the rotation transform to the original transform and
    // assign it to the node as its new transform.
    target->SetNodeTransform(originalTransform * transform);

    // Invalidate the target node so that it gets updated properly.
    target->InvalidateNode();
}

See Also