Orx–Data Driven 2D Game Engine


Orx is a cross platform open sourced 2D game framework written in C and available on Github under the permissive ZLib source license.  What makes Orx somewhat unique among it’s peers is the data driven approach that Orx takes.  Instead of storing game data using a collection of objects in code, it is instead split out into data files.  This enables you to quickly change the content of your game without having to do a recompilation.   So for example you make have an entry like the following for defining the camera in your scene:

[Viewport]  Camera            = Camera  BackgroundColor   = (255, 180, 0)    [Camera]  FrustumWidth  = 1024  FrustumHeight = 768  FrustumFar    = 1.0  FrustumNear   = 0.0  Position      = (0.0, 0.0, -1.0)  ;Zoom          = 3.0

By editing this config file you can make major alterations to the camera without having to recompile your code.  Chance the FrustrumWidth in the ini file, and the next time you run your game’s window will be smaller.  Now let’s look at the code required to consume the Viewport/Camera data in your C/C++ application:


Yep, that’s it.  This will create the viewport, notice the camera property and create one of them as well, entirely driven by data. 

This approach has a couple of advantages.  First it creates a clean separation of code and data which should in theory make for easier to maintain code.  It also takes these values out of the compilation process, meaning changes do not require you to recompile your code.  Additionally, this data driven approach makes creating tools for you game a cinch, as you are simply writing out simple easily understood text files. 

There are of course disadvantages too.  Since you moved the code out from the compiler, you lose features such as intellisense as well as compile time error checking.  Moving more details off into data files can make hunting down bugs even more difficult.

Let’s take a quick look at one of the samples from the Orx tutorials, with the comments stripped down for brevity.

#include "orx.h"  orxOBJECT *pstSoldier;    orxSTATUS orxFASTCALL EventHandler(const orxEVENT *_pstEvent)  {    orxANIM_EVENT_PAYLOAD *pstPayload;      /* Gets event payload */    pstPayload = (orxANIM_EVENT_PAYLOAD *)_pstEvent->pstPayload;      /* Depending on event type */    switch(_pstEvent->eID)    {      case orxANIM_EVENT_START:      {        /* Logs info */        orxLOG("Animation <%s>@<%s> has started!", pstPayload->zAnimName, orxObject_GetName(orxOBJECT(_pstEvent->hRecipient)));          break;      }        case orxANIM_EVENT_STOP:      {        /* Logs info */        orxLOG("Animation <%s>@<%s> has stopped!", pstPayload->zAnimName, orxObject_GetName(orxOBJECT(_pstEvent->hRecipient)));          break;      }        case orxANIM_EVENT_CUT:      {        /* Logs info */        orxLOG("Animation <%s>@<%s> has been cut!", pstPayload->zAnimName, orxObject_GetName(orxOBJECT(_pstEvent->hRecipient)));          break;      }        case orxANIM_EVENT_LOOP:      {        /* Logs info */        orxLOG("Animation <%s>@<%s> has looped!", pstPayload->zAnimName, orxObject_GetName(orxOBJECT(_pstEvent->hRecipient)));          break;      }        case orxANIM_EVENT_CUSTOM_EVENT:      {         /* Logs info */         orxLOG("Animation <%s>@<%s> has sent the event [%s]!", pstPayload->zAnimName, orxObject_GetName(orxOBJECT(_pstEvent->hRecipient)), pstPayload->stCustom.zName);          break;      }    }      /* Done! */    return orxSTATUS_SUCCESS;  }    /** Update callback   */  void orxFASTCALL Update(const orxCLOCK_INFO *_pstClockInfo, void *_pstContext)  {    orxVECTOR vScale;      /* Is walk right active? */    if(orxInput_IsActive("GoRight"))    {      /* Sets walk right as target anim */      orxObject_SetTargetAnim(pstSoldier, "WalkRight");    }    /* Is walk left active? */    else if(orxInput_IsActive("GoLeft"))    {      /* Sets walk left as target anim */      orxObject_SetTargetAnim(pstSoldier, "WalkLeft");    }    /* No walk active */    else    {      /* Removes target anim */      orxObject_SetTargetAnim(pstSoldier, orxNULL);    }      /* Is scale up active ? */    if(orxInput_IsActive("ScaleUp"))    {      /* Scales up the soldier */      orxObject_SetScale(pstSoldier, orxVector_Mulf(&vScale, orxObject_GetScale(pstSoldier, &vScale), orx2F(1.02f)));    }    /* Is scale down active? */    if(orxInput_IsActive("ScaleDown"))    {      /* Scales down the soldier */      orxObject_SetScale(pstSoldier, orxVector_Mulf(&vScale, orxObject_GetScale(pstSoldier, &vScale), orx2F(0.98f)));    }  }      /** Inits the tutorial   */  orxSTATUS orxFASTCALL Init()  {    orxCLOCK       *pstClock;    orxINPUT_TYPE   eType;    orxENUM         eID;    orxINPUT_MODE   eMode;    const orxSTRING zInputWalkLeft;    const orxSTRING zInputWalkRight;    const orxSTRING zInputScaleUp;    const orxSTRING zInputScaleDown;      /* Gets input binding names */    orxInput_GetBinding("GoLeft", 0, &eType, &eID, &eMode);    zInputWalkLeft  = orxInput_GetBindingName(eType, eID, eMode);      orxInput_GetBinding("GoRight", 0, &eType, &eID, &eMode);    zInputWalkRight = orxInput_GetBindingName(eType, eID, eMode);      orxInput_GetBinding("ScaleUp", 0, &eType, &eID, &eMode);    zInputScaleUp   = orxInput_GetBindingName(eType, eID, eMode);      orxInput_GetBinding("ScaleDown", 0, &eType, &eID, &eMode);    zInputScaleDown = orxInput_GetBindingName(eType, eID, eMode);      /* Displays a small hint in console */    orxLOG("n- '%s' & '%s' will change the soldier's animationsn- '%s' & '%s' will scale the soldier", zInputWalkLeft, zInputWalkRight, zInputScaleUp, zInputScaleDown);      /* Registers event handler */    orxEvent_AddHandler(orxEVENT_TYPE_ANIM, EventHandler);      /* Creates viewport */    orxViewport_CreateFromConfig("Viewport");      /* Creates soldier */    pstSoldier = orxObject_CreateFromConfig("Soldier");      /* Gets main clock */    pstClock = orxClock_FindFirst(orx2F(-1.0f), orxCLOCK_TYPE_CORE);      /* Registers our update callback */    orxClock_Register(pstClock, Update, orxNULL, orxMODULE_ID_MAIN, orxCLOCK_PRIORITY_NORMAL);      /* Done! */    return orxSTATUS_SUCCESS;  }    /** Run function   */  orxSTATUS orxFASTCALL Run()  {    orxSTATUS eResult = orxSTATUS_SUCCESS;      /* Should quit? */    if(orxInput_IsActive("Quit"))    {      /* Updates result */      eResult = orxSTATUS_FAILURE;    }      /* Done! */    return eResult;  }    /** Exit function   */  void orxFASTCALL Exit()  {    /* We're a bit lazy here so we let orx clean all our mess! 🙂 */  }    /** Main function   */  int main(int argc, char **argv)  {    /* Executes a new instance of tutorial */    orx_Execute(argc, argv, Init, Run, Exit);      return EXIT_SUCCESS;  }      #ifdef __orxMSVC__    // Here's an example for a console-less program under windows with visual studio  int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)  {    // Inits and executes orx    orx_WinExecute(Init, Run, Exit);      // Done!    return EXIT_SUCCESS;  }    #endif // __orxMSVC__  

This example illustrates a simple animation.  The game loop itself consists of implementing 3 simple functions, Init(), Run() and Exit() for handling various stages of the lifecycle.  The actual game loop itself is the Update callback function which is called continuously as your game is run.  Speaking of which, here is the code running.  The left and right arrow keys toggle between the animations, while up and down scale accordingly.


Now the “guts” of this example is actually in the ini file containing the data.  Let’s take a look:

[Display]  ; In this example, we use the same size for the screen display than our camera's frustum so as to obtain a 1:1 ratio  ScreenWidth   = @Camera.FrustumWidth  ScreenHeight  = @Camera.FrustumHeight  Title         = Anim Tutorial  Smoothing     = false
[Resource]  Texture = ../data/object     [Input]  SetList = MainInput    [MainInput]  KEY_ESCAPE = Quit    KEY_LEFT   = GoLeft  KEY_RIGHT  = GoRight  KEY_UP     = ScaleUp  KEY_DOWN   = ScaleDown    [Viewport]  Camera = Camera    [Camera]  FrustumWidth  = 640  FrustumHeight = 480  FrustumFar    = 1.0  FrustumNear   = 0.0  Position      = (0.0, 0.0, -1.0)    [Pivot]  Pivot = (15.0, 31.0, 0.0)    [[email protected]]  Texture = soldier.png    [Soldier]  Graphic             = Graphic  AnimationSet        = AnimSet  Scale               = 4.0    [AnimSet]  Direction   = right # down  StartAnim   = IdleRight  KeyDuration = 0.1  Digits      = 1  FrameSize   = (32, 32, 0)    Texture     = soldier_full.png
Pivot       = @Pivot    IdleRight   = 1 ; <= We only want one frame  IdleLeft    = 1  WalkRight   = -1 ; <= We want as many frame that can fit in the texture defined by WalkRight  WalkLeft    = -1    IdleRight-> = IdleRight 
IdleLeft->  = IdleLeft   WalkRight-> = WalkRight   WalkLeft->  = WalkLeft     [IdleLeft]  Flip        = x    [WalkLeft]  Flip        = x    [IdleRight]  Direction   = left    [IdleLeft]  Direction   = left # up    [WalkRight1]  KeyEvent    = !!Left!!    [WalkRight4]  KeyEvent    = !!Right!!    [WalkLeft1]  KeyEvent    = !!Right!!    [WalkLeft4]  KeyEvent    = !!Left!!  

As you can see, just about every aspect of the game is data driven here, from the animation names, the source graphic files, to the camera, viewport and even keys pressed and the corresponding binding to call for each key.

This post only scratched the service of how Orx works, but should give you an idea of the data driven approach they have taken.  It certainly wont be for everyone, but it could be a great fit for many people.  If you are interested in giving Orx a shot, be sure to check out our video, which is also embedded below, to walk through the installation process and get you up and running.  Then it’s time to jump into the extremely thorough getting started guide and tutorials available on the Orx website.


Scroll to Top