Home - Forums - Documentation - Gallery - Bugs

Introduction

This tutorial is the first part of a long (hopefully interesting) serial. In this serial we create a game step by step with Crystal Space. Every step will have a detailed description, with source code of course. I assume, you’ve read (at least) the first three tutorial in the Crystal Space Manual, when not, please, do it first. I assume it too, you can build a new (working) CS application in your system, and you don’t have issues when running CS apps in general.

Cally in the maze(Cim) will be a small, but real game. It will contents anything, that full games have: GUI, background music, sound effects, story line, debug console, cheat mode, simply rules, load/save system, hall of fame, and installer (on Windows) or install script (on Linux, tested on Suse 10.0). Cally in the maze falls under the LGPL license.

My main system is WindowsXP with MSVC 8 Express.

I use the latest stable version of Crystal Space (currently 1.0RC2).


Step One: Creating a Python console

The simpmap tutorial is a good starting point to begin Cally In The Maze. It has 3d graphics, collision detection, map loading,so ideal for us. Here is the code (I changed the class name only):

//cim.h

ifndef __CIM_H__
#define __CIM_H__

#include <crystalspace.h>

class cimGame : public csApplicationFramework, public csBaseEventHandler
{
private:
	
  bool Setup ();

  /// A pointer to the 3D engine.
  csRef<iEngine> engine;

  /// A pointer to the map loader plugin.
  csRef<iLoader> loader;

  ///A pointer to the 2d renderer plugin.
  csRef<iGraphics2D> g2d;

  /// A pointer to the 3D renderer plugin.
  csRef<iGraphics3D> g3d;

  /// A pointer to the keyboard driver.
  csRef<iKeyboardDriver> kbd;

  /// A pointer to the virtual clock.
  csRef<iVirtualClock> vc;

  /// A pointer to the collision detection system.
  csRef<iCollideSystem> cdsys;

  /// A pointer to the view which contains the camera.
  csRef<iView> view;

  /// A pointer to the sector the camera will be in.
  iSector* room;

  /// Our collider used for gravity and CD.
  csColliderActor collider_actor;

  /**
   * Handle keyboard events - ie key presses and releases.
   * This routine is called from the event handler in response to a 
   * csevKeyboard event.
   */
  bool OnKeyboard (iEvent&);

  /**
   * Setup everything that needs to be rendered on screen. This routine
   * is called from the event handler in response to a csevProcess
   * broadcast message.
   */
  void ProcessFrame ();
    
  /**
   * Finally render the screen. This routine is called from the event
   * handler in response to a csevFinalProcess broadcast message.
   */
  void FinishFrame ();
    
  /// Here we will load our world from a map file.
  bool LoadMap ();

public:

  /// Construct our game. This will just set the application ID for now.
  cimGame ();

  /// Destructor.
  ~cimGame ();

  /// Final cleanup.
  void OnExit ();

  /**
   * Main initialization routine. This routine will set up some basic stuff
   * (like load all needed plugins, setup the event handler, ...).
   * In case of failure this routine will return false. You can assume
   * that the error message has been reported to the user.
   */
  bool OnInitialize (int argc, char* argv[]);

  /**
   * Run the application.
   * First, there are some more initialization (everything that is needed 
   * by cimGame1 to use Crystal Space), then this routine fires up the main
   * event loop. This is where everything starts. This loop will  basically
   * start firing events which actually causes Crystal Space to function.
   * Only when the program exits this function will return.
   */
  bool Application ();

  CS_EVENTHANDLER_NAMES ("crystalspace.cim")
  CS_EVENTHANDLER_NIL_CONSTRAINTS
};

#endif

And here is the .cpp file:



/*
    cim.cpp
*/

#include "cim.h"

CS_IMPLEMENT_APPLICATION

//-----------------------------------------------------------------------------

cimGame::cimGame ()
{
  SetApplicationName ("CrystalSpace.cim");
}

cimGame::~cimGame ()
{
}

bool cimGame::Setup ()
{
  // Now get the pointer to various modules we need. We fetch them
  // from the object registry. The RequestPlugins() call we did earlier
  // registered all loaded plugins with the object registry.
  // The virtual clock.
  g3d = CS_QUERY_REGISTRY (GetObjectRegistry(), iGraphics3D);
  if (!g3d) return ReportError ("Failed to locate 3D renderer!");

  g2d = CS_QUERY_REGISTRY (GetObjectRegistry(), iGraphics2D);
  if (!g3d) return ReportError ("Failed to locate 2D renderer!");

  engine = CS_QUERY_REGISTRY (GetObjectRegistry(), iEngine);
  if (!engine) return ReportError ("Failed to locate 3D engine!");

  vc = CS_QUERY_REGISTRY (GetObjectRegistry(), iVirtualClock);
  if (!vc) return ReportError ("Failed to locate Virtual Clock!");

  kbd = CS_QUERY_REGISTRY (GetObjectRegistry(), iKeyboardDriver);
  if (!kbd) return ReportError ("Failed to locate Keyboard Driver!");

  loader = CS_QUERY_REGISTRY (GetObjectRegistry(), iLoader);
  if (!loader) return ReportError ("Failed to locate Loader!");

  cdsys = CS_QUERY_REGISTRY (GetObjectRegistry(), iCollideSystem);
  if (!cdsys) return ReportError ("Failed to locate CD system!");

  // We need a View to the virtual world.
  view.AttachNew(new csView (engine, g3d));
  iGraphics2D* g2d = g3d->GetDriver2D ();
  // We use the full window to draw the world.
  view->SetRectangle (0, 0, g2d->GetWidth (), g2d->GetHeight ());

  // Here we load our world from a map file.
  if (!LoadMap ()) return false;

  // Initialize collision objects for all loaded objects.
  csColliderHelper::InitializeCollisionWrappers (cdsys, engine);

  // Let the engine prepare all lightmaps for use and also free all images 
  // that were loaded for the texture manager.
  engine->Prepare ();

  // Find the starting position in this level.
  csVector3 pos (0);
  if (engine->GetCameraPositions ()->GetCount () > 0)
  {
    // There is a valid starting position defined in the level file.
    iCameraPosition* campos = engine->GetCameraPositions ()->Get (0);
    room = engine->GetSectors ()->FindByName (campos->GetSector ());
    pos = campos->GetPosition ();
  }
  else
  {
    // We didn't find a valid starting position. So we default
    // to going to room called 'room' at position (0,0,0).
    room = engine->GetSectors ()->FindByName ("room");
    pos = csVector3 (0, 0, 0);
  }
  if (!room)
    ReportError("Can't find a valid starting position!");

  // Now we need to position the camera in our world.
  view->GetCamera ()->SetSector (room);
  view->GetCamera ()->GetTransform ().SetOrigin (pos);

  // Initialize our collider actor.
  collider_actor.SetCollideSystem (cdsys);
  collider_actor.SetEngine (engine);
  csVector3 legs (.2f, .3f, .2f);
  csVector3 body (.2f, 1.2f, .2f);
  csVector3 shift (0, -1, 0);
  collider_actor.InitializeColliders (view->GetCamera (),
  	legs, body, shift);

  return true;
}
void cimGame::ProcessFrame ()
{
  // First get elapsed time from the virtual clock.
  csTicks elapsed_time = vc->GetElapsedTicks ();

  csVector3 obj_move (0);
  csVector3 obj_rotate (0);

  if (kbd->GetKeyState (CSKEY_SHIFT))
  {
    // If the user is holding down shift, the arrow keys will cause
    // the camera to strafe up, down, left or right from it's
    // current position.
    if (kbd->GetKeyState (CSKEY_RIGHT))
      obj_move = CS_VEC_RIGHT * 3.0f;
    if (kbd->GetKeyState (CSKEY_LEFT))
      obj_move = CS_VEC_LEFT * 3.0f;
    if (kbd->GetKeyState (CSKEY_UP))
      obj_move = CS_VEC_UP * 3.0f;
    if (kbd->GetKeyState (CSKEY_DOWN))
      obj_move = CS_VEC_DOWN * 3.0f;
  }
  else
  {
    // left and right cause the camera to rotate on the global Y
    // axis; page up and page down cause the camera to rotate on the
    // _camera's_ X axis (more on this in a second) and up and down
    // arrows cause the camera to go forwards and backwards.
    if (kbd->GetKeyState (CSKEY_RIGHT))
      obj_rotate.Set (0, 1, 0);
    if (kbd->GetKeyState (CSKEY_LEFT))
      obj_rotate.Set (0, -1, 0);
    if (kbd->GetKeyState (CSKEY_PGUP))
      obj_rotate.Set (1, 0, 0);
    if (kbd->GetKeyState (CSKEY_PGDN))
      obj_rotate.Set (-1, 0, 0);
    if (kbd->GetKeyState (CSKEY_UP))
      obj_move = CS_VEC_FORWARD * 3.0f;
    if (kbd->GetKeyState (CSKEY_DOWN))
      obj_move = CS_VEC_BACKWARD * 3.0f;
  }

  collider_actor.Move (float (elapsed_time) / 1000.0f, 1.0f,
    	obj_move, obj_rotate);

  // Tell 3D driver we're going to display 3D things.
  if (!g3d->BeginDraw (engine->GetBeginDrawFlags () | CSDRAW_3DGRAPHICS))
    return;

  // Tell the camera to render into the frame buffer.
  view->Draw ();
}

void cimGame::FinishFrame ()
{
  // Just tell the 3D renderer that everything has been rendered.
	
	g3d->FinishDraw ();
	g3d->Print (0);

}

bool cimGame::OnKeyboard(iEvent& ev)
{
	
  // We got a keyboard event.
  csKeyEventType eventtype = csKeyEventHelper::GetEventType(&ev);
  if (eventtype == csKeyEventTypeDown)
  {
    // The user pressed a key (as opposed to releasing it).
    utf32_char code = csKeyEventHelper::GetCookedCode(&ev);
	

    if (code == CSKEY_ESC)
    {
      // The user pressed escape to exit the application.
      // The proper way to quit a Crystal Space application
      // is by broadcasting a csevQuit event. That will cause the
      // main runloop to stop. To do that we get the event queue from
      // the object registry and then post the event.
      csRef<iEventQueue> q = 
        CS_QUERY_REGISTRY(GetObjectRegistry(), iEventQueue);
      if (q.IsValid()) q->GetEventOutlet()->Broadcast(csevQuit(GetObjectRegistry()));
    }	 	
  }
  return false;
}

bool cimGame::OnInitialize(int /*argc*/, char* /*argv*/ [])
{
  // RequestPlugins() will load all plugins we specify. In addition
  // it will also check if there are plugins that need to be loaded
  // from the config system (both the application config and CS or
  // global configs). In addition it also supports specifying plugins
  // on the commandline.
  iObjectRegistry *object_reg;
  if (!csInitializer::RequestPlugins(object_reg = GetObjectRegistry(),
    CS_REQUEST_VFS,
    CS_REQUEST_OPENGL3D,
    CS_REQUEST_ENGINE,
    CS_REQUEST_FONTSERVER,
    CS_REQUEST_IMAGELOADER,
    CS_REQUEST_LEVELLOADER,
    CS_REQUEST_REPORTER,
    CS_REQUEST_REPORTERLISTENER,
    CS_REQUEST_PLUGIN("crystalspace.collisiondetection.opcode",
		iCollideSystem),
    CS_REQUEST_END))
    return ReportError("Failed to initialize plugins!");

  csBaseEventHandler::Initialize(GetObjectRegistry());

  // Now we need to setup an event handler for our application.
  // Crystal Space is fully event-driven. Everything (except for this
  // initialization) happens in an event.
  if (!RegisterQueue(GetObjectRegistry(), csevAllEvents(GetObjectRegistry())))
    return ReportError("Failed to set up event handler!");
  return true;
}

void cimGame::OnExit()
{
}

bool cimGame::Application()
{
  // Open the main system. This will open all the previously loaded plug-ins.
  // i.e. all windows will be opened.
  if (!OpenApplication (GetObjectRegistry()))
    return ReportError ("Error opening system!");

  if (!Setup())
    return false;

  // This calls the default runloop. This will basically just keep
  // broadcasting process events to keep the game going.
  Run ();

  return true;
}

bool cimGame::LoadMap ()
{
  // Set VFS current directory to the level we want to load.
  csRef<iVFS> VFS (CS_QUERY_REGISTRY (GetObjectRegistry (), iVFS));
  VFS->ChDir ("/lev/flarge");
  // Load the level file which is called 'world'.
  if (!loader->LoadMapFile ("world"))
    ReportError("Error couldn't load level!");

  return true;
}

/*---------------------------------------------------------------------*
 * Main function
 *---------------------------------------------------------------------*/
int main (int argc, char* argv[])
{
  /* Runs the application. 
   *
   * csApplicationRunner<> is a small wrapper to support "restartable" 
   * applications (ie where CS needs to be completely shut down and loaded 
   * again). cimGame does not use that functionality itself, however, it
   * allows you to later use "cimGame.Restart();" and it'll just work.
   */
  return csApplicationRunner<cimGame>::Run (argc, argv);
}

Nothing new. We load and initialize the CS environment, load the necessary plugins, load a map, and create a collider actor and a camera. The user can move in the world and can jump.

A wise programmer always makes a debug console for his app. Just look at walktest , you can watch, test, debug etc. your maps , or just play around, using console commands. We will use heavily the console later, let’s go to create it.

The console in CS has three parts, a console input ( it ensures that you can type into the console) , a console output (it ensures that you can show the console’s content), and a so called execution callback (when you press enter, this callback will handle your commands).

The CS SDK has one console input plugin , and two console output plugin (standard and fancy, we will use the standard, but you can use the fancy version , if you want ). This execution callback struct is only a pure interface (no standard implementation of course, almost every application needs different execution callback), so we need implement this iConsoleExecCallback SCF interface. (There is not a big deal.)

We encapsulate the three class in one class, called cimConsole. The console class provides all neccessary services, to toggle, show and hide, intialize, etc, the console.. The console has an Initialize function, this function loads the needed plugins. The console class is easy to use ( I hope):

class cimConsole
{
private:
      
	
	iObjectRegistry* object_reg; 

	bool visible;

public:
csRef<iConsoleInput> conin;

	csRef<iConsoleOutput> conout;

	~cimConsole();

	bool Initialize(iObjectRegistry* obj_reg);

	void Show();

	void Hide();

	void Toggle();

	bool HandleEvent (iEvent& ev);

	bool IsVisible();
}

We need a script interpreter now, the console is useless without this. We don’t write one (like in walktest), We will use Python. When Python is enough for the guys by NASA or Google, it will be enough for Cally in the maze too. You can download it from here:

Python homepage


I use Python 2.3.5., but you can use newer Python versions too. Please build the cspython at least (and optionally _cspace.pyd) when this modules are not compiled yet..

Note for MSVC users:

When you use MSVC, like MSVC8 Express, you can see, the official wkstypical solution don’t have project for this. Don’t worry, just follow my MSVC and Python tutorial, (it’s not done yet). Other option: you can download the dll here:

cspython.dll

This dll requires CS1.0RC1, Python2.3.5 and works with MSVC8 Express only.

Ok, Python is ready to work, let’s go to create an execution callback!



struct ExecCallback : public scfImplementation1<ExecCallback,iConsoleExecCallback>
  {
    ExecCallback () : scfImplementationType (this) { }
    virtual ~ExecCallback () { }
    virtual void Execute (const char* cmd);
	csRef<iScript> python;
	bool InitPython(iObjectRegistry *obj_reg);
  };


We implement the iConsoleExecCallback struct. This struct has one member function only, the Execute function. We add an InitPython function, and a reference for the python module.

We hide this struct inside the main Console class, (the app’s other elements don’t need to reach this). The console class will be something like this:

| Article | Discussion | View source | History |