Version 10 (modified by Philip Taylor, 14 years ago) ( diff )

--

Defining interfaces in C++

Think of a name for the component. We'll use "Example" in this example; replace it with your chosen name in all the filenames and code samples below.

(If you copy-and-paste from the examples below, be aware that the coding conventions require indentation with tabs, not spaces, so make sure you get it right.)

Create the file simulation2/components/ICmpExample.h:

/* Copyright (C) 2010 Wildfire Games.
 * ...the usual copyright header...
 */

#ifndef INCLUDED_ICMPEXAMPLE
#define INCLUDED_ICMPEXAMPLE

#include "simulation2/Interface.h"

...any other forward declarations and includes you might need...

/**
 * Documentation to describe what this interface and its associated component types are
 * for, and roughly how they should be used.
 */
class ICmpExample : public IComponent
{
public:
    /**
     * Documentation for each method.
     */
    virtual int DoWhatever(int x, int y) = 0;

    ...

    DECLARE_INTERFACE_TYPE(Example)
};

#endif // INCLUDED_ICMPEXAMPLE

This defines the interface that C++ code will use to access components.

Create the file simulation2/components/ICmpExample.cpp:

/* Copyright (C) 2010 Wildfire Games.
 * ...the usual copyright header...
 */

#include "precompiled.h"

#include "ICmpExample.h"

#include "simulation2/InterfaceScripted.h"

BEGIN_INTERFACE_WRAPPER(Example)
DEFINE_INTERFACE_METHOD_2("DoWhatever", int, ICmpExample, DoWhatever, int, int)
// DEFINE_INTERFACE_METHOD for all the other methods too
END_INTERFACE_WRAPPER(Example)

This defines a JavaScript wrapper, so that scripts can access methods of components implementing that interface. See a later section for details.

This wrapper should only contain methods that are safe to access from simulation scripts: they must not crash, they must return deterministic results, etc. Methods that are intended for use solely by C++ should not be listed here.

Every interface must define a script wrapper, though in some cases they might contain no methods.

Now update the file simulation2/TypeList.h and add

INTERFACE(Example)

TypeList.h is used for various purposes - it will define the interface ID number IID_Example (in both C++ and JS), and it will hook the new interface into the interface registration system.

Remember to run the update-workspaces script after adding or removing any source files, so that they will be added to the makefiles or VS projects.

Interface method script wrappers

Interface methods are defined with the macro:

DEFINE_INTERFACE_METHOD_NumberOfArguments("MethodName", ReturnType, ICmpExample, MethodName, !ArgType0, !ArgType1, ...)

corresponding to the C++ method ReturnType ICmpExample::MethodName(!ArgType0, !ArgType1, ...)

There's a small limit to the number of arguments that are currently supported - if you need more, first try to save yourself some pain by using fewer arguments, otherwise you'll need to add a new macro into simulation2/InterfaceScripted.h and increase MAX_ARGS in scriptinterface/NativeWrapperDecls.h. (Not sure if anything else needs changing.)

The two MethodNames don't have to be the same - in rare cases you might want to expose it as DoWhatever to scripts but link it to the ICmpExample::DoWhatever_wrapper() method which does some extra conversions or checks or whatever.

For methods exposed to scripts like this, the arguments should be pass-by-value. E.g. use std::wstring arguments, not const std::wstring&.

To convert types between C++ and JS, ToJSVal<ReturnType> and FromJSVal<ArgTypeN> must be defined, as below.

Script type conversions

If you try to use a type without having defined conversions, you'll probably get mysterious linker errors that mention ToJSVal or FromJSVal. First, work out where the conversion should be defined. Basic data types (integers, STL containers, etc) go in scriptinterface/ScriptConversions.cpp. Non-basic data types from the game engine typically go in simulation2/scripting/EngineScriptConversions.cpp. (They could go in different files if that turns out to be cleaner - it doesn't matter where they're defined as long as the linker finds them).

To convert from a C++ type T to a JS value, define:

template<> jsval ScriptInterface::ToJSVal<T>(JSContext* cx, T const& val)
{
    ...
}

Use the standard SpiderMonkey JSAPI functions to do the conversion (possibly calling ToJSVal recursively). On error, you should return JSVAL_VOID (JS's undefined value) and probably report an error message somehow. Be careful about JS garbage collection (don't let it collect the objects you're constructing before you return them).

To convert from a JS value to a C++ type T, define:

template<> bool ScriptInterface::FromJSVal<T>(JSContext* cx, jsval v, T& out)
{
    ...
}

On error, return false (doesn't matter what you do with out). On success, return true and put the value in out. Still need to be careful about garbage collection (v is rooted, but it might have getters that execute arbitrary code and return unrooted values when you access properties, so don't let them be collected before you've finished using them).

Defining component types in C++

Now we want to implement the Example interface. We need a name for the component type - if there's only ever going to be one implementation of the interface, we might as well call it Example too. If there's going to be more than one, they should have distinct names like ExampleStatic and ExampleMobile etc.

Create simulation2/components/CCmpExample.cpp:

... copyright header ...

#include "precompiled.h"

#include "Component.h"
#include "ICmpExample.h"

... any other includes needed ...

class CCmpExample : public ICmpExample
{
public:
    static void ClassInit(CComponentManager& componentManager)
        ...

    DEFAULT_COMPONENT_ALLOCATOR(Example)

    ... member variables ...

    virtual void Init(const CSimContext& context, const CParamNode& paramNode)
        ...

    virtual void Deinit(const CSimContext& context)
        ...

    virtual void Serialize(ISerializer& serialize)
        ...

    virtual void Deserialize(const CSimContext& context, const CParamNode& paramNode, IDeserializer& deserialize)
        ...

    virtual void HandleMessage(const CSimContext&, const CMessage& msg)
        ...

    ... Implementation of interface functions: ...
    virtual int DoWhatever(int x, int y)
    {
        return x+y;
    }
};

REGISTER_COMPONENT_TYPE(IID_Example, Example)

The only optional method is HandleMessage - all others must be defined.

Update the file simulation2/TypeList.h and add

COMPONENT(Example)

Message handling

First you need to register for all the message types you want to receive, in ClassInit:

static void ClassInit(CComponentManager& componentManager)
{
    componentManager.SubscribeToMessageType(CID_Example, MT_Update);
    ...
}

(CID_Example is derived from the name of the component type, not the name of the interface.)

Then you need to respond to the messages in HandleMessage:

virtual void HandleMessage(const CSimContext& context, const CMessage& msg)
{
    switch (msg.GetType())
    {
    case MT_Update:
    {
        const CMessageUpdate& msgData = static_cast<const CMessageUpdate&> (msg);
        Update(msgData.turnLength); // or whatever processing you want to do
        break;
    }
    }
}

The CMessage structures are defined in simulation2/MessageTypes.h. Be very careful that you're casting msg to the right type.

Component creation

Component type instances go through one of two lifecycles:

CCmpExample();
Init(context, paramNode);
// any sequence of HandleMessage and Serialize and interface methods
Deinit(context);
~CCmpExample();
CCmpExample();
Deserialize(context, paramNode, deserialize);
// any sequence of HandleMessage and Serialize and interface methods
Deinit(context);
~CCmpExample();

The order of Init/Deserialize/Deinit between components is undefined, so they must not rely on other entities or components already existing; except that the SYSTEM_ENTITY is created before anything else and therefore may be used.

The same context object will be used in all these calls. (The component could safely store it in a CSimContext* m_Context member if necessary.)

In a typical component:

  • The constructor should do very little, other than perhaps initialising some member variables - usually the default constructor is fine so there's no need to write one.
  • Init should parse the paramNode (the data from the entity template) and store any needed data in member variables.
  • Deserialize should often explicitly call Init first (to load the original template data), and then read any instance-specific data from the deserializer.
  • Deinit should clean up any resources allocated by Init/Deserialize.
  • The destructor should clean up any resources allocated by the constructor - usually there's no need to write one.

Allowing interfaces to be implemented in JS

If we want to allow both C++ and JS implementations of ICmpExample, we need to define a special component type that proxies all the C++ methods to the script. Add the following to ICmpExample.cpp:

#include "simulation2/scripting/ScriptComponent.h"

...

class CCmpExampleScripted : public ICmpExample
{
public:
    DEFAULT_SCRIPT_WRAPPER(ExampleScripted)

    virtual int DoWhatever(int x, int y)
    {
        return m_Script.Call<int> ("DoWhatever", x, y);
    }
};

REGISTER_COMPONENT_SCRIPT_WRAPPER(IID_Example, ExampleScripted)

Then add to TypeList.h:

COMPONENT(ExampleScripted)

m_Script.Call takes the return type as a template argument, then the name of the JS function to call and the list of parameters. You could do extra conversion work before calling the script, if necessary. You need to make sure the types are handled by ToJSVal and FromJSVal (as discussed before) as appropriate.

Defining component types in JS

Now we want a JS implementation of ICmpExample. Think up a new name for this component, like ExampleTwo (but more imaginative). Then write binaries/data/mods/public/simulation/comonents/ExampleTwo.js:

function ExampleTwo() {}

ExampleTwo.prototype.Init = function() {
    ...
};

ExampleTwo.prototype.Deinit = function() {
    ...
};

ExampleTwo.prototype.OnUpdate = function(msg) {
    ...
};

Engine.RegisterComponentType(IID_Example, "ExampleTwo", ExampleTwo);

This uses JS's prototype system to create what is effectively a class, called ExampleTwo. (If you wrote new ExampleTwo(), then JS would construct a new object which inherits from ExampleTwo.prototype, and then would call the ExampleTwo function with this set to the new object. "Inherit" here means that if you read a property (or method) of the object, which is not defined in the object, then it will be read from the prototype instead.)

Engine.RegisterComponentType tells the engine to start using the JS class ExampleTwo, exposed (in template files etc) with the name "ExampleTwo", and implementing the interface ID IID_Example (i.e. the ICmpExample interface).

The Init and Deinit functions are optional. Unlike C++, there are no Serialize/Deserialize functions - each JS component instance is automatically serialized and restored. (This automatic serialization restricts what you can store as properties in the object - e.g. you cannot store function closures, because they're too hard to serialize. The details should be documented on some other page eventually.)

Instead of ClassInit and HandleMessage, you simply add functions of the form OnMessageType. When you call RegisterComponentType, it will find all such functions and automatically subscribe to the messages. The msg parameter is usually a straightforward mapping of the relevant CMessage class onto a JS object (e.g. OnUpdate can read msg.turnLength).

Defining a new message type

...

Note: See TracWiki for help on using the wiki.