Extending your Lua Interface

From CEGUI Wiki - Crazy Eddie's Gui System for Games (Open Source)
Jump to: navigation, search

Written for CEGUI 0.7


Works with versions 0.7.x (obsolete)

This article explains how to extend the lua interface by binding classes and functions from your game code using tolua++. In other words, this article will show you how to interact with your game objects within a lua script. A majority of the content explained here already exists within this wiki (namely: Creating a scriptable interface using CEGUI), but this article details the process in one convenient article.

The examples come from the code I am currently using in my game (simplified a bit for instructional purposes). I am using CEGUI version 0.7.5.

Contents

Step 1: Make the package file(s)

Package files are little more than copies of your header files, slightly modified for tolua++. Foremost, note that you only copy the public members. We are going to add two classes to our lua interface. First, I will show the actual c++ header files:

 // Game			 Game.hpp			      Header
 //_________________________________________________________________________
 namespace PAL_LIB
 {
     class Level;
     class Gui;
     //
     class Game
     {
     public:
          enum ContextTypeEnum
          {
               CT_MENU     = 0x01000uL,
               CT_LEVEL_01 = 0x02000uL,
               CT_LEVEL_02 = 0x04000uL,
               CT_LEVEL_03 = 0x08000uL,
          };
          typedef ContextTypeEnum Context;
          //
          static Game & Instance(void);
          static Game * InstancePtr(void);
          ~Game(void);
          //
          void    init          (void);
          void    run           (void);
          void    triggerExit   (void);
          void    pause         (void);
          void    step          (void);
          void    reloadContext (void);
          void	  changeContext (Level * context);
          void	  changeContext (Context context);
          Level * getContext    (void) const;
          Gui   * getGui        (void) const;
          //
     private:
          static Game * s_instance;
          Game(void);
          //
          void loopGame            (void);
          bool verifyExitConditions(void);
          //
          bool     running_;
          bool     restart_;
          Level  * context_;
          Level  * nextContext_;
          Gui    * gui_;
     }; // class Game
 } // ns PAL_LIB
 
 // GUI				 Gui.hpp			      Header
 //_________________________________________________________________________
 namespace CEGUI 
 {
     class WindowManager;
     class Window;
 };
 namespace PAL_LIB
 {
     class Gui
     {
          enum WindowEnum
          {
               UI_MAIN    = 0x01,
               UI_LOADING = 0x02,
               UI_LEVEL   = 0x04,
               UI_PAUSE   = 0x08,
               UI_HUD     = 0x10,
          };
          //		
     public:
          Gui(void);
          ~Gui(void);
          //
          void init          (void);
          void update        (float dt);
          //
          void setLoading    (uint progress, String const& title);		
          //
          void showMain      (void);
          void showLoading   (void);
          void showLevelEnd  (void);
          void showPause     (void);
          void showHUD       (void);
          //
          void hideMain      (void);
          void hideLoading   (void);
          void hideLevelEnd  (void);
          void hidePause     (void);
          void hideHUD	     (void);
          //
     protected:
          void updateMain     (float dt);
          void updateLoading  (float dt);
          void updateLevelEnd (float dt);
          void updatePause    (float dt);
          void updateHUD      (float dt);
          //
          ushort                 active_;
          CEGUI::WindowManager * wmgr_;
          CEGUI::Window        * wroot_;
     }; // class Gui
 } // ns PAL_LIB

Notice that I have these class defined within my namespace (PAL_LIB). As you will see, our package files will specify this namespace in a slightly different manner. Also note that you do not have to have to define your classes (though it is generally a good idea to partition a large project, such as game, into one or more namespaces). When we make our package files, we simply remove all the private and protected bits. Like so:

 // Game			Game.pkg			     Package
 //_________________________________________________________________________
 class Game
 {
 public:
     enum ContextTypeEnum
     {
          CT_MENU,
          CT_LEVEL_01,
          CT_LEVEL_02,
          CT_LEVEL_03
     };
     typedef ContextTypeEnum Context;
     //
     static Game & Instance(void);
     //
     void   triggerExit    (void);
     void   pause          (void);
     void   step           (void);
     void   reloadContext  (void);
     void   changeContext  (Context context);
     Gui  * getGui         (void) const;
 }; // class Game
 
 // GUI				 Gui.pkg			     Package
 //_________________________________________________________________________
 class Gui
 {
 public:
     void showMain     (void);
     void showLoading  (void);
     void showLevelEnd (void);
     void showPause    (void);
     void showHUD      (void);
     //
     void hideMain     (void);
     void hideLoading  (void);
     void hideLevelEnd (void);
     void hidePause    (void);
     void hideHUD      (void);
 }; // class Gui

You may have noticed that I left out a little more than just the non-public stuff from the game package: The enum values are not needed. The functions, Instance(void), getContext(void), changeContext(Level *), and a few others were left out because I don't want--or need--them in the lua interface. Similarly, the constructors and destructors are not copied into the package file. Note, however, if you wanted to instantiate an object from a lua script, then you would need a constructor (or some function that calls a constructor--like Game::Instance(void)).

Before we can use these packages with tolua++, we need one more package file. This package will simply include all the package files you want to add to your lua interface. In this case, it will include Game.pkg and Gui.pkg. Also, we will use this package file to define our namespace. One other thing to note here is the order in which the package files are listed. If I referenced Game.pkg after Gui.pkg, tolua++ would fail.

 // Lua Interface	     Lua Interface.pkg			     Package
 //_________________________________________________________________________
 $#include "Game.hpp"
 $#include "Gui.hpp"
 //
 namespace PAL_LIB
 {
     $pfile "LUAGame.pkg"
     $pfile "LUAGui.pkg"
 }

Step 2: Compile the package file(s)

For this step, you will need two things. The first, is tolua++.exe. You should find this in your CEGUI SDK bin folder (after you have compiled the SDK, that is). It may be named slightly differently depending on which build configuration you used to build CEGUI. Mine was named "tolua++ceguiStatic.exe" (yeah, yeah ... shame on me for statically linking), but I renamed it for convenience sake. The second thing you don't /really/ need, but it sure as heck makes life easier. It is a batch file (or shell script for you linux folks out there). You can make it by creating a text file, renaming it to "toluaMyLua.bat" (or [something similar].bat), and adding the following lines via your favorite text editor:

REM Create lua interface
    tolua++ -H LuaInterface.hpp -o LUAInterface.cpp LuaInterface.pkg
pause

Save it, run it, and ... hopefully--if you have done everything correctly--it will generate two files: LuaInterface.hpp and LUAInterface.cpp


Step 3: Include the single generated header in your application & apply it to your script module

Including the header in your application is easy (you just #include it), but first you will have to make a few adjustments to your project build configuration. You will need the Lua SDK (source code) for this part. If you don't already have it, you can download it here. After you download and extract the lua source, you will need to add the lua source folder to your "additional include directories". While you're there, also add "[CEGUI SDK Folder]cegui/include/ScriptingModules/LuaScriptModule/support/tolua++". This second include directory is merely for convenience (because tolua++.exe doesn't specify that awkwardly long path when it includes "tolua++.h" in its generated files).

Next, copy the two generated files into your project path and add them to your project. Open "LUAInterface.hpp" and add an include for "lstate.h". The final header should look this:

 /*
 ** Lua binding: LuaInterface
 ** Generated automatically by tolua++-1.0.92 on 12/15/10 11:10:54.
 */
 #include "lstate.h"
 //
 /* Exported function */
 int tolua_LuaInterface_open (lua_State* tolua_S);

Finally, open whichever source file you are using to initialize CEGUI. I initialize CEGUI in the init function of my Gui class: "Gui.cpp". Include "LUAInterface.h". Now, you may have to adjust the way you initialize CEGUI. I had to switch from the bootstrap method to the manual method. My initialization function looks like this (notice we're calling the function from LUAInterface, "tolua_LuaInterface_open(luaState);" after creating the LuaScriptModule):

 void Gui::init(void)
 {
     Direct3D9Renderer & renderer = Direct3D9Renderer::create(GetDirect3DDevice());
     //
     // Add tolua++ bindings to your script module
     script_   = &LuaScriptModule::create();
     lua_State * luaState = script_->getLuaState();
     tolua_LuaInterface_open(luaState);
     //
     // Create a ResourceProvider and initialize the required dirs
     DefaultResourceProvider * rp = new DefaultResourceProvider;
     rp->setResourceGroupDirectory("schemes",     "gui/schemes/"     );
     rp->setResourceGroupDirectory("imagesets",   "gui/imagesets/"   );
     rp->setResourceGroupDirectory("fonts",       "gui/fonts/"       );
     rp->setResourceGroupDirectory("layouts",     "gui/layouts/"     );
     rp->setResourceGroupDirectory("looknfeels",  "gui/looknfeel/"   );
     rp->setResourceGroupDirectory("lua_scripts", "gui/lua_scripts/" );
     //
     // Create the CEGUI::System object (using the render, resource provider,
     // and script module created above .. the two NULLs specify System to
     // use the default xml parser and image codec, respectively)
     System * system = &System::create(renderer, rp, NULL, NULL, script_);
     //
     // set the default resource groups to be used
     Imageset::setDefaultResourceGroup          ("imagesets"   );
     Font::setDefaultResourceGroup              ("fonts"       );
     Scheme::setDefaultResourceGroup            ("schemes"     );
     WidgetLookManager::setDefaultResourceGroup ("looknfeels"  );
     WindowManager::setDefaultResourceGroup     ("layouts"     );
     ScriptModule::setDefaultResourceGroup      ("lua_scripts" );
     //
     // Load lua scripts
     system.executeScriptFile("gui_masteroids.lua", "lua_scripts");
     //
     wmgr_  = WindowManager::getSingletonPtr();
     wroot_ = WindowManager::getSingleton().getWindow("Root");
     wroot_->activate();		
 }

And that's it as far as your game code is concerned. You may want to compile your project now to ensure you got everything correct. The rest is done via your layouts and scripts ... which is the next step.

Step 4: Creating your lua script(s) & adding events to your layout(s)

Now you need some Lua script functions to exploit your new lua interface functions. The following lua script is a simplified version of what I am using in my game:

 -----------------------------------------
 -- SCRIPT: gui_masteroids.lua
 -----------------------------------------
 function btn_clicked_main_play(e)
    local we = CEGUI.toWindowEventArgs(e)
    PAL_LIB.Game:Instance():getGui():hideMain()
    PAL_LIB.Game:Instance():changeContext(PAL_LIB.Game.CT_LEVEL_01)
 end
 --
 function btn_clicked_options(e)
    local we = CEGUI.toWindowEventArgs(e)
    CEGUI.WindowManager:getSingleton():getWindow("Root"):getChild("Root/ErrorMsg"):show()
 end
 --
 function btn_clicked_quit(e)
    local we = CEGUI.toWindowEventArgs(e)
    PAL_LIB.Game:Instance():triggerExit()
 end
 --
 function btn_clicked_msgbox_ok(e)
    local we = CEGUI.toWindowEventArgs(e)
    local w  = we.window:getParent()
    w:hide()
    w:deactivate()
 end
 --
 function btn_clicked_pause_main(e)
    PAL_LIB.Game:Instance():getGui():hidePause()
    PAL_LIB.Game:Instance():unpause();
    PAL_LIB.Game:Instance():changeContext(PAL_LIB.Game.CT_MENU)
 end
 --
 function btn_clicked_pause_resume(e)
    PAL_LIB.Game:Instance():getGui():hidePause()
    PAL_LIB.Game:Instance():unpause();
 end
 --
 function btn_clicked_pause_restart(e)
    PAL_LIB.Game:Instance():getGui():hidePause()
    PAL_LIB.Game:Instance():unpause();
    PAL_LIB.Game:Instance():reloadContext()
 end
 --
 -----------------------------------------
 -- Script Entry Point
 -----------------------------------------
 local logger = CEGUI.Logger:getSingleton()
 logger:logEvent(">>> Init script says hello")
 --
 local system    = CEGUI.System:getSingleton()
 local fontman   = CEGUI.FontManager:getSingleton()
 local schememan = CEGUI.SchemeManager:getSingleton()
 local winman    = CEGUI.WindowManager:getSingleton()
 --
 schememan:create("TaharezLook.scheme", "schemes" )
 fontman:create	("FairChar-30.font",    "fonts"   )
 fontman:create	("DejaVuSans-10.font",  "fonts"   )
 --
 system:setDefaultMouseCursor("TaharezLook", "MouseArrow")
 system:setDefaultTooltip("TaharezLook/Tooltip")
 --
 local root = winman:loadWindowLayout("Masteroids.layout")
 system:setGUISheet(root)
 --
 logger:logEvent("<<< Init script says goodbye")

This script is called near the end of my initialization function (see above, Step 3). It loads a scheme, two fonts, and my layout. It also contains some function which will be used as event callbacks for my UI elements. As you may notice, several function make calls to varies functions of my Game object. The first function, "btn_clicked_main_play" tells the Gui object to hide Main menu window (which could be done without calling the Gui object, except that I have some other code in that function which I didn't want add to the lua interface); and then, it tells the Game object to change contexts (which effectively causes my game to switch states and load the first level). These functions are called via the event system. So, the next step is to specify (subscribe to) events in your layout file. Here's mine (again, simplified):

 <?xml version="1.0" encoding="UTF-8"?>
 <GUILayout >
    <Window Type="DefaultWindow" Name="Root" >
        <Property Name="UnifiedAreaRect" Value="{{0,0},{0,0},{1,0},{1,0}}" />
        <Window Type="TaharezLook/FrameWindow" Name="Root/Pause" >
            <Property Name="Text" Value="Pause Menu" />
            <Property Name="Visible" Value="False" />
            <Property Name="TitlebarFont" Value="DejaVuSans-10" />
            <Property Name="SizingEnabled" Value="False" />
            <Property Name="TitlebarEnabled" Value="True" />
            <Property Name="UnifiedAreaRect" Value="{{0,0},{0,0},{0.35,0},{0.5,0}}" />
            <Property Name="VerticalAlignment" Value="Centre" />
            <Property Name="CloseButtonEnabled" Value="False" />
            <Property Name="HorizontalAlignment" Value="Centre" />
            <Window Type="TaharezLook/Button" Name="Root/Pause/btn_Resume" >
                <Property Name="Font" Value="FairChar-30" />
                <Property Name="Text" Value="RESUME" />
                <Property Name="UnifiedAreaRect" Value="{{0.05,0},{0.05,0},{0.95,0},{0.2,0}}" />
                <Event Name="Clicked" Function="btn_clicked_pause_resume" />
            </Window>
            <Window Type="TaharezLook/Button" Name="Root/Pause/btn_Restart" >
                <Property Name="Font" Value="FairChar-30" />
                <Property Name="Text" Value="RESTART" />
                <Property Name="UnifiedAreaRect" Value="{{0.05,0},{0.225,0},{0.95,0},{0.375,0}}" />
                <Event Name="Clicked" Function="btn_clicked_pause_restart" />
            </Window>
            <Window Type="TaharezLook/Button" Name="Root/Pause/btn_Options" >
                <Property Name="Font" Value="FairChar-30" />
                <Property Name="Text" Value="OPTIONS" />
                <Property Name="UnifiedAreaRect" Value="{{0.05,0},{0.4,0},{0.95,0},{0.55,0}}" />
                <Event Name="Clicked" Function="btn_clicked_options" />
            </Window>
            <Window Type="TaharezLook/Button" Name="Root/Pause/btn_Main" >
                <Property Name="Font" Value="FairChar-30" />
                <Property Name="Text" Value="MAIN MENU" />
                <Property Name="UnifiedAreaRect" Value="{{0.05,0},{0.625,0},{0.95,0},{0.775,0}}" />
                <Event Name="Clicked" Function="btn_clicked_pause_main" />
            </Window>
            <Window Type="TaharezLook/Button" Name="Root/Pause/btn_Quit" >
                <Property Name="Font" Value="FairChar-30" />
                <Property Name="Text" Value="QUIT" />
                <Property Name="UnifiedAreaRect" Value="{{0.05,0},{0.8,0},{0.95,0},{0.95,0}}" />
                <Event Name="Clicked" Function="btn_clicked_quit" />
            </Window>
        </Window>
        <Window Type="TaharezLook/FrameWindow" Name="Root/Main" >
            <Property Name="Text" Value="Masteroid! XmachinaX v0.01.101130 - BETA" />
            <Property Name="Visible" Value="False" />
            <Property Name="TitlebarFont" Value="DejaVuSans-10" />
            <Property Name="SizingEnabled" Value="False" />
            <Property Name="TitlebarEnabled" Value="True" />
            <Property Name="UnifiedAreaRect" Value="{{0,0},{0,0},{0.85,0},{0.2,0}}" />
            <Property Name="DragMovingEnabled" Value="False" />
            <Property Name="VerticalAlignment" Value="Centre" />
            <Property Name="CloseButtonEnabled" Value="False" />
            <Property Name="HorizontalAlignment" Value="Centre" />
            <Window Type="TaharezLook/Button" Name="Root/Main/btn_Play" >
                <Property Name="Font" Value="FairChar-30" />
                <Property Name="Text" Value="PLAY" />
                <Property Name="UnifiedAreaRect" Value="{{0.025,0},{0.2,0},{0.325,0},{0.8,0}}" />
                <Event Name="Clicked" Function="btn_clicked_main_play" />
            </Window>
            <Window Type="TaharezLook/Button" Name="Root/Main/btn_Options" >
                <Property Name="Font" Value="FairChar-30" />
                <Property Name="Text" Value="OPTIONS" />
                <Property Name="UnifiedAreaRect" Value="{{0.35,0},{0.2,0},{0.65,0},{0.8,0}}" />
                <Event Name="Clicked" Function="btn_clicked_options" />
            </Window>
            <Window Type="TaharezLook/Button" Name="Root/Main/btn_Quit" >
                <Property Name="Font" Value="FairChar-30" />
                <Property Name="Text" Value="QUIT" />
                <Property Name="UnifiedAreaRect" Value="{{0.675,0},{0.2,0},{0.975,0},{0.8,0}}" />
                <Event Name="Clicked" Function="btn_clicked_quit" />
            </Window>
        </Window>
        <Window Type="TaharezLook/FrameWindow" Name="Root/ErrorMsg" >
            <Property Name="Text" Value="Error" />
            <Property Name="Visible" Value="False" />
            <Property Name="AlwaysOnTop" Value="True" />
            <Property Name="TitlebarFont" Value="DejaVuSans-10" />
            <Property Name="SizingEnabled" Value="False" />
            <Property Name="TitlebarEnabled" Value="True" />
            <Property Name="UnifiedAreaRect" Value="{{0,0},{0,0},{0.3,0},{0.2,0}}" />
            <Property Name="VerticalAlignment" Value="Centre" />
            <Property Name="CloseButtonEnabled" Value="False" />
            <Property Name="HorizontalAlignment" Value="Centre" />
            <Window Type="TaharezLook/StaticText" Name="Root/ErrorMsg/txt_Message" >
                <Property Name="Text" >Sorry, the options feature is not implemented ... yet.</Property>
                <Property Name="UnifiedAreaRect" Value="{{0,0},{0.1,0},{0.85,0},{0.591019,0}}" />
                <Property Name="HorizontalAlignment" Value="Centre" />
            </Window>
            <Window Type="TaharezLook/Button" Name="Root/ErrorMsg/btn_OK" >
                <Property Name="Text" Value="OK" />
                <Property Name="UnifiedAreaRect" Value="{{0,0},{0.7,0},{0.85,0},{0.88941,0}}" />
                <Property Name="HorizontalAlignment" Value="Centre" />
                <Event Name="Clicked" Function="btn_clicked_msgbox_ok" />
            </Window>
        </Window>
    </Window>
 </GUILayout>

The bits of interest here are the <Event ...> tags. Each button has one listed after the <Property ...> tags. The Event tag has two parameters Name and Function. Name is the name of the event. These names can difficult to track down (I imagine a complete list may exist somewhere, but I've yet to find it), but you can find most of them in the documentation API (under "Class->Class Members", click "e" and scroll down till you see things prefaced with Event). In this tutorial, I only used the "Clicked" event.

The second parameter is where you specify the lua function name. So, if you look at the last window definition ("Root/ErrorMsg") you see the OK button will call "btn_clicked_ok" when it is clicked ... and if you look back the lua script, you see this simply hides (ie., closes the ErrorMsg window). Notice that you can use the same function more than once. For example, "Root/Main" and "Root/Pause" each contain a button named "[...]/btn_Options" and they both specify the same lua function.

Closing Thoughts

I hope that this tutorial helps. I know I was wishing for something like this when I implemented this in my game. The content is available, but it's scattered about several wiki pages, sites, and versions of CEGUI. Many thanks to CrazyEddie and the dev team; this is a wonderful, powerful feature. If I've made any mistakes or if you have anything to add to this tutorial, please, feel free to edit then in.