Defining a Custom Controller

From C4 Engine Wiki

Jump to: navigation, search
Image:warning.jpg Under Construction

This article is still under construction and may contain sections that are incomplete.

In the C4 Engine, a controller is attached to a scene graph 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 Get Info dialog.

Note: If you make these changes to the "Game" Application, then you could add the changes to the files MGGame and MGControllers. Just look for similar code in those files and add the code below as appropriate.

Contents

Defining a Custom Controller

An application can define custom controllers, and they can be registered with the engine so that the World Editor knows about them. The first thing that a custom controller needs is a type identifier, which is a 32-bit number normally represented by a four-character code. In this article, we will use the example of a controller called SpinController that causes a node to spin about its local z-axis. We can define its type as follows.

enum
{
    kControllerSpin = 'spin'
};

(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 SpinController 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 spin. Several member functions are included to handle serialization and user interface—these are discussed below.

class SpinController : public Controller
{
   private:
 
        float         spinRate;             // In radians per millisecond
        float         spinAngle;            // The current angle, in radians
        Transform4D   originalTransform;    // The target's original transform
 
        SpinController(const SpinController& spinController);
 
        Controller *Replicate(void) const;
 
   public:
 
        SpinController();
        SpinController(float rate);
        ~SpinController();
 
        float GetSpinRate(void) const
        {
            return (spinRate);
        }
 
        void SetSpinRate(float rate)
        {
            spinRate = rate;
        }
 
        static bool ValidNode(const Node *node);
 
        // Serialization functions
        void Pack(Packer& data, unsigned long packFlags) const;
        void Unpack(Unpacker& data, unsigned long unpackFlags);
 
        // User interface functions
        long GetSettingCount(void) const;
        Setting *GetSetting(long index) const;
        void SetSetting(const Setting *setting);
 
        void Preprocess(void);
 
        // The function that moves the target node
        void Move(void);
};

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

SpinController::SpinController() : Controller(kControllerSpin)
{
    // Set a default value for the spin rate of one revolution per second
    spinRate = K::two_pi / 1000.0F;
    spinAngle = 0.0F;
}
 
SpinController::SpinController(float rate) : Controller(kControllerSpin)
{
    spinRate = rate;
    spinAngle = 0.0F;
}
 
SpinController::~SpinController()
{
}

Notice that the controller's type kControllerSpin 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 if you want the controller to be accessible in the World Editor. For this example, the copy constructor would be implemented as follows.

SpinController::SpinController(const SpinController& spinController) : Controller(spinController)
{
    spinRate = spinController.spinRate;
    spinAngle = 0.0F;
}

This time, the reference to the spinController object is passed to the base class's constructor.

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

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

Controller Registration

If you want your custom controller to appear in the World Editor, then it must be registered with the engine. This is accomplished by creating a ControllerReg object. The controller registration contains information about the controller type and its name, and its mere existence registers the controller type that it represents. A controller registration for the SpinController class would normally look like the following.

// Define the controller registration
ControllerReg<SpinController> spinControllerReg;

The registration object is initialized with the following code.

spinControllerReg(kControllerSpin, "Spin"),

The ValidNode function declared in the SpinController 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 SpinController objects to be assigned to Geometry nodes, then we would implement the ValidNode function as follows.

bool SpinController::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 SpinController example, these functions would typically be implemented as follows.

void SpinController::Pack(Packer& data, unsigned long packFlags) const
{
    Controller::Pack(data, packFlags);
 
    // Write the spin rate
    data << spinRate;
 
    // Write the current angle
    data << spinAngle;
 
    // Write the original transform
    data << originalTransform;
}
 
void SpinController::Unpack(Unpacker& data, unsigned long unpackFlags)
{
    Controller::Unpack(data, unpackFlags);
 
    // Read the spin rate
    data >> spinRate;
 
    // Read the current angle
    data >> spinAngle;
 
    // 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 Get Info dialog for a node with a controller attached to it. The GetSettingCount function returns the number of individual settings that need to be shown, and the settings are retrieved or changed using the GetSetting and SetSetting functions. For the SpinController example, there is one setting representing the spin rate stored in the controller. The user interface to change this value would be implemented as follows.

long SpinController::GetSettingCount(void) const
{
    // There's only one setting
    return (1);
}
 
Setting *SpinController::GetSetting(long index) const
{
    // Is it asking for the first setting?
    if (index == 0)
    {
        // Yes, return a new text setting and set its value in revolutions per second
        return (new TextSetting('rate', Text::FloatToString(spinRate * 1000.0F / K::two_pi),
            "Spin rate", 7, &EditableTextElement::FloatNumberKeyFilter));
    }
 
    return (nullptr);
}
 
void SpinController::SetSetting(const Setting *setting)
{
    // Are we setting the spin rate?
    if (setting->GetSettingIdentifier() == 'rate')
    {
        // Yes, grab the value from the setting and convert it back to radians per millisecond
        const char *text = static_cast<const TextSetting *>(setting)->GetText();
        spinRate = Text::StringToFloat(text) * K::two_pi / 1000.0F;
    }
}

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 text that will initially be shown to the user. The third parameter is the title of the setting that, in this case, will be displayed next to the editable text box. Next is the width of the text box, followed by the maximum number of characters that may be entered. The last parameter is a pointer to a filter function that specifies which characters are allowed to be entered—in this case, only characters used in a floating-point number.

Moving the Target Node

The Preprocess function is called once when the controller's target node is inserted into a scene. 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 ensures that shadow volumes are not cached for the objects that we're going to be moving. The body of our Preprocess function is shown below. Note that the base class's Preprocess function should always be called.

void SpinController::Preprocess(void)
{
    Controller::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->GetNextNode(node);
    } while (node);
}

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

void SpinController::Move(void)
{
    Matrix3D    rotator;
 
    // Calculate the new spin angle based on how much time has passed
    float angle = spinAngle + spinRate * TheTimeMgr->GetFloatDeltaTime();
 
    // Make sure it's in the [-pi, pi] range
    if (angle > K::pi) angle -= K::two_pi;
    else if (angle < -K::pi) angle += K::two_pi;
 
    spinAngle = angle;
 
    // Now make a 3x3 rotation matrix
    rotator.SetRotationAboutZ(angle);
 
    // We'll rotate about the center of the target node's bounding sphere
    Node *target = GetTargetNode();
    const Point3D& worldCenter = target->GetBoundingSphere()->GetCenter();
    Point3D objectCenter = target->GetInverseWorldTransform() * worldCenter; 
 
    // 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->Invalidate();
}

Multiplayer

To be written.

Further Examples

There are some example Custom Controller Move() methods over in the Code Snippets, including adding a translation to the rotation, and getting a turret gun to track and fire at the local player.:

http://www.terathon.com/wiki/index.php?title=Code_Snippets

Documentation Links

Personal tools