Tech/LxEngine/Tutorials/Tutorial 1

From Athile

Jump to:navigation, search

Contents

Overview

The objective of this tutorial is to use LxEngine to create a Window that can render with OpenGL. The tutorial will simply clear the screen using the glClear command to demonstrate this.

This is not a very exciting tutorial, but will give a whirlwind tour of the LxEngine architecture and major classes.


Concepts Introduced

  • Engine
  • Document, View, ViewImp
  • lxvar
  • Engine::run()
  • Renderer
  • UIBinding

Prerequisites

  • This tutorial assumes you have downloaded and built the LxEngine SDK successfully and are able to execute the samples
  • This tutorial assumes a very basic familiarity with OpenGL and does not explain how OpenGL functions used in the tutorial work

Results

If all goes well, the application should produce a Blue Screen of Success™.

The Blue Screen of Success™ can be exited by pressing the ESC key.

Tutorial01.png

 

Tutorial

The code for this tutorial is all contained in a single file, main.cpp. The full project can be browsed on github.

Since this is one of the first tutorials, a full listing of the file is included at the bottom of this page in #Appendix B: Full Source Listing. This tutorial itself will be going through the code a bit out of order, so it may be helpful to skim the full source listing first to orient yourself.

(Now would be a good time to click on the full source listing link if you haven't already.)

Headers

Summary: include lx0/lxengine.hpp in order to use LxEngine

Let's look at the first meaningful section of the code: what headers are included.

//===========================================================================//
//   H E A D E R S   &   D E C L A R A T I O N S 
//===========================================================================//
 
#include <lx0/lxengine.hpp>
#include <lx0/views/canvas.hpp>
 
#ifdef _MSC_VER
    #include <windows.h>
#endif
#include <gl/gl.h>


The lx0 includes

All LxEngine headers are located within the lx0 subdirectory to avoid naming collisions and conflicts. All of the "core" or "standard" functionality of LxEngine is included when lx0/lxengine.hpp is included. All LxEngine applications are expected to include lx0/lxengine.hpp

However, any optional or plug-in functionality must be included explicitly via another header. In this case, the plug-in functionality is the "canvas" functionality. Canvas is an optional component included in the standard LxEngine library. It represents a particular type of View, in this case a window that supports OpenGL rendering. Other types of View implementations are possible: such as OGRE-based windows, DirectX-based windows, or custom View implementations. Since not all application will necessarily be using the Canvas implementation, this is an optional component that needs to included explicitly.

Optionally functionality is usually included via a single header included of the #include <lx0/type-of-component/name-of-component>. For example, in this case, a "view" is being added, so the type of component is "views". Another component type is "subsystem", which for major subsystems like a physics plug-in, javascript implementation, or the custom LxEngine OpenGL rasterizer. The remaining component-types are currently "util", for collections of utility functionality rather than neatly encapsulated components, and "prototype", for in-development plug-in behavior that is not yet fully usable.

The windows.h and gl.h includes

The next two includes are not directly related to LxEngine: they simply include the standard OpenGL headers so functions like glClear can be used. Note that on Windows, it's still unfortunately necessary to included windows.h prior to including gl.h - otherwise, there's nothing Windows specific about this tutorial application!

main()

Summary: LxEngine throws exceptions, be ready to catch them on error conditions you wish to handle

Next, let's look at the start of the main function:

int 
main (int argc, char** argv)
{
    int exitCode = -1;
    try
    {

(...code here to be discussed later...)

    }
    catch (std::exception& e)
    {
        lx_fatal("Fatal: unhandled std::exception.\nException: %s\n", e.what());
    }
 
    return exitCode;
}


The notable item is that the entire program is enclosed in try { } catch block. LxEngine does throw exceptions. Exceptions are reserved for conditions that imply an application error has occurred and the application will likely want to close. LxEngine error handling is discussed elsewhere - for now, it's good to know that LxEngine does use exceptions. (FAQ: I don't like exceptions. Why does LxEngine use them?)

The Engine Singleton

Summary: Engine::acquire() is where to start

        lx0::EnginePtr spEngine = lx0::Engine::acquire();

The next important line is the call to Engine::acquire(). LxEngine uses a Singleton object of the Engine class to orchestrate all the uses of LxEngine within that application. Acquiring the singleton for the first time does some minimal initialization, so it's important that this be the first step of the application before any other LxEngine services are used.

It's also worth noting the LxEngine naming convention:

The Ptr suffix is used thorough the LxEngine code base, as reference counted pointers are the standard for all LxEngine objects (FAQ: Aren't std::shared_ptrs<> inefficient?).


Remember that #include of canvas.hpp? We now register the Canvas functionality with the Engine. Basically, we give the plugin a string name (which we'll use later to identify that plug-in) and provide a function for the Engine for creating instances of that plug-in. Notice that the core Engine object does not know anything about plug-in components, therefore addViewPlugin must be called and must pass in the lambda function telling the Engine object how to create a "Canvas" view.

        spEngine->addViewPlugin("Canvas", [] (lx0::View* pView) { return lx0::createCanvasViewImp(); });

The Document & View

Summary: LxEngine is an MVC architecture, you'll need a Document and View to get going

LxEngine use a Model-View-Controller design pattern for the majority of the architecture. In basic terms, all the application 'data' is stored in a generic Document and any sort of view of that data is done via a View object.

        lx0::DocumentPtr spDocument = spEngine->createDocument();
 
        lx0::ViewPtr spView = spDocument->createView("Canvas", "view", new Renderer );


The Engine is used to create the Document. Again, remember the Engine's purpose is to orchestrate everything that goes on in the application, so therefore it wants to know about all the major objects in the system.

Sure, this application doesn't have any data, so there's technically no need for a Document: but since the vast majority of applications will, LxEngine does enforce that a View must have a corresponding parent Document - even if it is an empty Document, as in this case.

Next the Document creates a View. This is done via naming the view plugin to use, assigning that view instance a name and lastly connecting the View to Renderer. The View itself is like a blank canvas that gives the application a chance to draw on it. The Renderer is the user-supplied object that does that actual drawing. The instance name is used so other components can get access to the View or Views on a Document without having to explicitly store a pointer to them (another instance of loose coupling in the LxEngine architecture).

We'll skip over the new Renderer for a moment (it tells the View what to actually draw) in order to finish up looking at the main() function.

A bit more Setup

        spView->addUIBinding( new UIBindingImp );
 
        lx0::lxvar options;
        options.insert("title", "Tutorial 01");
        options.insert("width", 640);
        options.insert("height", 480);
        spView->show(options);

First, we add a UIBinding to the View. What's that? It's a simple class that maps UI events (i.e. key presses, mouse moves, etc.) into 'application' events (e.g. quit, redraw the screen). We'll look into that in a moment. It's fairly straightforward.

Then, we need to show that View we created a moment ago. Since the View has a host of optional parameters, we pass them into the show() method using an lxvar. What's an lxvar? It's basically a lightweight C++ class for storing JSON-like variant data of ints, floats, booleans, arrays, and/or maps. For non-performance sensitive code, it's very flexible and useful.

Engine::run()

Summary: Engine::run() is the platform-independent application message loop

What's the big black box inside the run() method?

        exitCode = spEngine->run();

The run() method essentially wraps up an event loop that handles and dispatches platform messages (e.g. Win32 MSGs), object notifications (e.g. View::update() calls), application events (e.g. the "quit" event).

The run() method will continue until the Engine is sent a "quit" event. That will never happen by default, but fortunately, the UIBindingImp discussed shortly will map the ESC key to a "quit" event so that we're not trapped in an infinite loop.

Engine::shutdown()

Summary: Call Engine::shutdown() before closing the application

In C++, aren't the destructors supposed to do all the clean-up?

Yes, they are. However, due to the use of shared pointers, the Engine class requires a shutdown() call to make sure all major objects in the system have an opportunity to clean-up nicely in the proper order. The explicit shutdown() call also gives the Engine object an opportunity to do some internal memory and consistency checks in a debug environment to help improve the robustness of the application.

        spEngine->shutdown();

Another way to think of the shutdown call: this provides a "root" object from which the clean-up occurs, descending downward to the Document, View, and other major objects in the system. Construction and destruction order of application-wide objects can be a problem in large applications due to dependencies between objects. The shutdown() method provides a more well-defined process for tearing down those objects.

Be sure to call Engine::shutdown before exiting to avoid some warnings and complaints from the application during the Engine destructor.

UIBindingImp

The UIBindingImp class' purpose is quite simple:

Map device events to application events.

class UIBindingImp : public lx0::UIBinding
{
public:
    virtual void updateFrame (lx0::ViewPtr spView, const lx0::KeyboardState& keyboard)
    {
        if (keyboard.bDown[lx0::KC_ESCAPE])
            lx0::Engine::acquire()->sendEvent("quit");
        if (keyboard.bDown[lx0::KC_R])
            spView->sendEvent("redraw");
    }
};

The notion of application events will be discussed in more detail in later tutorials, but for now, the above is hopefully somewhat self explanatory. If the ESCAPE key is down, send a "quit" message to the Engine. If the R key is down, send a "redraw" message to the view. The messages are string-based to allow maximum flexibility in defining custom events (as well as for keeping disparate components only loosely coupled). (FAQ: Aren't string-based events bad/inefficient/not type safe?)

The UIBinding interface provides several virtual functions for responding to various UI / device events. In this case, the code is hooked into the updateFrame() method which is called once per "frame" or iteration of the Engine::run() loop.

Renderer

The Renderer is another simple interface providing virtual methods that get called by the host View so that the View can render something meaningful. In particular, initialize() and render() are of interest.

class Renderer : public lx0::View::Component
{
public:
    virtual void initialize(lx0::ViewPtr spView)
    {
        glClearColor(0.1f, 0.3f, 0.8f, 1.0f);
    }
 
    virtual void render (void)	
    {
        glClear(GL_COLOR_BUFFER_BIT);
    }
};

The Canvas ViewImp does all the basic OpenGL functionality such as the context creation, therefore the render only needs to initialize the data it in particular cares about. The Canvas implementation also handles the buffer swap. (Admittedly, what the Canvas class does and doesn't do needs to be documented somewhere other than simply in the source code itself!)

For developers familiar with OpenGL, what's happening here should be quite straightforward:

  1. The clear color is initialized once, right after the view is setup
  2. Every time the window is redraw, glClear is called to clear the buffer

Additional OpenGL calls can easily be added to the initialize and render methods to provide more intricate drawing than simply clearing the screen.

Conclusion

Even the most basic LxEngine application involves numerous objects:

However, this covers the majority of the entire LxEngine architecture. There are a few other "core" classes such as Element and Controller, but for the most part these are all the core classes that you'll ever need to know to use LxEngine correctly. Individual subsystems and plug-ins introduce variations on these core objects, but for the most part do not introduce any fundamental new concepts.

Yes, this is a fairly long tutorial to display a blank window - but once this is in place, it should be fairly easy to accelerate along to more advanced features and custom behavior.

Appendix A: Feedback, Questions, & Issues

Please provide any feedback, questions, or issues on the forums.

LxEngine is in active development and in need of improvements. Suggestions for making it simpler to use are most welcome!

Appendix B: Full Source Listing

The latest source for this file is located on github here: https://github.com/athile/lxengine/blob/master/lx0/dev/samples/tutorials/tutorial_01/main.cpp


  1. //===========================================================================//
  2. /*
  3.                                    LxEngine
  4.  
  5.     LICENSE
  6.  
  7.     Copyright (c) 2011 athile@athile.net (http://www.athile.net)
  8.  
  9.     Permission is hereby granted, free of charge, to any person obtaining a 
  10.     copy of this software and associated documentation files (the "Software"), 
  11.     to deal in the Software without restriction, including without limitation 
  12.     the rights to use, copy, modify, merge, publish, distribute, sublicense, 
  13.     and/or sell copies of the Software, and to permit persons to whom the 
  14.     Software is furnished to do so, subject to the following conditions:
  15.  
  16.     The above copyright notice and this permission notice shall be included in
  17.     all copies or substantial portions of the Software.
  18.  
  19.     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20.     IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21.     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22.     AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23.     LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
  24.     FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 
  25.     IN THE SOFTWARE.
  26. */
  27. //===========================================================================//
  28.  
  29. //===========================================================================//
  30. //   H E A D E R S   &   D E C L A R A T I O N S 
  31. //===========================================================================//
  32.  
  33. #include <lx0/lxengine.hpp>
  34. #include <lx0/views/canvas.hpp>
  35.  
  36. #ifdef _MSC_VER
  37.     #include <windows.h>
  38. #endif
  39. #include <gl/gl.h>
  40.  
  41. //===========================================================================//
  42. //   U I - B I N D I N G
  43. //===========================================================================//
  44.  
  45. class UIBindingImp : public lx0::UIBinding
  46. {
  47. public:
  48.     virtual void updateFrame (lx0::ViewPtr spView, const lx0::KeyboardState& keyboard)
  49.     {
  50.         if (keyboard.bDown[lx0::KC_ESCAPE])
  51.             lx0::Engine::acquire()->sendEvent("quit");
  52.         if (keyboard.bDown[lx0::KC_R])
  53.             spView->sendEvent("redraw");
  54.     }
  55. };
  56.  
  57. //===========================================================================//
  58. //   R E N D E R E R
  59. //===========================================================================//
  60.  
  61. class Renderer : public lx0::View::Component
  62. {
  63. public:
  64.     virtual void initialize(lx0::ViewPtr spView)
  65.     {
  66.         glClearColor(0.1f, 0.3f, 0.8f, 1.0f);
  67.     }
  68.  
  69.     virtual void render (void)	
  70.     {
  71.         glClear(GL_COLOR_BUFFER_BIT);
  72.     }
  73.  
  74. };
  75.  
  76. //===========================================================================//
  77. //   E N T R Y - P O I N T
  78. //===========================================================================//
  79.  
  80. int 
  81. main (int argc, char** argv)
  82. {
  83.     int exitCode = -1;
  84.     try
  85.     {
  86.         lx0::EnginePtr spEngine = lx0::Engine::acquire();
  87.         spEngine->addViewPlugin("Canvas", [] (lx0::View* pView) { return lx0::createCanvasViewImp(); });
  88.  
  89.         lx0::DocumentPtr spDocument = spEngine->createDocument();
  90.  
  91.         lx0::ViewPtr spView = spDocument->createView("Canvas", "view", new Renderer );
  92.         spView->addUIBinding( new UIBindingImp );
  93.  
  94.         lx0::lxvar options;
  95.         options.insert("title", "Tutorial 01");
  96.         options.insert("width", 640);
  97.         options.insert("height", 480);
  98.         spView->show(options);
  99.  
  100.         exitCode = spEngine->run();
  101.         spEngine->shutdown();
  102.     }
  103.     catch (std::exception& e)
  104.     {
  105.         lx_fatal("Fatal: unhandled std::exception.\nException: %s\n", e.what());
  106.     }
  107.  
  108.     return exitCode;
  109. }
Navigation
Toolbox