Defining a Custom Controller: Difference between revisions

From C4 Engine Wiki
Jump to navigation Jump to search
(Created page with "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 [http://c4engine.com/docs/Controller/Controller.html <code>Controller</code>] 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...")
 
No edit summary
 
(One intermediate revision by the same user not shown)
Line 7: Line 7:
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.
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.


<source lang="cpp">
<syntaxhighlight lang="cpp">
enum
enum
{
{
     kControllerTwist = 'twst'
     kControllerTwist = 'twst'
};
};
</source>
</syntaxhighlight>


(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.)
(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.)
Line 18: Line 18:
Next, the controller subclass needs to be defined. We declare the <code>TwistController</code> class to be a subclass of the <code>Controller</code> 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&mdash;these are discussed below.
Next, the controller subclass needs to be defined. We declare the <code>TwistController</code> class to be a subclass of the <code>Controller</code> 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&mdash;these are discussed below.


<source lang="cpp">
<syntaxhighlight lang="cpp">
class TwistController : public Controller
class TwistController : public Controller
{
{
Line 62: Line 62:
         void MoveController(void);
         void MoveController(void);
};
};
</source>
</syntaxhighlight>


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


<source lang="cpp">
<syntaxhighlight lang="cpp">
TwistController::TwistController() : Controller(kControllerTwist)
TwistController::TwistController() : Controller(kControllerTwist)
{
{
Line 83: Line 83:
{
{
}
}
</source>
</syntaxhighlight>


Notice that the controller's type <code>kControllerTwist</code> is passed to the base class's constructor.
Notice that the controller's type <code>kControllerTwist</code> is passed to the base class's constructor.
Line 91: Line 91:
The copy constructor and the <code>Replicate()</code> function must be included for all controllers. For this example, the copy constructor would be implemented as follows.
The copy constructor and the <code>Replicate()</code> function must be included for all controllers. For this example, the copy constructor would be implemented as follows.


<source lang="cpp">
<syntaxhighlight lang="cpp">
TwistController::TwistController(const TwistController& twistController) : Controller(twistController)
TwistController::TwistController(const TwistController& twistController) : Controller(twistController)
{
{
Line 97: Line 97:
     twistAngle = 0.0F;
     twistAngle = 0.0F;
}
}
</source>
</syntaxhighlight>


It's important to observe that the reference to the copied <code>TwistController</code> object is passed to the base class constructor this time instead of the controller's type identifier.
It's important to observe that the reference to the copied <code>TwistController</code> object is passed to the base class constructor this time instead of the controller's type identifier.
Line 103: Line 103:
The <code>Replicate()</code> function simply constructs a new instance of the controller using the copy constructor. This would usually be implemented as follows.
The <code>Replicate()</code> function simply constructs a new instance of the controller using the copy constructor. This would usually be implemented as follows.


<source lang="c++">
<syntaxhighlight lang="c++">
Controller *TwistController::Replicate(void) const
Controller *TwistController::Replicate(void) const
{
{
     return (new TwistController(*this));
     return (new TwistController(*this));
}
}
</source>
</syntaxhighlight>


== Controller Registration ==
== Controller Registration ==
Line 114: Line 114:
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.
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.


<source lang="cpp">
<syntaxhighlight lang="cpp">
// Define the controller registration.
// Define the controller registration.
ControllerReg<TwistController> twistControllerReg;
ControllerReg<TwistController> twistControllerReg;
</source>
</syntaxhighlight>


The registration object is initialized with the following code.
The registration object is initialized with the following code.


<source lang="cpp">
<syntaxhighlight lang="cpp">
twistControllerReg(kControllerTwist, "Twist");
twistControllerReg(kControllerTwist, "Twist");
</source>
</syntaxhighlight>


The <code>ValidNode()</code> function declared in the <code>TwistController</code> 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 <code>ValidNode()</code> function should return <code>true</code> when it's okay to assign the controller to the node passed to it, and <code>false</code> if it's not okay. As an example, if we only wanted <code>TwistController</code> objects to be assigned to geometry nodes, then we would implement the <code>ValidNode()</code> function as follows.
The <code>ValidNode()</code> function declared in the <code>TwistController</code> 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 <code>ValidNode()</code> function should return <code>true</code> when it's okay to assign the controller to the node passed to it, and <code>false</code> if it's not okay. As an example, if we only wanted <code>TwistController</code> objects to be assigned to geometry nodes, then we would implement the <code>ValidNode()</code> function as follows.


<source lang="c++">
<syntaxhighlight lang="c++">
bool TwistController::ValidNode(const Node *node)
bool TwistController::ValidNode(const Node *node)
{
{
     return (node->GetNodeType() == kNodeGeometry);
     return (node->GetNodeType() == kNodeGeometry);
}
}
</source>
</syntaxhighlight>


Notice that the <code>ValidNode()</code> function is declared <code>static</code>. The World Editor calls this function automatically when it needs to know whether the controller can be used with a specific node.
Notice that the <code>ValidNode()</code> function is declared <code>static</code>. The World Editor calls this function automatically when it needs to know whether the controller can be used with a specific node.
Line 140: Line 140:
A custom controller must implement the <code>Pack()</code> and <code>Unpack()</code> functions so that its data can be written to a file and later restored. (These functions override the virtual functions in the [http://c4engine.com/docs/ResourceMgr/Packable.html <code>Packable</code>] class.) Each of these functions needs to first call its counterpart in the <code>Controller</code> base class. For the <code>TwistController</code> example, these functions would typically be implemented as follows.
A custom controller must implement the <code>Pack()</code> and <code>Unpack()</code> functions so that its data can be written to a file and later restored. (These functions override the virtual functions in the [http://c4engine.com/docs/ResourceMgr/Packable.html <code>Packable</code>] class.) Each of these functions needs to first call its counterpart in the <code>Controller</code> base class. For the <code>TwistController</code> example, these functions would typically be implemented as follows.


<source lang="cpp">
<syntaxhighlight lang="cpp">
void TwistController::Pack(Packer& data, uint32 packFlags) const
void TwistController::Pack(Packer& data, uint32 packFlags) const
{
{
Line 168: Line 168:
     data >> originalTransform;
     data >> originalTransform;
}
}
</source>
</syntaxhighlight>


== User Interface ==
== User Interface ==
Line 174: Line 174:
The <code>Controller</code> class is a subclass of the [http://c4engine.com/docs/InterfaceMgr/Configurable.html <code>Configurable</code>] class, which means it can expose a user interface that appears in the [[World Editor]]. The <code>Controller</code> 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 <code>BuildSettingList()</code> function is called when the editor needs all of the configurable settings to be built, and the <code>CommitSetting()</code> function is called for each setting when the user confirms the new configuration. For the <code>TwistController</code> 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.
The <code>Controller</code> class is a subclass of the [http://c4engine.com/docs/InterfaceMgr/Configurable.html <code>Configurable</code>] class, which means it can expose a user interface that appears in the [[World Editor]]. The <code>Controller</code> 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 <code>BuildSettingList()</code> function is called when the editor needs all of the configurable settings to be built, and the <code>CommitSetting()</code> function is called for each setting when the user confirms the new configuration. For the <code>TwistController</code> 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.


<source lang="cpp">
<syntaxhighlight lang="cpp">
void TwistController::BuildSettingList(List<Setting> *settingList) const
void TwistController::BuildSettingList(List<Setting> *settingList) const
{
{
Line 191: Line 191:
     }
     }
}
}
</source>
</syntaxhighlight>


The first parameter passed to the <code>TextSetting</code> constructor (<code>'rate'</code>) is just an identifier that the controller uses to keep track of which setting is which&mdash;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.
The first parameter passed to the <code>TextSetting</code> constructor (<code>'rate'</code>) is just an identifier that the controller uses to keep track of which setting is which&mdash;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.
Line 199: Line 199:
The <code>PreprocessController()</code> 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 <code>kGeometryDynamic</code> 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 <code>PreprocessController()</code> function is shown below. Note that the base class's <code>PreprocessController()</code> function should always be called.
The <code>PreprocessController()</code> 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 <code>kGeometryDynamic</code> 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 <code>PreprocessController()</code> function is shown below. Note that the base class's <code>PreprocessController()</code> function should always be called.


<source lang="cpp">
<syntaxhighlight lang="cpp">
void TwistController::PreprocessController(void)
void TwistController::PreprocessController(void)
{
{
Line 225: Line 225:
     } while (node);
     } while (node);
}
}
</source>
</syntaxhighlight>


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


<source lang="cpp">
<syntaxhighlight lang="cpp">
void TwistController::MoveController(void)
void TwistController::MoveController(void)
{
{
    Matrix3D    rotator;
     // Calculate the new twist angle based on how much time has passed.
     // Calculate the new twist angle based on how much time has passed.
     float angle = twistAngle + twistRate * TheTimeMgr->GetFloatDeltaTime();
     float angle = twistAngle + twistRate * TheTimeMgr->GetFloatDeltaTime();
Line 244: Line 242:


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


     // We'll rotate about the object-space center of the target node's bounding box.
     // We'll rotate about the object-space center of the target node's bounding box.
Line 260: Line 258:
     target->InvalidateNode();
     target->InvalidateNode();
}
}
</source>
</syntaxhighlight>


== See Also ==
== See Also ==

Latest revision as of 01:19, 8 December 2023

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