flohofwoe.net

Source Code

Here’s some highlevel source code which is useful to understand what’s going on in the demos.

Every demo is its own “executable” embedded as minified Javascript code into a “shell” HTML page, this is what comes out of the emscripten linker stage (you can also emit a pure JS script of course).

I’ve split the demos into 3 parts:

  1. A main.cc file, this simply instantiates an application object, and starts the main loop.
  2. A generical application class called EMSCTestApplication which derived from N3’s new PhasedApplication class (also shown). EMSCTestApplication simply holds all the code which is identical for all demos, and some useful utility methods.
  3. An application class for each demo which is derived from EMSCTestApplication, these contain the “special parts”.

The main.cc file

Here’s the main file of the Dragons demo. This basically contains the main() function, this looks a bit different from a typical hello-world: instead of a main() function the entry point of a Nebula3 app is the NebulaMain() function, and an ImplementNebulaApplication() macro.

The reason for this is that not all target platforms have a standard main() function (e.g. Windows apps have WinMain()), or NebulaMain might not correspond at all to main(). For instance on OSX, Android and NaCl, NebulaMain is actually executed in a thread which has been spawned by the application main thread.

//------------------------------------------------------------------------------
//  dragons.cc
//  (C) 2012 A.Weissflog
//------------------------------------------------------------------------------
#include "stdneb.h"
#include "dragonsapplication.h"

ImplementNebulaApplication();

void
NebulaMain(const Util::CommandLineArgs& args)
{
    App::DragonsApplication app;
    app.SetCompanyName("Bigpoint GmbH");
    app.SetAppTitle("N3 Dragons Test App");
    app.SetAppID("N3DA");
    app.SetAppVersion("1.0");
    app.SetCmdLineArgs(args);
    Util::String rootUrl;
#if __OSX__
    rootUrl = "httpnz://0.0.0.0:8000/cdndata";
#else
    rootUrl = "httpnz://localhost:8000/cdndata";
#endif
    rootUrl = args.GetString("-rooturl", rootUrl);
    app.SetOverrideRootDirectory(rootUrl);
    app.StartMainLoop();
}

emsctestapplication.cc

This is the generic super-class of the Nebula3/emscripten demos. The EMSCTestApplication class is derived from Nebula3’s PhasedApplication base class, which implements a “phased application model”. An application goes through the phases Initial, SetupPreloadQueue, Preloading, Opening, Running, Closing and Quit. These phases decide themselves when they are ticked forward to the next phase.

//------------------------------------------------------------------------------
//  emsctestapplication.cc
//  (C) 2012 A.Weissflog
//------------------------------------------------------------------------------
#include "stdneb.h"
#include "emsctestapplication.h"
#include "input/mouse.h"
#include "input/keyboard.h"
#include "input/touchpad.h"
#include "graphics2/graphicsfacade.h"
#include "graphics2/graphicsstage.h"
#include "graphics2/graphics2protocol.h"

#if __NEBULA3_HTTP__
#include "core/debug/corepagehandler.h"
#include "core/debug/stringatompagehandler.h"
#include "threading/debug/threadpagehandler.h"
#include "memory/debug/memorypagehandler.h"
#include "io/debug/consolepagehandler.h"
#include "io/debug/iopagehandler.h"
#endif

namespace App
{
using namespace Util;
using namespace Graphics2;
using namespace Input;
using namespace Timing;
using namespace Math;
using namespace Http;

//------------------------------------------------------------------------------
/**
 */
EMSCTestApplication::EMSCTestApplication()
{
    // empty
}

//------------------------------------------------------------------------------
/**
    Define files which must be preloaded before OnOpening() can be called.
 */
void
EMSCTestApplication::OnSetupPreloadQueue()
{
    n_printf("> OnSetupPreloadQueue()\n");

    this->AddPreloadFile("export:tables/gfxskintags.bin");
    this->AddPreloadFile(NEBULA3_PLACEHOLDER_MESH);
    this->AddPreloadFile(NEBULA3_PLACEHOLDER_COLORTEXTURE);
    this->AddPreloadFile(NEBULA3_PLACEHOLDER_BUMPTEXTURE);
    this->AddPreloadFile(NEBULA3_PLACEHOLDER_SPECTEXTURE);
    this->AddPreloadFile("mdl:system/placeholder.n3");
    PhasedApplication::OnSetupPreloadQueue();
}

//------------------------------------------------------------------------------
/**
    Setup the application. The HTTP stuff is only initialized when 
    running as native desktop application and creates a simple debug
    HTTP server. If the Opening phase takes to long, it can decide
    to spread its work across several frames.
 */
void
EMSCTestApplication::OnOpening()
{
    n_printf("> OnOpening()\n");

    // setup central time sources
    this->centralMasterTime = CentralMasterTime::Create();
    this->centralMasterTime->Setup();
    this->centralTime = CentralTime::Create();
    this->centralTime->Setup();
    
    #if __NEBULA3_HTTP__
    this->httpInterface = HttpInterface::Create();
    this->httpInterface->Open();
    this->httpServerProxy = HttpServerProxy::Create();
    this->httpServerProxy->Open();
    this->httpServerProxy->AttachRequestHandler(Debug::CorePageHandler::Create());
    this->httpServerProxy->AttachRequestHandler(Debug::StringAtomPageHandler::Create());
    this->httpServerProxy->AttachRequestHandler(Debug::ThreadPageHandler::Create());
    this->httpServerProxy->AttachRequestHandler(Debug::MemoryPageHandler::Create());
    this->httpServerProxy->AttachRequestHandler(Debug::IoPageHandler::Create());
    this->httpServerProxy->AttachRequestHandler(Debug::ConsolePageHandler::Create());
    #endif

    // setup the graphics system
    this->graphicsFacade = GraphicsFacade::Create();
    Graphics2::DisplaySetup displaySetup;
    #if __IOS__
        displaySetup.SetWidth(this->args.GetInt("-w", 1024));
        displaySetup.SetHeight(this->args.GetInt("-h", 768));
    #else
        displaySetup.SetWidth(this->args.GetInt("-w", 800));
        displaySetup.SetHeight(this->args.GetInt("-h", 452));
    #endif
    displaySetup.SetWindowTitle("CoreGraphics2 Test");
    this->graphicsFacade->Setup(displaySetup, false);
    this->graphicsFacade->SetStatisticsDisplayEnabled(true);

    // setup input server
    this->inputServer = InputServer::Create();
    this->inputServer->Open();

    #if NEBULA3_USE_CONSOLE
        this->gameConsoleFacade = GameConsole::GameConsoleFacade::Create();
        this->gameConsoleFacade->Setup();
    #endif

    // reset the camera
    this->ResetCamera();

    // setup the scene (provided by subclasses)
    this->SetupLightEntities();
    this->SetupModelEntities();

    // call parent class (ticks phase forward to Running
    PhasedApplication::OnOpening();
}

//------------------------------------------------------------------------------
/**
 */
void
EMSCTestApplication::OnClosing()
{
#if NEBULA3_USE_CONSOLE
    this->gameConsoleFacade->Discard();
    this->gameConsoleFacade = 0;
#endif

    this->ClearScene();
    this->inputServer->Close();
    this->inputServer = 0;
    this->graphicsFacade->GetDefaultStage()->RemoveAllEntities();
    this->graphicsFacade->Discard();
    
#if __NEBULA3_HTTP__
    this->httpServerProxy->Close();
    this->httpServerProxy = 0;
    this->httpInterface->Close();
    this->httpInterface = 0;
#endif
    
    this->centralTime->Discard();
    this->centralTime = 0;
    this->centralMasterTime->Discard();
    this->centralMasterTime = 0;
    
    PhasedApplication::OnClosing();
}

//------------------------------------------------------------------------------
/**
 */
void
EMSCTestApplication::ResetCamera()
{
    this->mayaCameraUtil.Reset();
    this->graphicsFacade->GetDefaultCamera()->Transform()->SetTransform(this->mayaCameraUtil.GetCameraTransform());
}

//------------------------------------------------------------------------------
/**
 */
void
EMSCTestApplication::SetupModelEntities()
{
    
}

//------------------------------------------------------------------------------
/**
 */
void
EMSCTestApplication::SetupLightEntities()
{
    // override in subclass
}

//------------------------------------------------------------------------------
/**
 */
void
EMSCTestApplication::ClearScene()
{
    // override in subclass
}

//------------------------------------------------------------------------------
/**
 */
void
EMSCTestApplication::UpdateSystem()
{
    // update time
    this->centralMasterTime->Advance();
    this->centralTime->Advance();

    // handle input
    this->HandleInput();
    
    #if NEBULA3_USE_CONSOLE
        this->gameConsoleFacade->Update();
    #endif

    #if __NEBULA3_HTTP__
        this->httpServerProxy->HandlePendingRequests();
    #endif
}

//------------------------------------------------------------------------------
/**
 */
void
EMSCTestApplication::HandleInput()
{
    InputServer* inputServer = InputServer::Instance();
    const Ptr<Keyboard>& keyboard = inputServer->GetDefaultKeyboard();
    const Ptr<Mouse>& mouse = inputServer->GetDefaultMouse();
    const Ptr<TouchPad>& touchPad = inputServer->GetDefaultTouchPad();
    
    if (touchPad.isvalid())
    {
        this->mayaCameraUtil.SetZoomButton(false);
        this->mayaCameraUtil.SetOrbitButton(false);
        if (touchPad->Pinching())
        {
            this->mayaCameraUtil.SetZoomButton(true);
            this->mayaCameraUtil.SetMouseMovement(touchPad->GetVelocity() * 0.1f);
        }
        if (touchPad->Panning())
        {
            this->mayaCameraUtil.SetOrbitButton(true);
            this->mayaCameraUtil.SetMouseMovement(touchPad->GetVelocity() * float2(0.5f, 0.5f));
        }
    }

    if (mouse.isvalid())
    {
        this->mayaCameraUtil.SetOrbitButton(mouse->ButtonPressed(MouseButton::LeftButton));
        this->mayaCameraUtil.SetPanButton(mouse->ButtonPressed(MouseButton::MiddleButton));
        this->mayaCameraUtil.SetZoomButton(mouse->ButtonPressed(MouseButton::RightButton));
        this->mayaCameraUtil.SetZoomInButton(mouse->WheelForward());
        this->mayaCameraUtil.SetZoomOutButton(mouse->WheelBackward());
        this->mayaCameraUtil.SetMouseMovement(mouse->GetMovement() * 0.5f);
    }
    
    // process keyboard input
    if (keyboard.isvalid())
    {
        float zoomIn = 0.0f;
        float zoomOut = 0.0f;
        float2 panning(0.0f, 0.0f);
        if (keyboard->KeyDown(Key::Space))
        {
            this->mayaCameraUtil.Reset();
        }
        this->mayaCameraUtil.SetZoomIn(zoomIn);
        this->mayaCameraUtil.SetZoomOut(zoomOut);
        
        if (keyboard->KeyDown(Key::M))
        {
            Memory::DumpMemoryStatus();
        }
        
        if (keyboard->KeyDown(Key::F1))
        {
            this->graphicsFacade->ToggleStatisticsDisplay();
        }        
    }

    // update the camera util
    this->mayaCameraUtil.Update();
}

//------------------------------------------------------------------------------
/**
 */
void
EMSCTestApplication::UpdateScene()
{
    // update the camera
    matrix44 cameraTransform = this->mayaCameraUtil.GetCameraTransform();
    this->graphicsFacade->GetDefaultCamera()->Transform()->SetTransform(cameraTransform);
}

//------------------------------------------------------------------------------
/**
 */
bool
EMSCTestApplication::IsQuitRequested()
{
    return this->graphicsFacade->IsQuitRequested();
}

//------------------------------------------------------------------------------
/**
 */
void
EMSCTestApplication::OnRunning()
{
    this->ioInterface->Update();
    this->inputServer->BeginFrame();
    this->inputServer->OnFrame();
    this->UpdateSystem();
    this->UpdateScene();
    this->graphicsFacade->OnFrame();
    this->inputServer->EndFrame();
    Threading::Thread::YieldThread();
    if (this->IsQuitRequested())
    {
        this->UpdateState(Closing);
    }
}

//------------------------------------------------------------------------------
/**
    Utility method for subclasses to create a character model with
    skinlist and animation.
 */
Ptr<GraphicsEntity>
EMSCTestApplication::CreateCharacter(const StringAtom& resId, const StringAtom& skinList, const StringAtom& anim, const float4& pos)
{
    GraphicsFacade* graphicsFacade = GraphicsFacade::Instance();

    matrix44 m = matrix44::identity(); // matrix44::rotationy(n_deg2rad(180.0f));
    vector translate = pos;
    m.translate(translate);

    Ptr<GraphicsEntity> modelEntity = graphicsFacade->CreateCharacterEntity();
    modelEntity->Transform()->SetTransform(m);
    modelEntity->Model()->SetResourceId(resId);
    graphicsFacade->GetDefaultStage()->AttachEntity(modelEntity);

    Ptr<Graphics2::ApplySkinList> applySkinList = Graphics2::ApplySkinList::Create();
    applySkinList->SetSkinList(skinList);
    modelEntity->Character()->PushMessage(applySkinList.cast<CharacterMessage>());
    Ptr<Graphics2::AnimPlayClip> playClip = Graphics2::AnimPlayClip::Create();
    playClip->SetClipName(anim);
    playClip->SetTrackIndex(0);
    playClip->SetLoopCount(0.0f);
    playClip->SetTimeOffset(Timing::Tick(n_rand() * 10000));
    modelEntity->Character()->PushMessage(playClip.cast<CharacterMessage>());

    return modelEntity;
}

//------------------------------------------------------------------------------
/**
    Utility method for subclasses to create a simple graphics object.
 */
Ptr<GraphicsEntity>
EMSCTestApplication::CreateModel(const StringAtom& resId)
{
    GraphicsFacade* graphicsFacade = GraphicsFacade::Instance();
    Ptr<GraphicsEntity> modelEntity = graphicsFacade->CreateModelEntity();
    modelEntity->Model()->SetResourceId(resId);
    graphicsFacade->GetDefaultStage()->AttachEntity(modelEntity);
    return modelEntity;
}

} // namespace App

phasedapplication.cc

This is Nebula3’s PhasedApplication class for single-threaded platforms which cannot “own the game loop”. The basic idea is, that the application is called back by some outer runtime, and must return within a small amount of time (usually 16ms for 60fps apps, or 32ms for 30fps apps).

//------------------------------------------------------------------------------
//  ohasedapplication.cc
//  (C) 2012 A.Weissflog
//------------------------------------------------------------------------------
#include "stdneb.h"
#include "phasedapplication.h"
#if __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#elif __FLASCC__
#include <AS3/AS3.h>
#endif

extern "C" void N3EnterFrame(void)
{
    App::PhasedApplication::OnPhasedFrame();
}

namespace App
{
using namespace Parallel;
using namespace Util;
using namespace IO;

PhasedApplication* PhasedApplication::self = 0;

//------------------------------------------------------------------------------
/**
 */
PhasedApplication::PhasedApplication() :
    state(Initial)
{
    self = this;
}

//------------------------------------------------------------------------------
/**
 */
PhasedApplication::~PhasedApplication()
{
    // empty
}

//------------------------------------------------------------------------------
/**
    This method is called by NebulaMain() to enter the game loop. On some
    platforms this method will never return, on others it will return
    when the game finishes.
 */
void
PhasedApplication::StartMainLoop()
{
    n_assert(0 != self);
    #if __EMSCRIPTEN__
    emscripten_set_main_loop(OnPhasedFrame, 0, 1);
    #elif __FLASCC__
    AS3_GoAsync();
    #else
    while (Terminated != this->state)
    {
        OnPhasedFrame();
    }
    #endif
}

//------------------------------------------------------------------------------
/**
    This is the general on-frame callback which will branch into
    specific on-frame methods based on the current state. 
 */
void
PhasedApplication::OnPhasedFrame()
{
    // branch to handler methods depending on current state
    // the handler methods are responsible to tick the state forward
    n_assert(0 != self);
    switch (self->state)
    {
        case Initial:           self->OnInitial(); break;
        case SetupPreloadQueue: self->OnSetupPreloadQueue(); break;
        case Preloading:        self->OnPreloading(); break;
        case Opening:           self->OnOpening(); break;
        case Running:           self->OnRunning(); break;
        case Closing:           self->OnClosing(); break;
        case Quit:              self->OnQuit(); break;
        default:                break;
    }
}

//------------------------------------------------------------------------------
/**
 */
void
PhasedApplication::UpdateState(State newState)
{
    this->state = newState;
}

//------------------------------------------------------------------------------
/**
    Called by subclasses to add a file to the preload queue, and start 
    loading/downloading the file.
 */
void
PhasedApplication::AddPreloadFile(const String& path)
{
    n_dbgout("PhasedApplication::AddPreloadFile(%s)\n", path.AsCharPtr());

    Ptr<IO::ReadStream> ioRequest = IO::ReadStream::Create();
    ioRequest->SetURI(path);
    this->ioInterface->Send(ioRequest);
    this->preloadQueue.Append(ioRequest);
}

//------------------------------------------------------------------------------
/**
    Setup the most minimal Nebula3 runtime and tick state forward
    to SetupPreloadQueue.
 */
void
PhasedApplication::OnInitial()
{
    n_dbgout("--- PhasedApplication::OnInitial()\n");

    StringAtom rootDir = "home:";
    if (this->overrideRootDirectory.IsValid())
    {
        rootDir = this->overrideRootDirectory;
    }

    // initialize core subsystem
    this->coreServer = Core::CoreServer::Create();
    this->coreServer->SetCompanyName(this->companyName);
    this->coreServer->SetAppTitle(this->appTitle);
    this->coreServer->SetAppID(this->appID);
    this->coreServer->SetRootDirectory(rootDir);
    this->coreServer->SetRootKey(this->rootKey);
    this->coreServer->Open();
    
    // setup parallel subsystem
    this->parallelFacade = ParallelFacade::Create();
    this->parallelFacade->Setup();

    #if __NEBULA3_HTTP_FILESYSTEM__
    // setup http subsystem
    this->httpClientRegistry = Http::HttpClientRegistry::Create();
    this->httpClientRegistry->Setup();
    #endif

    // initialize io subsystem
    this->ioServer = IoServer::Create();
    this->ioInterface = IoInterface::Create();
    this->ioInterface->Open();

    // advance to next state
    this->UpdateState(SetupPreloadQueue);
}

//------------------------------------------------------------------------------
/**
    This method is overriden in subclasses to populate the file preload-queue.
    Subclasses should then call their parent class to tick the state 
    to Preloading.
 */
void
PhasedApplication::OnSetupPreloadQueue()
{
    n_dbgout("--- PhasedApplication::OnSetupPreloadQueue()\n");
    this->UpdateState(Preloading);
}

//------------------------------------------------------------------------------
/**
    This method is called each frame during the Preload phase. It checks 
    whether all files in the preloadQueue are downloaded (or failed to 
    download), and then tick the current state forward to Opening.
 */
void
PhasedApplication::OnPreloading()
{
    // need to trigger IO interface for async io to work
    this->ioInterface->Update();

    // handle pending preloads
    IoMemoryCache* memCache = IoMemoryCache::Instance();
    bool allDone = true;
    IndexT i;
    for (i = this->preloadQueue.Size() - 1; i >= 0; --i)
    {
        const Ptr<IO::ReadStream>& curMsg = this->preloadQueue[i];
        if (curMsg->Handled())
        {
            // add to IoMemoryCache
            if (curMsg->GetResult())
            {
                memCache->AddEntry(curMsg->GetURI().LocalPath(), curMsg->GetStream());
            }
            else
            {
                n_warning("PhasedApplication::OnPreloading(): failed to preload file '%s'\n", curMsg->GetURI().AsString().AsCharPtr());
            }
            this->preloadQueue.EraseIndex(i);
        }
        else
        {
            allDone = false;
            break;
        }
    }
    if (allDone)
    {
        this->UpdateState(Opening);
    }
}

//------------------------------------------------------------------------------
/**
    Called during the Opening phase. Subclasses do initialization work 
    here which depends on preloaded files. After all opening work is
    done, the subclass should either call the parent class to tick
    the state forward, or call UpdateState(Running) themselves.
 */
void
PhasedApplication::OnOpening()
{
    n_dbgout("--- PhasedApplication::OnOpening()\n");
    this->UpdateState(Running);
}

//------------------------------------------------------------------------------
/**
    This is the actual on-frame callback for the "game loop". Subclasses
    should *not* call the parent class method, since this would tick the
    app state forward to OnClosing (I think this a bit of a design fault...).
 */
void
PhasedApplication::OnRunning()
{
    this->ioInterface->Update();
    this->UpdateState(Closing);
}

//------------------------------------------------------------------------------
/**
    Called during the Closing phase, when the app should shutdown. Should
    be overriden by the subclass to perform cleanup work. Similar
    to the Opening phase, if the method takes too long it should spread
    its work across several frames. Calling the parent class method will
    then switch the app to Quit.
 */
void
PhasedApplication::OnClosing()
{
    n_dbgout("--- PhasedApplication::OnClosing()\n");
    this->UpdateState(Quit);
}

//------------------------------------------------------------------------------
/**
    This is the counterpart to OnInitial() and should be called
    exactly once. 
 */
void
PhasedApplication::OnQuit()
{
    n_dbgout("--- PhasedApplication::OnQuit()\n");

    // clear the preload queue, this will "unpin" the data from memory
    IndexT i;
    for (i = 0; i < this->preloadQueue.Size(); i++)
    {
        const Ptr<IO::ReadStream>& curMsg = this->preloadQueue[i];
        if (curMsg->GetStream().isvalid() && curMsg->GetStream()->IsOpen())
        {
            curMsg->GetStream()->Close();
        }
    }
    this->preloadQueue.Clear();
    this->ioInterface->Close();
    this->ioInterface = 0;
    this->ioServer = 0;
    #if __NEBULA3_HTTP_FILESYSTEM__
    this->httpClientRegistry->Discard();
    this->httpClientRegistry = 0;
    #endif
    this->parallelFacade->Discard();
    this->parallelFacade = 0;
    this->coreServer = 0;
    this->UpdateState(Terminated);
    #if __EMSCRIPTEN__
    emscripten_cancel_main_loop();
    #endif
    
}

//------------------------------------------------------------------------------
/**
    This is a relict from Nebula3's default Application class which must not be 
    called in a "phased runtime environment".
 */
bool
PhasedApplication::Open()
{
    n_error("PhasedApplication::Open() called!\n");
    return false;
}

//------------------------------------------------------------------------------
/**
    This is a relict from Nebula3's default Application class which must not be
    called in a "phased runtime environment".
 */
void
PhasedApplication::Close()
{
    n_error("PhasedApplication::Close() called!\n");
}

//------------------------------------------------------------------------------
/**
    This is a relict from Nebula3's default Application class which must not be
    called in a "phased runtime environment".
 */
void
PhasedApplication::Exit()
{
    n_error("PhasedApplication::Exit() called!\n");
}

//------------------------------------------------------------------------------
/**
    This is a relict from Nebula3's default Application class which must not be
    called in a "phased runtime environment".
 */
void
PhasedApplication::OnFrame()
{
    n_error("PhasedApplication::OnFrame() called!\n");
}

} // namespace App

dragonsapplication.cc

This is the application class of the Dragons demo, all it does is create light sources, character models, handle input and override the ResetCamera() method with a special version which tries to keep all dragons in view after dragons are added or removed.

//------------------------------------------------------------------------------
//  dragonsapplication.cc
//  (C) 2012 A.Weissflog
//------------------------------------------------------------------------------
#include "stdneb.h"
#include "dragonsapplication.h"
#include "graphics2/graphicsentity.h"
#include "graphics2/graphicsstage.h"
#include "input/keyboard.h"
#include "input/touchpad.h"

namespace App
{
using namespace Math;
using namespace Util;
using namespace Input;
using namespace Graphics2;

//------------------------------------------------------------------------------
/**
 */
DragonsApplication::DragonsApplication() :
    numEntitiesAlongAxis(1)
{
    // empty
}

//------------------------------------------------------------------------------
/**
 */
void
DragonsApplication::ResetCamera()
{
    float distance = (float(this->numEntitiesAlongAxis) * 5.0f) + 5.0f;
    this->mayaCameraUtil.Setup(point(0.0f, 1.0f, 0.0f), point(0.0f, distance * 0.3f, -distance), vector(0.0f, 1.0f, 0.0f));
    this->mayaCameraUtil.Update();
    this->graphicsFacade->GetDefaultCamera()->Transform()->SetTransform(this->mayaCameraUtil.GetCameraTransform());
}

//------------------------------------------------------------------------------
/**
 */
void
DragonsApplication::SetupLightEntities()
{
    matrix44 globalLightTransform = matrix44::rotationx(n_deg2rad(-45.0f));
    globalLightTransform = matrix44::multiply(globalLightTransform, matrix44::rotationy(n_deg2rad(-135.0f)));
    this->globalLight = this->graphicsFacade->CreateGlobalLightEntity();
    this->globalLight->Transform()->SetTransform(globalLightTransform);
    this->globalLight->Lighting()->Light().SetColor(float4(1.0f, 1.0f, 1.0f, 1.0f));
    this->globalLight->Lighting()->Light().SetBackColor(float4(0.2f, 0.2f, 0.2f, 1.0f));
    this->globalLight->Lighting()->Light().SetAmbientColor(float4(0.0f, 0.0f, 0.0f, 1.0f));
    this->globalLight->Lighting()->Light().SetSpecularIntensity(1.0f);
    this->globalLight->Lighting()->Light().SetCastShadows(false);
    this->graphicsFacade->GetDefaultStage()->AttachEntity(this->globalLight);
    
    this->pointLight0 = this->graphicsFacade->CreatePointLightEntity();
    this->pointLight0->Lighting()->SetTransformFromPosDirAndRange(point(-2.0f, 1.0f, 0.0f), vector::upvec(), 7.0f);
    this->pointLight0->Lighting()->Light().SetColor(float4(1.5f, 0.8f, 0.0f, 1.0f));
    this->pointLight0->Lighting()->Light().SetSpecularIntensity(0.0f);
    this->pointLight0->Lighting()->Light().SetCastShadows(false);
    this->graphicsFacade->GetDefaultStage()->AttachEntity(pointLight0);
    
    this->pointLight1 = this->graphicsFacade->CreatePointLightEntity();
    this->pointLight1->Lighting()->SetTransformFromPosDirAndRange(point(+2.0f, 1.0f, 0.0f), vector::upvec(), 7.0f);
    this->pointLight1->Lighting()->Light().SetColor(float4(0.0f, 0.5f, 1.5f, 1.0f));
    this->pointLight1->Lighting()->Light().SetSpecularIntensity(0.0f);
    this->pointLight1->Lighting()->Light().SetCastShadows(false);
    this->graphicsFacade->GetDefaultStage()->AttachEntity(this->pointLight1);
}

//------------------------------------------------------------------------------
/**
 */
void
DragonsApplication::SetupModelEntities()
{
    Array<StringAtom> anims;
    anims.Append("combat_idle_01");
    anims.Append("run_01");
    int min = -(this->numEntitiesAlongAxis - 1) / 2;
    int max = +(this->numEntitiesAlongAxis - 1) / 2;
    
    Ptr<GraphicsEntity> modelEntity;
    int i, x, z;
    for (i = 0, x = min; x <= max; x++)
    {
        for (z = min; z<= max; z++, i++)
        {
            float4 translate(float(x) * 6.0f, 0.0f, float(z) * 6.0f, 0.0f);
            if (this->modelEntities.Size() > i)
            {
                // re-use existing entity
                matrix44 m = matrix44::identity(); // matrix44::rotationy(n_deg2rad(180.0f));
                m.translate(translate);
                this->modelEntities[i]->Transform()->SetTransform(m);
            }
            else
            {
                // need to create a new model entity
                modelEntity = this->CreateCharacter("mdl:characters/dragon_brood.n3",
                                                    "dragon_brood_warrior",
                                                    anims[i % anims.Size()],
                                                    translate);
                this->modelEntities.Append(modelEntity);
            }
        }
    }
    
    // discard surplus entities
    while (this->modelEntities.Size() > i)
    {
        this->graphicsFacade->GetDefaultStage()->RemoveEntity(this->modelEntities.Back());
        this->graphicsFacade->DiscardEntity(this->modelEntities.Back());
        this->modelEntities.EraseIndex(this->modelEntities.Size() - 1);
    }
    
    n_printf("%d dragon(s)\n", this->modelEntities.Size());
    n_printf("--- Version 3 ---\n");
    n_printf("Up Key: More Dragons\n");
    n_printf("Down Key: Less Dragons\n");
    n_printf("Mouse Buttons + Drag: Move Camera\n");
}

//------------------------------------------------------------------------------
/**
 */
void
DragonsApplication::ClearScene()
{
    // we just need to clear our smart pointers, the rest will be
    // taken care of when the graphics system shuts down
    this->globalLight = 0;
    this->pointLight0 = 0;
    this->pointLight1 = 0;
    this->modelEntities.Clear();
}

//------------------------------------------------------------------------------
/**
 */
void
DragonsApplication::HandleInput()
{
    EMSCTestApplication::HandleInput();
    
    InputServer* inputServer = InputServer::Instance();
    bool moreDragons = false;
    bool lessDragons = false;
    
    // for platforms with keyboard support:
    const Ptr<Keyboard>& keyboard = inputServer->GetDefaultKeyboard();
    if (keyboard.isvalid())
    {
        if (keyboard->KeyDown(Key::Up))   moreDragons = true;
        if (keyboard->KeyDown(Key::Down)) lessDragons = true;
    }
    
    // for platforms with touch input support:
    const Ptr<TouchPad>& touchPad = inputServer->GetDefaultTouchPad();
    if (touchPad.isvalid())
    {
        if (touchPad->Tapped())
        {
            if (touchPad->GetPosition().x() > 0.5f) moreDragons = true;
            else                                    lessDragons = true;
        }
    }
    
    if (moreDragons)
    {
        this->numEntitiesAlongAxis += 2;
        this->SetupModelEntities();
        this->ResetCamera();
    }
    if (lessDragons)
    {
        if (this->numEntitiesAlongAxis > 1)
        {
            this->numEntitiesAlongAxis -= 2;
            this->SetupModelEntities();
            this->ResetCamera();
        }
    }
}
    
} // namespace App