CHAPTER 11

image

Programming: Decoupling Game Tool GUIs from Core Editing Operations

Nicus¸or Nedelcu

Since the early days of video game development when the programmer had to write the code plus design and create the levels without the aid of a game editor, the tools to create games have evolved into the must-have game development software we use today. Now the level editors are built into the development kits, and the developer’s job is much easier—but still filled with potential pitfalls.

In the past few years, it has become common to decouple game level editor operations and functionality from game-specific features, so that the editor can be reused for more games and game types. The same thing has happened on the game engine side: engines have become more and more flexible and reusable.

But problems remain. One big issue with game level editors is complexity and manageability. Once you have added many features to the editor, it will grow in source code size and complexity, and will become harder and harder to maintain and extend. Another problem is that you have to choose a GUI toolkit to create your interface. That can become a headache if you ever decide to switch to another GUI toolkit, since many editing operations are tied in with the UI code itself.

To address the issue of changing GUI toolkits in these fast and ever-shifting times, we present a method of decoupling the visual user interface code from the non-GUI editing operations code in the game level editor or other tools. By separating the UI from core editing functions, you can change to another GUI toolkit in no time, leaving the editing operations code almost untouched. The decoupling operation can be accomplished via C++ editor core functionality code and various editor user interfaces using GUI toolkits like Qt,1  MS WinForms, WPF, MFC, HTML5/JavaScript, or even a command-line editor UI, all using the same editor functionality code as a common hub. Communication between the editor functions and the visual interface is achieved through a command system (basically the Command Pattern). We will also explore the architecture of a plug-in system using this command communication approach.

Editor Ecosystem

The editor is split into two major logical parts:

  • Non-visual: Consisting of editor core, plug-ins, and their commands (no GUI).
  • Visual: Created using the UI toolkit of your choice, which will call the commands provided by the plug-ins and editor core.

In Figure 11-1, you can see the entire editor ecosystem.

9781430267003_Fig11-01.jpg

Figure 11-1. The editor ecosystem

The editor GUI can be developed using any UI SDK/API, and it can have its own plug-ins. For example, sub-editors like the model editor, cinematic editor, scene editor, material editor, etc., can be hosted by the main editor, and you can even run them as separate tools. Each tool will implement its own UI functionality and will call commands by their name and parameter values (arguments). The editor core will search its registered commands list and dispatch the call to the appropriate plug-in command.

You can also have an editor network layer, which waits for tools to connect to it and simply dispatches command calls and sends back their results. There are various other methods of communication between the GUI and the editor core; these methods use IPC (inter-process communication2) such as pipes, DDE, and shared memory or files, but sockets are supported on all platforms, so they are the obvious first choice.

Editor Core C++ API

Now let’s get to the nuts and bolts of the actual code. First you will declare your editor C++ interface to be used by the plug-ins. You are going to expose plug-in and command methods and interfaces, a simple history (undo/redo) system, and an event system (used for triggering events in the editor, plug-ins can register themselves as event sinks to receive or trigger events).

Let’s start with the building block interfaces related to commands, undo, events, and other primitive constructs. You will use a self-contained, independent header file, with only pure interfaces, not relying on external headers so it can be easily wrapped or converted to other languages. (It’s especially important to keep the interfaces simple. If you were using something like SWIG (Simplified Wrapper and Interface Generator3), for example, having too many dependencies in the C++ code would complicate things for the SWIG converter, sometimes failing to properly create a wrapper for other languages.)

After you define your simple types like uint32, you define a Handle union to be used as a pointer transporter between the calling application and the editor core internals. This will keep things simpler, since the user can’t use the pointer itself anyway (see Listing 11-1).

Listing 11-1. A Generic Handle Container

// A generic handle, used to pass pointersin command parameters without having // to know the pointer type
union Handle
{
   Handle()
      : hVoidPtr(NULL)
   {}
   explicit Handle(int32 aVal)
      : hInt(aVal)
   {}
   explicit Handle(int64 aVal)
      : hInt64(aVal)
   {}
   explicit Handle(void* pVal)
      : hVoidPtr(pVal)
   {}
   int32 hInt;
   int64 hInt64;
   void* hVoidPtr;
};

You will also need a Version structure to be used in the various version comparisons/validations you will have for the editor API and plug-in versions (see Listing 11-2).

Listing 11-2. A Generic Version Holder Structure

// A version structure, holding version information for plug-in or editor
struct Version
{
   Version();
   Version(uint32 aMajor, uint32 aMinor, uint32 aBuild);
   bool operator <= (const Version& rOther) const;
   bool operator >= (const Version& rOther) const;
   Version& operator = (const char* pVerStr);
 
   uint32 major, minor, build;
};

After this, a central point of the editor core API is the main editor interface (see Listing 11-3), which will provide command, plug-in, and event methods to be used by plug-ins and their commands, and also by the main editor skeleton application, which will manage those plug-ins.

Listing 11-3. The Main Editor Core Interface

// The editor main interface
struct IEditor
{
  enum ELogMsgType
  {
    eLogMsg_Info,
    eLogMsg_Debug,
    eLogMsg_Warning,
    eLogMsg_Error,
    eLogMsg_Fatal
  };
 
  virtual ~IEditor(){}
  virtual Version GetVersion() const = 0;
  virtual void PushUndoAction(IUndoAction* pAction) = 0;
  virtual bool CanUndo(uint32 aSteps = 1) = 0;
  virtual void Undo(uint32 aSteps = 1) = 0;
  virtual void Redo(uint32 aSteps = 1) = 0;
  virtual void ClearHistory(int32 aSteps = -1) = 0;
  virtual bool RegisterCommand(IPlugin* pPlugin,
                   TPfnCommand pCmdFunc,
                   const char* pCmdName) = 0;
  virtual bool UnregisterCommand(TPfnCommand pCmdFunc) = 0;
  virtual bool IsCommandRegistered(
                   const char* pCmdName) = 0;
  virtual bool RegisterEvent(IEvent* pEvent) = 0;
  virtual bool UnregisterEvent(IEvent* pEvent) = 0;
  virtual bool IsEventRegistered(
                   const char* pEventName) = 0;
  virtual bool TriggerEvent(IEvent* pEvent,
                   IEventSink::ETriggerContext aContext,
                   void* pUserData) = 0;
  virtual void CallEventSinks(IEvent* pEvent,
                   void* pUserData) = 0;
  virtual bool RegisterEventSink(
                   IEventSink* pEventSink) = 0;
  virtual bool UnregisterEventSink(
                   IEventSink* pEventSink) = 0;
  virtual IParameterValues* CreateParameterValues() = 0;
  virtual IParameterDefinitions*
               CreateParameterDefinitions() = 0;
  virtual bool Call(
                   const char* pCommandName,
                   IParameterValues* pParams) = 0;
  virtual void WriteLog(
                   ELogMsgType aType,
                   const char* pModule,
                   const char* pFormat, ...) = 0;
};

This is the main editor interface at a glance. Its methods are quite self-explanatory, the most used methods being the Call(...) method, which is used to execute commands by their name and requires a parameter “bag” (optional), and the IParameterValues interface, created before the call by the user using the CreateParameterValues() method and then filling up the parameter values for the command to use.

Plug-ins

The plug-ins are DLLs loaded by the core editor DLL. Each plug-in will expose and register its commands in the editor’s ecosystem and provide information about these commands through a manifest file associated with the plug-in’s DLL.

A core editor plug-in consists of two files:

  • A C++ DLL file, the plug-in code (Example.dll)
  • A manifest file (Example.plugin.xml4), having the same base file name as the plug-in’s DLL (Example), containing information about it.

Listing 11-4 shows an example of a plug-in manifest file.

Listing 11-4. Plug-in Manifest File

<plugin
    name="Example"
    description="The example editor plugin"
    author="Nicusor Nastase Nedelcu"
    url="http://some.com"
    guid="31D91906-1125-4784-81FF-119C15267FC3"
    version="1.0.0"
    minEditorVersion="1.0.0"
    maxEditorVersion="2.0.0"
    icon="example.png"
    unloadable="true">
    <dependencies>
        <depends nameHint="OtherPlugin"
            guid="DAA91906-1125-4784-81FF-319C15267FC3" />
        <depends nameHint="SomeOtherPlugin"
            guid="F51A2113-1361-1431-A3EA-B4EA2134A111" />
    </dependencies>
    <commands>
        <command name="get_some_thing"
            info="This command get something">
            <param name="someParam1" type="int32"
                info="this is parameter 1" />
            <param name="someParam2" type="float"
                info="this is parameter 2" />
        </command>
    </commands>
</plugin>

Of course, you can choose any format for the manifest file, like JSON or a custom text format. The important thing is that the plug-in’s DLL does not contain any information about the plug-in or its commands. Only the manifest file holds that information.

Plug-ins can be located in a directory structure, as shown in Listing 11-5.

Listing 11-5. Example of Plug-in and Editor Directory Structure

Plugins
    Example1
        Example1.dll
        Example1.plugin.xml
    Example2
        Example2.dll
        Example2.plugin.xml
 
EditorCore.dll (the editor code library)
EditorUI.exe (the main editor application)

One reason for storing the plug-in information inside external files is that plug-ins can be listed (with all their details) in the editor’s plug-in manager without being loaded into memory. In this way, you can avoid loading some plug-ins you do not need to load, but still have information about them. For example, there can be special editor configurations for lighting artists, programmers, or level designers, and these configuration files can be shared among users.

As you can see from the plug-in manifest, you have added information about the name, description, author, and other useful properties, but also about the plug-in’s dependencies (other plug-in GUIDs5). Optionally, there should be information about the commands, such as name, description, parameters, and return values, since you do not store this information in the C++ source files. This information can be used by a debug layer to check the command syntax at runtime and help the discovery of incorrect command calls during development.

For plug-in identification, you will use a GUID in the form shown in Listing 11-6.

Listing 11-6. The GUID Structure Used to Identify Plug-ins

// A plug-in unique ID, in a GUID form (see more about Microsoft
// GUID) and online/offline GUID generators
struct PluginGuid
{
    PluginGuid();
 
    // construct the guid from several elements/parts
    // example:
    // as text: 31D9B906-6125-4784-81FF-119C15267FCA
    // as C++: 0x31d9b906, 0x6125, 0x4784, 0x81,
    // 0xff, 0x11, 0x9c, 0x15, 0x26, 0x7f, 0xca
    PluginGuid(uint32 a, uint16 b, uint16 c, uint8 d,
 uint8 e, uint8 f, uint8 g, uint8 h, uint8 i,
        uint8 j, uint8 k);
 
    bool operator == (const PluginGuid& rOther) const;
 
    // convert a GUID string to binary
    // string format: "11191906-6125-4784-81FF-119C15267FC3"
    bool fromString(const char* pGUID);
 
    uint32 data1;
    uint16 data2;
    uint16 data3;
    uint8 data4[8];
};

You will use the interface shown in Listing 11-7 to get information about the discovered plug-ins (gathered from the plug-in manifest files).

Listing 11-7. The Interface that Describes a Plug-in (from the Plug-in Manifest)

struct IPluginInfo
{
    virtual ~IPluginInfo(){}
    virtual const char* GetName() const = 0;
    virtual const char* GetDescription() const = 0;
    virtual const char* GetAuthor() const = 0;
    virtual const char* GetWebsiteUrl() const = 0;
    virtual PluginGuid GetGuid() const = 0;
    virtual Version GetVersion() const = 0;
    virtual Version GetMinEditorVersion() const = 0;
    virtual Version GetMaxEditorVersion() const = 0;
    virtual const char* GetIconFilename() const = 0;
    virtual bool IsUnloadable() const = 0;
    virtual PluginGuidArray GetPluginDependencies()
                                const = 0;
};

The plug-in interface methods are easy to understand, but let’s talk about GetMinEditorVersion() and GetMaxEditorVersion(). These methods are used to check whether the plug-in can be loaded into the current editor and help avoid loading plug-ins that are not supposed to run under newer or older editor versions.

The simple creation process of new plug-ins and commands should be the crux of this system, thus coding new command sets hosted in the plug-ins should be straightforward. In the editor core API, there is an interface each plug-in must implement on its side, called IPlugin, as shown in Listing 11-8.

Listing 11-8. The Interface to be Implemented by a Plug-in

struct IPlugin
{
    virtual ~IPlugin(){}
    virtual void Initialize(IEditor* pEditor) = 0;
    virtual void Shutdown() = 0;
    virtual bool IsCommandEnabled(TPfnCommand pCmdFunc)= 0;
};

Commands

You will create the editor core as a C++ DLL. This handles the loading of plug-ins that are exposing editing commands. The GUI will call the commands using only the core editor interfaces (see Figure 11-2).

9781430267003_Fig11-02.jpg

Figure 11-2. The command system diagram

The command system is designed as an RPC-like (remote procedure call) architecture, where commands are actually functions that are called with arguments and return one or more values. The call itself can be made directly using the editor core C++ API or the UI editor application connecting sockets to an editor core server, transmitting the command call data and then receiving the returned values.

A command executes only non-GUI-related code so it will not deal with the GUI functions itself, only engine calls and game data. The GUI code will take care of visual representation for the user, and it will call the available commands.

The plug-ins will expose their set of commands, but they will have nothing to do with the GUI itself. You can create a separate plug-in system for the editor’s GUI. This is where the true decoupling kicks in, the editor core plug-ins being just “buckets” of non-GUI-related commands with the editor GUI using those commands. There is no need for a 1:1 match between the UI functions and the commands. You only need to expose the basic/simple commands, which should be generic enough to be used by multiple UI tools in various situations.

Command Parameters

When calling the commands, you have the option to send parameters to them, and for this you need to define the parameter type, direction, and description. This information is read from the plug-in’s manifest file, but it’s optional since the calling of commands is accomplished through a parameter set that is aware of the data types at the moment of setting the values. In Listing 11-9, you declare the IParameter interface.

Listing 11-9. The Command Parameter Interface

struct IParameter
{
    enum EDataType
    {
        eDataType_Unknown,
        eDataType_Int8,
        eDataType_Int16,
        eDataType_Int32,
        eDataType_Int64,
        eDataType_Float,
        eDataType_Double,
        eDataType_Text,
        eDataType_Handle
    };
    enum EDirection
    {
        eDirection_Input,
        eDirection_Output,
        eDirection_InputOutput
    };
    virtual ~IParameter(){}
    virtual const char* GetName() const = 0;
    virtual const char* GetDescription() const = 0;
    virtual EDataType GetDataType() const = 0;
    virtual EDirection GetDirection() const = 0;
    virtual bool IsArray() const = 0;
};

The IParameter interface is implemented by the editor core DLL, so plug-in developers do not need to care about the implementation, only what methods it provides, such as the name of the parameter, description, type, direction (if it’s an in/out parameter), and whether the parameter is an array of the type specified.

To keep the parameter information in one place, you declare an IParameterDefinitions interface, which holds the parameter information list for a command, as seen in Listing 11-10.

Listing 11-10. The Parameter Definitions Container Interface

struct IParameterDefinitions
{
    virtual size_t GetCount() const = 0;
    virtual IParameter* Get(size_t aIndex) const = 0;
    virtual bool Add(
            const char* pName,
            IParameter::EDataType aDataType,
            const char* pDescription,
            IParameter::EDirection aDirection,
            bool bArray) = 0;
};

When calling the commands, you need to pass the parameters. For this, you will use a IParameterValues values “bag,” which can set/get parameters and store the values. You can use other approaches for passing parameters, like #define extravaganza or templates to declare several command call forms with from one to ten parameters in their declaration. Listing 11-11 shows the parameter values interface.

Listing 11-11. The Parameter Values Container Interface

// Parameter values container, used to pass and receive
// in/out parameter values from a command call
struct IParameterValues
{
    virtual ~IParameterValues(){}
    virtual void SetInt8(const char* pParamName,
                         int8 aValue) = 0;
    virtual void SetInt16(const char* pParamName,
                          int16 aValue) = 0;
    virtual void SetInt32(const char* pParamName,
                          int32 aValue) = 0;
    virtual void SetInt64(const char* pParamName,
                          int64 aValue) = 0;
    virtual void SetFloat(const char* pParamName,
                          float aValue) = 0;
    virtual void SetDouble(const char* pParamName,
                           double aValue) = 0;
    virtual void SetText(const char* pParamName,
                         const char* pValue) = 0;
    virtual void SetHandle(const char* pParamName,
                           Handle aValue) = 0;
 
    virtual void SetInt8Array(const char* pParamName,
                              Int8Array aArray) = 0;
    virtual void SetInt16Array(const char* pParamName,
                               Int16Array aArray) = 0;
    virtual void SetInt32Array(const char* pParamName,
                               Int32Array aArray) = 0;
    virtual void SetInt64Array(const char* pParamName,
                               Int64Array aArray) = 0;
    virtual void SetFloatArray(const char* pParamName,
                               FloatArray aArray) = 0;
    virtual void SetDoubleArray(const char* pParamName,
                                DoubleArray aArray) = 0;
    virtual void SetTextArray(const char* pParamName,
                              TextArray aArray) = 0;
    virtual void SetHandleArray(const char* pParamName,
                                HandleArray aArray) = 0;
 
    virtual int8 GetInt8(
                     const char* pParamName) const = 0;
    virtual int16 GetInt16(
                     const char* pParamName) const = 0;
    virtual int32 GetInt32(
                     const char* pParamName) const = 0;
    virtual int64 GetInt64(
                     const char* pParamName) const = 0;
    virtual float GetFloat(
                     const char* pParamName) const = 0;
    virtual double GetDouble(
                     const char* pParamName) const = 0;
    virtual const char* GetText(
                     const char* pParamName) const = 0;
    virtual Handle GetHandle(
                     const char* pParamName) const = 0;
 
    virtual Int8Array GetInt8Array(
                     const char* pParamName) const = 0;
    virtual Int16Array GetInt16Array(
                     const char* pParamName) const = 0;
    virtual Int32Array GetInt32Array(
                     const char* pParamName) const = 0;
    virtual Int64Array GetInt64Array(
                     const char* pParamName) const = 0;
    virtual FloatArray GetFloatArray(
                     const char* pParamName) const = 0;
    virtual DoubleArray GetDoubleArray(
                     const char* pParamName) const = 0;
    virtual TextArray GetTextArray(
                     const char* pParamName) const = 0;
    virtual HandleArray GetHandleArray(
                     const char* pParamName) const = 0;
 
    // delete all parameter values
    virtual void Clear() = 0;
    // get the number of parameters this list holds
    virtual size_t GetCount() const = 0;
    // get the data type of parameter at given index
    virtual IParameter::EDataType GetDataType(
                     size_t aIndex) const = 0;
    // get the direction of parameter at given index
    virtual IParameter::EDirection GetDirection(
                     size_t aIndex) const = 0;
    // get the data type of parameter at given index
    virtual const char* GetName(size_t aIndex) const = 0;
    // is this parameter an array at given index?
    virtual bool IsArray(size_t aIndex) const = 0;
};

To avoid memory fragmentation due to frequent command calls, you would ideally manage the parameter values through a memory pool. The actual command is a callback function receiving a parameter values set, and is declared as shown in Listing 11-12.

Listing 11-12. The Command Callback Function Type

typedef void (*TPfnCommand)(IParameterValues* pParams);

For debugging and auto-documentation purposes, the editor core API can provide detailed command information through the ICommand interface, which can hold the command description from the plug-in manifest file, plus the command callback function pointer, as shown in Listing 11-13.

Listing 11-13. The Command Information Provider Interface

struct ICommand
{
    virtual ~ICommand(){}
    virtual const char* GetName() const = 0;
    virtual const char* GetDescription() const = 0;
    virtual const char* GetIconFilename() const = 0;
    virtual TPfnCommand GetCommandFunc() = 0;
    virtual const IParameterDefinitions*
                      GetParameterDefinitions() const = 0;
};

Direct Editor API Command Calls

You can call the editor core interface for executing commands directly from C++ or use a wrapper tool for another language like C# (SWIG). To call the commands in C++, use the code shown in Listing 11-14.

Listing 11-14. How to Call a Command

// create a parameter values bag
IParameterValues* pParams =
                      pEditor->CreateParameterValues();
// set some parameter values
pParams->SetInt32(“someParam1”, 123);
pParams->SetText(“someName”, “Elena Lenutza Nedelcu”);
pParams->SetText(“someOtherName”, “Dorinel Nedelcu”);
// the actual command call
pEditor->Call(“someCommandName”, pParams);
// retrieve the return values
float fRetVal = pParams->GetFloat(“returnSomeValue”);
int someNum = pParams->GetInt32(“otherValue”);

Remote Editor API Command Calls

You can use sockets for calling the commands remotely, since they’re cross-platform and relatively easy to use from any language or environment. On the editor core DLL side, you will have a network server executable, and on the editor UI side, you will have a network client sending and receiving command data.

Communication can be accomplished through reliable UDP or TCP. For a local editor on the same machine, TCP would be okay even for LAN scenarios. If you are not so keen on using TCP because you consider it slow, UDP should suffice to send commands. All logic remains the same in this networked scenario, but this setup opens doors to online collaboration of multiple clients operating on the same data on the server. I’m not going to discuss this here, since it’s a subject for a whole chapter (a challenging and interesting one!).

Networked editing is also feasible for debugging and remote in-editor live tutorials.

Putting It All Together

The editor can be implemented in Qt (just an example, chosen for its cross-platform support, though C# can also be supported using Mono on platforms other than Windows). This editor will be an empty skeleton that contains a plug-in manager dialog and nothing else, since all the functionality will be brought in by the plug-ins. Once again you need to emphasize the separation of the plug-in systems. They are two systems, one for the UI, and one for the editor core commands. UI plug-ins will use the commands found in the editor core plug-ins (see Figure 11-1 at the beginning of this chapter). The main UI editor can even do without a plug-in system if it’s so intended, but the editor core command plug-ins will still exist.

Implementing a Plug-in with Commands

To ensure that you have a simple way of implementing new commands, the method of declaring commands and plug-ins must be straightforward. In the editor core API, IPlugin is the interface a plug-in must implement. To help rapid plug-in development, you can write a series of macros. In this sample plug-in, implementing a few commands would look like the code shown in Listing 11-15.

Listing 11-15. A Sample Plug-in Implementation

#include “EditorApi.h”
 
void example_my_command1(IParameterValues* pParams)
{
   // get our calling parameter values
    int numberOfHorses =
             pParams->GetInt32("numberOfHorses");
   std::string dailyMessage =
             pParams->GetText("dailyMessage");
   // do something important here for the command...
 
   // return some parameter values
   pParams->SetDouble("weightOfAllHorses", 1234.0f);
   pParams->SetText("userFullName", "Mihaela Claudia V.");
}
 
void example_my_command2(IParameterValues* pParams)
{
   // now here we'll try to grab an array
   FloatArray magicFloats =
                 pParams->GetFloatArray("magicFloats");
 
   for (size_t i = 0; i < magicFloats.count; ++i)
   {
      float oneMagicFloat = magicFloats.elements[i];
      // do something majestic with the float...
   }
   // we do not need to return any value now
}
 
BEGIN_PLUGIN
 
void Initialize(IEditor* pEditor)
{
   REGISTER_COMMAND(example_my_command1);
   REGISTER_COMMAND(example_my_command2);
}
 
// used to check if a command is disabled at that time
// can be helpful for UI to disable buttons in toolbars
// or other related visual feedback
bool IsCommandEnabled(TPfnCommand pCmdFunc)
{
   return true;
}
 
void Shutdown()
{
}
 
END_PLUGIN

Note that BEGIN_PLUGIN and END_PLUGIN are macros hiding the start/end of the IPlugin interface implementation. The Initialize method is called when the plug-in is loaded into the editor. You are also registering the plug-in’s commands by just referring invoking the global functions example_my_command1 and example_my_command1. The Shutdown method is called when the plug-in is unloaded (no need to call the unregister commands; this can be tracked and executed by the editor core itself, since it knows the IPlugin pointer when the commands are registered). The IsCommandEnabled method is used to verify whether a command has the status of “enabled” so it can be called/executed.

Be sure to name the commands in a way that avoids conflicts. Usually some sort of group naming, like the name of the plug-in and the actual command action name, should be enough, like assets_reload, assets_set_tag, assets_delete, or if you prefer camel-case style, Assets_SetTag.

The generated plug-in will be named example.dll and will be accompanied by its manifest file, example.plugin.xml. Of course, the plug-in must export a CreatePluginInstance global function so the editor core can load it and instantiate the IPlugin implementation.

Events

To make the plug-ins aware of events occurring in the editor ecosystem, they can register themselves as event sinks, as shown in Listing 11-16.

Listing 11-16. An Event Sink, Which Can Be Implemented by the Plug-ins

// Every plug-in can register its event sink so it can
// receive notifications about events happening in the
// editor ecosystem, coming from other plug-ins or the
// editor core itself
struct IEventSink
{
    // When the event sink call is received, before, during or
    // after the event was consumed
    // The eTriggerContext_During can be used to have
    // lengthy events being processed and many triggered to
    // update some progress bars
    enum ETriggerContext
    {
        eTriggerContext_Before,
        eTriggerContext_During,
        eTriggerContext_After
    };
    virtual ~IEventSink(){}
    virtual void OnEvent(IEvent* pEvent,
                         ETriggerContext aContext,
                         void* pUserData) = 0;
};

The IEventSink::OnEvent method is called whenever an event is triggered by other plug-ins or their commands and broadcast to the registered event sinks. The method receives a pointer to the triggered event interface (see Listing 11-17).

Listing 11-17. An Event, Implemented by the Trigger Code

// An event is triggered when certain actions are happening
// in the editor or its plug-ins. For example we can have an
// event at Save level or an object moved with the mouse
struct IEvent
{
    virtual ~IEvent(){}
    virtual const char* GetName() = 0;
    virtual void OnTrigger(void* pUserData) = 0;
    virtual void* GetUserData() = 0;
};

Listing 11-18 shows how to trigger an event.

Listing 11-18. Creating, Registering, and Triggering an Event

// we declare an event
struct MyEvent: IEvent
{
    virtual const char* GetName()
    {
        return “MyCoolEvent”;
    }
    // this will be called when the event is triggered,
    // before being broadcast to all the event sinks
    // so the event can even modify the user data
    virtual void OnTrigger(void* pUserData)
    {
        // modify or store the pUserData
        m_pData = pUserData;
    }
    virtual void* GetUserData()
    {
        return m_pData;
    }
    uint8 m_pData;
} s_myEvent;
 
// we register an event (usually in the Initialize method
// of the plug-in)
...
REGISTER_EVENT(&s_myEvent);
...
// in some command, we trigger the event
void my_command(IParameterValues* pParams)
{
    uint8* pSomeData;
    // ....... do things with pSomeData
    g_pEditor->TriggerEvent(
                   &s_myEvent,
                   IEventSink::eTriggerContext_After,
                   pSomeData);
}

In some plug-ins, an event sink registered for a particular event would be notified of the event being triggered, as shown in Listing 11-19.

Listing 11-19. Creating and Registering an Event Sink

// declare our event sink
struct MyEventSink: IEventSink
{
    void OnEvent(IEvent* pEvent,
                 ETriggerContext aContext,
                 void* pUserData)
    {
        // is this the event we’re looking for?
        if (!strcmp(pEvent->GetName(), “MyCoolEvent”))
        {
            uint8* pEventData = pEvent->GetUserData();
            // ...do things when that event was triggered
        }
    }
} s_myEventSink;
 
// inside the plug-in’s Initialize method, register
// the event sink
...
pEditor->RegisterEventSink(&s_myEventSink);
...

In Figure 11-3, you can see a demo application of this system, with the editor skeleton having just one menu item and a settings dialog where plug-ins are managed.

9781430267003_Fig11-03.jpg

Figure 11-3. Skeleton editor UI application and settings dialog, with the plug-in manager (made with Qt)

Conclusion

Decoupling the UI from core editing functionality helps the development and fast feature-set iteration of game creation tools, since fewer hard-coded dependencies and monolithic schemes are used. The tools can be extended and used for a wide range of projects, the editor itself being quite agnostic to the game type and even the engine used. The solution presented here can be implemented in many ways, from the command system interfaces to the UI or plug-in system. In all cases, one thing remains constant: the use of UI-independent editing operations is separated from the tools’ GUI using the command layer. I hope this article inspires you to make the right choices when creating extensible, elegant solutions for your game development tools.

________________

1Nokia. “Qt—Cross-platform application and UI framework.” http://qt.nokia.com.

2“Inter-process communication.” http://en.wikipedia.org/wiki/Inter-process_communication.

3“SWIG.” http://en.wikipedia.org/wiki/SWIG.

4“XML.” http://en.wikipedia.org/wiki/XML.

5Wikipedia. “Globally unique identifier.” http://en.wikipedia.org/wiki/Globally_unique_identifier.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset