Defining a Custom Method

From C4 Engine Wiki
Revision as of 21:38, 12 September 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 method refers to an individual action that can appear in a script. There are many types of methods built into the engine, and an application can define its own custom methods by implementing new subclasses of the Method class.

Defining a Method Subclass

In this article, we will use the example of a method called ChangeLightColorMethod that simply changes the color of a light source. Note that this particular functionality already exists as part of the more general built-in method Change Settings, so the example here is for illustrative purposes only.

A custom method subclass needs to have a unique type identifier that is registered with the engine so that the Script 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
{
    kMethodChangeLightColor = 'litc'
};

(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 method subclass needs to be defined. We declare the ChangeLightColorMethod class to be a subclass of the Method class as follows, and we include a single data member to hold the color value that will be assigned to a light source when the method executes. Several member functions are included to handle serialization and user interface—these are discussed below.

class ChangeLightColorMethod : public Method
{
   private:

        ColorRGB    lightColor;

        ChangeLightColorMethod(const ChangeLightColorMethod& changeLightColorMethod);

        Method *Replicate(void) const;

   public:

        ChangeLightColorMethod();
        ChangeLightColorMethod(const ColorRGB& color);
        ~ChangeLightColorMethod();

        const ColorRGB& GetLightColor(void) const
        {
            return (lightColor);
        }

        void SetLightColor(const ColorRGB& color)
        {
            lightColor = color;
        }

        // 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;

        // This function is called when the method is executed in a script.
        void ExecuteMethod(const ScriptState *state) override;
};

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

ChangeLightColorMethod::ChangeLightColorMethod() : Method(kMethodChangeLightColor)
{
    // Set a default value.
    lightColor.Set(1.0F, 1.0F, 1.0F);
}

ChangeLightColorMethod::ChangeLightColorMethod(const ColorRGB& color) : Method(kMethodChangeLightColor)
{
    lightColor = color;
}

ChangeLightColorMethod::~ChangeLightColorMethod()
{
}

Notice that the method's type kMethodChangeLightColor is passed to the base class constructor.

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

ChangeLightColorMethod::ChangeLightColorMethod(const ChangeLightColorMethod& changeLightColorMethod) : Method(changeLightColorMethod)
{
    lightColor = changeLightColorMethod.lightColor;
}

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

The Replicate() function simply constructs a new instance of the method using the copy constructor. This should always be implemented as follows.

Method *ChangeLightColorMethod::Replicate(void) const
{
    return (new ChangeLightColorMethod(*this));
}

Method Registration

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

// Define the method registration.
MethodReg<ChangeLightColorMethod> changeLightColorRegistration;

The registration object is initialized with the following code.

changeLightColorRegistration(kMethodChangeLightColor, "Change Light Color");

There is an optional third parameter that can specify flags for the method, and these are described in the documentation for the MethodReg class.

Serialization

A custom method 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 Method base class. For the ChangeLightColorMethod example, these functions would typically be implemented as follows.

void ChangeLightColorMethod::Pack(Packer& data, uint32 packFlags) const
{
    Method::Pack(data, packFlags);

    // Write the ColorRGB object.
    data << lightColor;
}

void ChangeLightColorMethod::Unpack(Unpacker& data, uint32 unpackFlags)
{
    Method::Unpack(data, unpackFlags);

    // Read the ColorRGB object.
    data >> lightColor;
}

User Interface

The Method class is a subclass of the Configurable class, which means it can expose a user interface that appears in the Script Editor. The Method object is queried by the Script Editor for its configurable settings when the user double-clicks on the method to open its Method Info window. 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 ChangeLightColorMethod example, there is one setting representing the light color stored in the method object. The user interface to change this color would be implemented as follows.

void ChangeLightColorMethod::BuildSettingList(List<Setting> *settingList) const
{
    // There is one setting, and it's a color picker.
    settingList->AppendListElement(new ColorSetting('colr', lightColor, "Light color", "New Light Color"));
}

void ChangeLightColorMethod::CommitSetting(const Setting *setting)
{
    // Are we setting the light color?
    if (setting->GetSettingIdentifier() == 'colr')
    {
        // Yes, grab the RGB color from the setting.
        lightColor = static_cast<const ColorSetting *>(setting)->GetColor().GetColorRGB();
    }
}

The first parameter passed to the ColorSetting constructor ('colr') is just an identifier that the property uses to keep track of which setting is which—it can be anything you want. The second parameter is the color that will initially be shown to the user. The third parameter is the title of the setting that will be displayed next to the color box in the settings list. The last parameter is the title that will be used for the color picker dialog when the user clicks on the color box.

Method Execution

When a script is running and it's time to execute your custom method, the script controller calls the method's ExecuteMethod() function. A method can do anything it wants when it executes as long as it doesn't cause itself to be deleted. When a method finishes performing its actions, it needs to indicate that it has completed by calling the HandleCompletion() function of its Completable base class. A method may finish inside the call to its ExecuteMethod() function, or it may finish sometime later after more time-consuming operations have completed.

The ExecuteMethod() function for the ChangeLightColorMethod class could be implemented as follows.

void ChangeLightColorMethod::Execute(const ScriptState *state)
{
    // Get the target node for this method and
    // make sure it's actually a light source.
    Node *node = GetTargetNode(state);
    if ((node) && (node->GetNodeType() == kNodeLight))
    {
        LightObject *object = static_cast<Light *>(node)->GetObject();

        // Set the new light color.
        object->SetLightColor(lightColor);

        // Mark the object as modified.
        object->SetModifiedFlag();
    }

    // Indicate that we are finished.
    HandleCompletion();
}

The call to the GetTargetNode() function retrieves the node that the user has selected as the target of the method in the Script Editor. It's possible that this target hasn't been selected or that the user linked it to a node that was not a light source, so the above code performs some checks before it carries out its task. Some custom methods do not operate on a target node, and these methods should specify the kMethodNoTarget flag in the method registration so that the user cannot select a target node in the Script Editor.

The SetModifiedFlag() function should be called whenever an object is modified so that the engine knows to write the object data when a game is saved. If this flag is not set, then the original object data would be reloaded when a saved game is resumed.

Method Output Values

A method may produce an output value that can be stored in a script variable. A method may also generate an independent boolean result that is used for conditional execution of subsequent methods in a script. A method can specify its output value by calling the SetOutputValue() function. When a method outputs a value, it should specify the kMethodOutputValue flag in its method registration so that the Script Editor allows the user to assign an output variable to the method.

If a method calls the SetOutputValue() function, then the boolean result for the method is automatically set according to the output value. A method can set the boolean result without specifying an output value, or it can override the output value, by calling the SetMethodResult() function.

See Also