Cel Tutorial

1. About Entities and Property Classes

You can imagine an entity like an abstract word like "animal". This animal will be more defined if you add some properties to it, like skin, organs, bones and so on, along with some attributes like skin or eye color, width and height... This all forms the physics of an animal and is normally enough to define a type like human, cat, dog... This is what the physical layer in CEL manages. Apart from that each animal is different because some attributes are different and minds (or his souls?) are different too. This can be compared with the behaviour layer in cel.

2. Setting up CS

The goal if this tutorial is setting up a simple app that loads a level, places some movable boxes around and adds a player that can move around. First thing to do is setting up CS and loading cel plugins of course. (This is mostly CS related so I won't go in depth here). We'll start with the following "base" CS-Application:

// just include all CS headers
#include <css.h>

CS_IMPLEMENT_APPLICATION

class CelTutorial
{
public:
    CelTutorial();
    ~CelTutorial();

    bool Initialize (int argc, const char* const argv[]);
    void Start();
    void SetupFrame();
    void FinishFrame();
    bool HandleEvent (iEvent& ev);

protected:
    iObjectRegistry* object_reg;
    iGraphics3D* g3d;
    iKeyboardDriver* kbd;
    iVirtualClock* vc;
    iLoader* loader;
    iEngine* engine;
};

// a global pointer for the static event handler
CelTutorial* celtutorial;

bool CelTutorialEventHandler(iEvent& ev)
{
    return celtutorial->HandleEvent(ev);
}

CelTutorial::CelTutorial()
{
    vc = NULL;
    kbd = NULL;
    g3d = NULL;
    object_reg = NULL;
    engine = NULL;
    loader = NULL;
}

CelTutorial::~CelTutorial()
{
    SCF_DEC_REF(vc);
    SCF_DEC_REF(kbd);
    SCF_DEC_REF(g3d);
    SCF_DEC_REF(engine);
    SCF_DEC_REF(loader);
    csInitializer::DestroyApplication(object_reg);
}

bool CelTutorial::Initialize(int argc, const char* const argv[])
{
     object_reg = csInitializer::CreateEnvironment (argc, argv);
     if (!object_reg) return false;

     if (!csInitializer::RequestPlugins (object_reg,
        CS_REQUEST_VFS,
        CS_REQUEST_SOFTWARE3D,
        CS_REQUEST_ENGINE,
        CS_REQUEST_FONTSERVER,
        CS_REQUEST_IMAGELOADER,
        CS_REQUEST_LEVELLOADER,
        CS_REQUEST_REPORTER,
        CS_REQUEST_REPORTERLISTENER,
        CS_REQUEST_END))
     {
         csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
                 "crystalspace.application.celtest",
                 "Can't initialize plugins!");
         return false;
     }

     if (!csInitializer::SetupEventHandler (object_reg, CelTutorialEventHandler))
     {
         csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
                 "crystalspace.application.celtest",
                 "Can't initialize event handler!");
         return false;
     }

     // Check for commandline help.
     if (csCommandLineHelper::CheckHelp (object_reg))
     {
         csCommandLineHelper::Help (object_reg);
         return false;
     }

     // The virtual clock.
     vc = CS_QUERY_REGISTRY (object_reg, iVirtualClock);
     if (!vc)
     {
         csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
                 "crystalspace.application.celtest",
                 "Can't find the virtual clock!");
         return false;
     }

     // Find the pointer to engine plugin
     engine = CS_QUERY_REGISTRY (object_reg, iEngine);
     if (!engine)
     {
         csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
                 "crystalspace.application.celtest",
                 "No iEngine plugin!");
         return false;
     }

     loader = CS_QUERY_REGISTRY (object_reg, iLoader);
     if (!loader)
     {
         csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
                 "crystalspace.application.celtest",
                 "No iLoader plugin!");
         return false;
     }

     g3d = CS_QUERY_REGISTRY (object_reg, iGraphics3D);
     if (!g3d)
     {
         csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
                 "crystalspace.application.celtest",
                 "No iGraphics3D plugin!");
         return false;
     }

     kbd = CS_QUERY_REGISTRY (object_reg, iKeyboardDriver);
     if (!kbd)
     {
         csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
                 "crystalspace.application.celtest",
                 "No iKeyboardDriver plugin!");
         return false;
     }

     // Open the main system. This will open all the previously loaded plug-ins.
     if (!csInitializer::OpenApplication (object_reg))
     {
         csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
                 "crystalspace.application.celtest",
                 "Error opening system!");                                               return false;
     }

     return true;
}

void CelTutorial::Start()
{
    csDefaultRunLoop (object_reg);
}

void CelTutorial::SetupFrame()
{
}

void CelTutorial::FinishFrame()
{
    g3d->FinishDraw ();
    g3d->Print (NULL);
}

bool CelTutorial::HandleEvent(iEvent& ev)
{
    if (ev.Type == csevBroadcast)
    {
        if (ev.Command.Code == cscmdProcess)
        {
            SetupFrame();
            return true;
        }
        else if (ev.Command.Code == cscmdFinalProcess)
        {
            FinishFrame();
            return true;
        }
    }
    else if (ev.Type == csevKeyDown)
    {
        if (ev.Key.Code == CSKEY_ESC)
        {
            iEventQueue* q = CS_QUERY_REGISTRY (object_reg, iEventQueue);
            if (q)
            {
                q->GetEventOutlet()->Broadcast (cscmdQuit);
                q->DecRef();
            }
            return true;
        }
    }

    return false;
}
 

int main(int argc, char* argv[])
{
    celtutorial = new CelTutorial();

    if (celtutorial->Initialize (argc, argv))
        celtutorial->Start();

    delete celtutorial;
    return 0;
}

You can find all this code along with a makefile in part1.zip. Note that you have to copy cs-config into the dir and set CRYSTAL var before typing "make".
 

3. Setting up CEL

Now we have to load the cel plugins. At the moment we need some tweaking of SCF here as CS does only support plugin registering in code or the scf.cfg but not in the config file of an application. So we have to register all cel plugins into SCF at first. This should happen right after CreateEnvironment. We insert this into the CelTutorial::Initialize function:

bool CelTutorial::Initialize(int argc, const char* const argv[])
{
  object_reg = csInitializer::CreateEnvironment (argc, argv);
  if (!object_reg) return false;

  // @@@ The code below is temporary until we have a general solution
  // in CS for having plugins outside the CS hierarchy (by an additional
  // .csinfo file for every plugin for example).
  iSCF::SCF->RegisterClass ("cel.physicallayer", "plimp");
  iSCF::SCF->RegisterClass ("cel.behaviourlayer.test", "bltest");
  iSCF::SCF->RegisterClass ("cel.pcfactory.test", "pftest");
  iSCF::SCF->RegisterClass ("cel.pcfactory.mesh", "pfmesh");
  iSCF::SCF->RegisterClass ("cel.pcfactory.meshselect", "pfmesh");
  iSCF::SCF->RegisterClass ("cel.pcfactory.movable", "pfmove");
  iSCF::SCF->RegisterClass ("cel.pcfactory.solid", "pfmove");
  iSCF::SCF->RegisterClass ("cel.pcfactory.movableconst_cd", "pfmove");
  iSCF::SCF->RegisterClass ("cel.pcfactory.gravity", "pfmove");
  iSCF::SCF->RegisterClass ("cel.pcfactory.timer", "pftools");
  iSCF::SCF->RegisterClass ("cel.pcfactory.tooltip", "pftools");
  iSCF::SCF->RegisterClass ("cel.pcfactory.region", "pfengine");
  iSCF::SCF->RegisterClass ("cel.pcfactory.camera", "pfengine");
  iSCF::SCF->RegisterClass ("cel.pcfactory.inventory", "pfinv");
  iSCF::SCF->RegisterClass ("cel.pcfactory.pckeyinput", "pfinput");
  iSCF::SCF->RegisterClass ("cel.pcfactory.characteristics", "pfinv");

// ...

This registers the physical and behaviour layer plugins along with several property class factories. Note that some plugins implement multiple factories, so we have to mention them multiple times. I'll explain later what all these layer and factories are.
Then we want CS to load the plugins and to register them in the Object Registry. This is done by adding them to the plugin request list in csInitializer::RequestPlugins, we also force CS to load the rapid collision detection plugin. We don't need to have the property class factories in the registry because these register themself in the physical layer while loading. We'll come to that later. Here's the code:

  if (!csInitializer::RequestPlugins (object_reg,
        CS_REQUEST_VFS,
        CS_REQUEST_SOFTWARE3D,
        CS_REQUEST_ENGINE,
        CS_REQUEST_FONTSERVER,
        CS_REQUEST_IMAGELOADER,
        CS_REQUEST_LEVELLOADER,
        CS_REQUEST_REPORTER,
        CS_REQUEST_REPORTERLISTENER,
        CS_REQUEST_PLUGIN ("cel.physicallayer", iCelPlLayer),
        CS_REQUEST_PLUGIN ("cel.behaviourlayer.test", iCelBlLayer),
        CS_REQUEST_PLUGIN ("crystalspace.collisiondetection.rapid",
                iCollideSystem),
        CS_REQUEST_END))
  {
    csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
        "crystalspace.application.celtest",
        "Can't initialize plugins!");
    return false;
  }

// ...

Because we don't need the property classes to be in the registry we load them manually:

// null terminated list
static const char* celtut_propfactlist[] = {
    "cel.pcfactory.test",
    "cel.pcfactory.movable",
    "cel.pcfactory.solid",
    "cel.pcfactory.movableconst_cd",
    "cel.pcfactory.gravity",
    "cel.pcfactory.region",
    "cel.pcfactory.camera",
    "cel.pcfactory.tooltip",
    "cel.pcfactory.timer",
    "cel.pcfactory.inventory",
    "cel.pcfactory.characteristics",
    "cel.pcfactory.mesh",
    "cel.pcfactory.meshselect",
    "cel.pcfactory.pckeyinput",
    0
};

void CelTutorial::LoadPlugin(const char* name)
{
  iPluginManager* plugin_mgr = CS_QUERY_REGISTRY (object_reg, iPluginManager);
  iBase* plug = CS_LOAD_PLUGIN_ALWAYS(plugin_mgr, name);
  plugin_mgr->DecRef();
  if (!plug)
  {
    csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
            "crystalspace.application.celtest",
            "CEL '%s' plugin missing!", name);
    return false;
  }
  plug->DecRef();

  return true;
}

//...

   if (!csInitializer::RequestPlugins (object_reg,
        CS_REQUEST_VFS,
   //...
  {
  }

  // Load Property Class factories
  while (const char** pfname = celtut_propfactlist; *pfname; pfname++)
  {
     if (!LoadPlugin (*pfname)) return false;
  }

As we'll need the Physical Layer and the Behaviour Layer a bit more often we keep references to them:

class CelTutorial
{
   // ...
protected:
   iCelPlLayer* pl;
   iCelBlLayer* bl;
   //...
}

 pl = CS_QUERY_REGISTRY(object_reg, iCelPlLayer);
 if (!pl)
 {
      csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
              "crystalspace.application.celtest",
              "No iPlLayer plugin!");
      return false;
 }
 bl = CS_QUERY_REGISTRY(object_reg, iCelBlLayer);
  if (!bl)
  {
      csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
              "crystalspace.application.celtest",
              "No iBlLayer plugin!");
      return false;
  }

Finally don't forget to DecRef the references in the destructor and to set them to null in the constructor. Your code should finally look like this:
 part2.zip
 

4. Adding first Entities and Property Classes:

Entities are managed by the Physical Layer. That's basically a class that helps you creating entities and attaching property classes to them. So if you want to create an entity you have to call the CreateEntity function from the phsical layer. After that we can also set the name of the entity.
At first we want to create a world for our objects. This world will be a Cel Entity too of course:

iCelEntity* CelTutorial::CreateRoom(const char* mapfile)
{
     iCelEntity* entity;

     entity = pl->CreateEntity();
     if (!entity) return NULL;
     entity->SetName("room");

So we have a completely empty and undefined entity now. We want this entity to contain some static world geometry (a level). For this purpose cel contains the "pcregion" property class. But how do we attach that to the entity?
Well each property class has a property class factory which is a class that is only responsible for creating instances of a property class. These property class factories are in plugins (that's all the pfxxx plugins above). At loadtime these property factories register with the physical layer who has a complete list of all property class factories then. The creation of a property class is luckily quite simple (you don't have to take care about all the fatcory stuff :): Just call CreatePropertyClass (entity, name of property class) :

          iCelPropertyClass* pc;
         pc = pl->CreatePropertyClass (entity, "pcregion");

So we have a property class assigned to our entity now, but we need to set some paramters. For this reason each property class implements 1 (or more) interfaces which can be used to configure them. In our case it's the iPcRegion interface. We can just query a pointer to it with the help of the SCF system. After finishing your settings don't forget to DecRef it!
a region is setup in 3 steps: first we do SetWorldFile(vfspath, filename) where vfspath is the VFS path to a Crystal Space map and name the name of the mapfile. Then you should give the region a name (this is the name used in Crystal Space internally, as a pcregion class basically encapsulates a Region in Crystal Space), this is done with SetRegionName(name). Finally we can call Load() and force the region to load the Crystal Space map into memory with this:

         iPcRegion* pcregion = SCF_QUERY_INTERFACE_FAST (pc, iPcRegion);
         if (!pcregion) return NULL;
         pcregion->SetWorldFile (mapfile, "world");
         pcregion->SetRegionName ("room");
         if (!pcregion->Load()) return NULL;
         pcregion->DecRef();

That's nice so far but still gives a black screen because no camera has been created. A camera is usually attached to the player entity so we creating one:

iCelEntity* CelTutorial::CreateActor()
{
    iCelEntity* entity;
    iCelPropertyClass* pc;

    entity = pl->CreateEntity();
    if (!entity) return NULL;
    entity->SetName("actor");

    pc = pl->CreatePropertyClass (entity, "pccamera");
    if (!pc) return NULL;

So we need to configure our camera now. It's done through the iPcCamera interface.  At first we set the mode with SetMode(cammode, use_cd), cammode is a either follow if you want to follow a mesh object (that only works if the camera has a pcmesh property class assigned to it of course) and freelook (in this case the camera is like a ghost that can fly around everywhere), the second argument specifies if collision detection should be used. In this case we use freelook mode with collision detection.
Finally we have to point the camera into a region. But we need a pointer to the pcregion property class that is in our world entity now... For getting property classes out of entities you can use the CEL_QUERY_PROPCLASS macro. The final code looks like this then:

    iPcCamera* pccamera = SCF_QUERY_INTERFACE_FAST (pc, iPcCamera);
    iPcRegion* pcregion = CEL_QUERY_PROPCLASS
        (world->GetPropertyClassList(), iPcRegion);
    pccamera->SetRegion(pcregion);
    pccamera->SetMode (iPcCamera::freelook, true);
    pccamera->DecRef();
    pcregion->DecRef();

    return entity;
}

Finally we add 2 iCelEntity variable to our class and add another function that we'll call in Initialize:

bool CelTutorial::CreateWorld()
{
    world = CreateRoom("/lev/partsys");
    if (!world) return false;

    actor = CreateActor();
    if (!actor) return false;

    return true;
}

The final code should look like the stuff part3.zip then. After compiling you should be able to move around in the map :) This was a complicated version of loading a map so far but I hope I can show you the power of cel in the next sections.