@c -*-texinfo-*- @node Tutorial WalkTut, , Tutorials, Tutorials @subsection WalkTut Tutorial @cindex zone manager @cindex entity creation @cindex @code{csApplicationFramework} @cindex @code{csBaseEventHandler} This is a tutorial to show how you can use Crystal Entity Layer in your own applications. The original game data and the Blender level and model source used to create the artwork of this tutorial can be downloaded from: @uref{http://www.crystalspace3d.org/celtutorial.zip} The source files of this tutorial can be found in the @file{apps/tutorial/walktut} directory that is included with @sc{cel}. The world and entity file can be found in the @file{data} directory (@file{walktut_world} and @file{walktut_entities}). Remember that the first time you run this application you have to add the @samp{-relight} commandline option so that the lightmaps are correctly updated. During the game you can use the following inputs: @itemize @bullet @item @samp{arrow keys}: movement. @item @samp{m}: camera mode (first person, third person, @dots{}). @item @samp{left mouse button}: pick up an item and put in inventory. @item @samp{d}: drop the last item picked up. @end itemize This tutorial features: @itemize @bullet @item Camera handling using the @samp{pcdefaultcamera} property class. @item Movement of an actor in a game using the @samp{pclinearmovement} property class. @item Visualization of the player using the @samp{pcmesh} property class. @item Handling of the player inventory using the @samp{pcinventory} property class. @item Handling of keyboard and mouse input using the @samp{pccommandinput} property class. @item Loading world and entity file using the @samp{pczonemanager} property class. @item Usage of the @samp{celentity} addon to add entities from an @sc{xml} map file. @item How to create behaviours in C++ for game logic. @end itemize Here is the tutorial: @menu * Tutorial Main:: main.cpp * Tutorial App Header:: app.h * Tutorial App:: app.cpp * Tutorial Behave Header:: behave.h * Tutorial Behave:: behave.cpp @end menu @subsubheading The XML Entity File Except for the level and player entities all other entities are defined in the @file{entities} file. Here is an example of how such an entity looks: @example @end example In this particular example we use the @samp{cel.addons.celentity} addon (@pxref{Addons CelEntity}) to create an entity with the name @samp{badone}. It has the following property classes: @itemize @bullet @item @samp{pcmesh} (@pxref{PropClass Mesh}). This mesh is setup with the @samp{monkey} model that was made in @samp{Blender}. @item @samp{pcmeshselect} Used for selection of mesh. @item @samp{pcproperties} This is a generic property class for storing named properties. In this particular case the @samp{badone_behave} behaviour will read out the properties to construct the path for the catmull rom spline. @item @samp{pctimer} (@pxref{PropClass Timer}). This timer will be used to restart the movement along the path after 5 seconds (5000 milliseconds). @item @samp{pclinearmovement} (@pxref{PropClass LinMove}). This is the generic movement system. @end itemize We also create the entity with the @samp{badone_behave} behaviour. @node Tutorial Main, Tutorial App Header, Tutorial WalkTut, Tutorial WalkTut @subsubsection main.cpp This is the main entry point for the application. It does nothing more then make an instance of @code{MainApp} and pass control to that. @example #include #include "app.h" CS_IMPLEMENT_APPLICATION int main (int argc, char *argv[]) @{ MainApp app; return app.Main (argc, argv); @} @end example @node Tutorial App Header, Tutorial App, Tutorial Main, Tutorial WalkTut @subsubsection app.h In this header file we declare our main application class. This is the class where we store references to commonly used modules from Crystal Space and Crystal Entity Layer. This is also the class that contains the main event handling loop. @example #ifndef APP_H #define APP_H #include #include /* * Our main application class inherits from csApplicationFramework * and csBaseEventHandler. */ class MainApp : public csApplicationFramework, public csBaseEventHandler @{ private: // References to modules from Crystal Space. csRef g3d; csRef engine; csRef loader; csRef vfs; csRef vc; csRef kbd; // References to modules from Crystal Entity Layer. csRef pl; csRef bl; // Our level and player entity. csRef level_entity; csRef player_entity; // Inherited from csBaseEventHandler: handle a keyboard event. bool OnKeyboard (iEvent&); // Inherited from csBaseEventHandler: handle a signle frame. void ProcessFrame (); // Inherited from csBaseEventHandler: handle a signle frame. void FinishFrame (); // Create the level entity and load the level in it. // Returns false on failure. The error has been already reported to // the reporter in that case. bool LoadLevel (); // Create the player. // Returns false on failure. The error has been already reported to // the reporter in that case. bool CreatePlayer (); public: MainApp (); virtual ~MainApp (); // Inherited from csApplicationFramework: initialization. virtual bool OnInitialize (int argc, char* argv[]); // Inherited from csApplicationFramework: initialization. virtual bool Application (); @}; #endif @end example @node Tutorial App, Tutorial Behave Header, Tutorial App Header, Tutorial WalkTut @subsubsection app.cpp Here is our main application source file. @example #include #include #include #include #include #include #include #include #include #include "app.h" #include "behave.h" MainApp::MainApp () @{ SetApplicationName ("CEL Tutorial"); @} MainApp::~MainApp () @{ @} @end example In @code{LoadLevel()} we create the level entity. @code{iCelPlLayer} has a @code{CreateEntity()} conveniance function which creates the entity, assigns a behaviour to it and also creates the property classes for it. In this particular case we create an entity called @samp{level} and assign it with the behaviour that is called @samp{level_behave} (this one will be defined later in this tutorial). We also create a @samp{pczonemanager} property class (@pxref{PropClass ZoneMgr}) for it. This property class manages loading and unloading of levels (map files) in the game. @code{CEL_QUERY_PROPCLASS_ENT} is an important macro that you can use to fetch a reference to one of the property classes in an entity. Every property class implements some interface in addition to the standard @code{iCelPropertyClass} interface. In case of the zone manager this interface is @code{iPcZoneManager}. The @code{ReportError()} method is part of @code{csApplicationFramework} and makes it easier to use the reporter (Crystal Space plugin) to report errors to the user. After creating the entity we need to setup the property classes. In this case this means the zone manager. It is possible to setup the zone manager from an @sc{xml} descriptor file but in this case we set it up manually to load two files: one @file{walktut_world} file which is created in @samp{Blender} and contains the 3D geometry and one @file{walktut_entities} file which we created manually. The entities file contains definitions for entities in our game. @example bool MainApp::LoadLevel () @{ level_entity = pl->CreateEntity ("level", bl, "level_behave", "pczonemanager", CEL_PROPCLASS_END); if (!level_entity) return ReportError ("Error creating level entity!"); // Now get the iPcZoneManager interface so we can setup the level. csRef zonemgr = CEL_QUERY_PROPCLASS_ENT (level_entity, iPcZoneManager); iCelZone* zone = zonemgr->CreateZone ("main"); iCelRegion* region = zonemgr->CreateRegion ("main"); zone->LinkRegion (region); iCelMapFile* mapfile = region->CreateMapFile (); mapfile->SetPath ("/cellib/lev"); mapfile->SetFile ("walktut_world"); iCelMapFile* entitiesfile = region->CreateMapFile (); entitiesfile->SetPath ("/cellib/lev"); entitiesfile->SetFile ("walktut_entities"); return true; @} @end example Here we create the player. This time we use the @samp{player_behave} behaviour that we define later. The player entity also uses a lot more property classes. We need a camera, a mesh (3D geometry), the movement system, keyboard input, and an inventory. After creating the entity we again have to setup the various property classes. The camera needs to know about the zone manager so we also fetch that from the level entity that we created earlier. In @code{SetZoneManager()} we also indicate where the camera should start. In this example we pick region @samp{main} and the name of the camera is @samp{Camera}. For our player mesh we pick the @samp{cally} model that is part of Crystal Entity Layer. Since we are possibly using third-person camera mode the zone manager needs to know about the player mesh. This is needed so that the zone manager can load the needed regions depending on where the player moves. Then we setup @samp{pclinearmovement} and @samp{pcactormove} with the proper movement parameters. This includes the dimensions of the collision box and various speed parameters with which we will move the player. Finally we need to bind various keys to actual commands. The behaviour will get these commands (and not the keys) so this cleanly separates the actual keys and the operations performed by those keys. In real games you probably want to read the key definitions from a configuration file. @example bool MainApp::CreatePlayer () @{ player_entity = pl->CreateEntity ("player", bl, "player_behave", "pcdefaultcamera", "pcmesh", "pclinearmovement", "pcactormove", "pccommandinput", "pcinventory", CEL_PROPCLASS_END); if (!player_entity) return ReportError ("Error creating player entity!"); // Get the iPcCamera interface so that we can set the camera. csRef pccamera = CEL_QUERY_PROPCLASS_ENT ( player_entity, iPcCamera); // Get the zone manager from the level entity which should have // been created by now. csRef pczonemgr = CEL_QUERY_PROPCLASS_ENT ( level_entity, iPcZoneManager); pccamera->SetZoneManager (pczonemgr, true, "main", "Camera"); // Get the iPcMesh interface so we can load the right mesh // for our player. csRef pcmesh = CEL_QUERY_PROPCLASS_ENT ( player_entity, iPcMesh); pcmesh->SetPath ("/cel/data"); pcmesh->SetMesh ("test", "cally.cal3d"); if (!pcmesh->GetMesh ()) return ReportError ("Error loading model!"); if (pczonemgr->PointMesh ("player", "main", "Camera")) return ReportError ("Can't find region or start position in region!"); // Get iPcLinearMovement so we can setup the movement system. csRef pclinmove = CEL_QUERY_PROPCLASS_ENT ( player_entity, iPcLinearMovement); pclinmove->InitCD ( csVector3 (0.5,0.8,0.5), csVector3 (0.5,0.4,0.5), csVector3 (0,0,0)); // Get the iPcActorMove interface so that we can set movement speed. csRef pcactormove = CEL_QUERY_PROPCLASS_ENT ( player_entity, iPcActorMove); pcactormove->SetMovementSpeed (3.0f); pcactormove->SetRunningSpeed (5.0f); pcactormove->SetRotationSpeed (1.75f); // Get iPcCommandInput so we can do key bindings. The behaviour // layer will interprete the commands so the actor can move. csRef pcinput = CEL_QUERY_PROPCLASS_ENT ( player_entity, iPcCommandInput); // We read the key bindings from the standard config file. pcinput->Bind ("up", "forward"); pcinput->Bind ("down", "backward"); pcinput->Bind ("left", "rotateleft"); pcinput->Bind ("right", "rotateright"); pcinput->Bind ("m", "cammode"); pcinput->Bind ("d", "drop"); return true; @} @end example The following three methods are called by the event handler whenever a certain event occurs. @code{ProcessFrame()} is called every frame. Normally you would put the code here to draw 3D graphics. However in case of CRystal Entity Layer it is the camera property class that actually takes care of this so the implementation here is empty. In @code{FinishFrame()} we actually render everything on screen. When the player presses a key the @code{OnKeyboard()} routine is called. In this case we only listen to the escape key to exit the application since all other keys are handled by the player entity. @example void MainApp::ProcessFrame () @{ @} void MainApp::FinishFrame () @{ // Just tell the 3D renderer that everything has been rendered. g3d->FinishDraw (); g3d->Print (0); @} bool MainApp::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 q = CS_QUERY_REGISTRY(GetObjectRegistry(), iEventQueue); if (q.IsValid()) q->GetEventOutlet()->Broadcast( csevQuit(GetObjectRegistry())); @} @} return false; @} @end example Initialization routines. @code{OnInitialize()} is called by the application framework. In this routine we call @code{RequestPlugins()} to load various plugins that we will need. In addition to the standard plugins that most Crystal Space applications need (like OpenGL renderer, engine, level loader, @dots{}) we also load the @sc{cel} physical layer and the @sc{opcode} collision detection plugin. Most of these plugins we will not use directly but they are used by the property classes. @code{Application()} is called when it is time to open the application screen. Here we fetch various modules from the object registry and store a reference in our main class. Here we also create our behaviour layer and we register it to the object registry and also to the physical layer (with @code{RegisterBehaviourLayer()}). Then we need to load the property class factories for all property classes that we plan to use in this tutorial. Finally we load our level (@code{LoadLevel())} and create the player (@code{CreatePlayer()}). The last thing we do here is call @code{Run()} which will start the main event loop. This function only returns when the application exits. @example bool MainApp::OnInitialize (int argc, char* argv[]) @{ if (!celInitializer::RequestPlugins (object_reg, 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 ("cel.physicallayer", iCelPlLayer), CS_REQUEST_PLUGIN ("crystalspace.collisiondetection.opcode", iCollideSystem), CS_REQUEST_END)) return ReportError ("Can't initialize plugins!"); csBaseEventHandler::Initialize(object_reg); if (!RegisterQueue(object_reg, csevAllEvents(object_reg))) return ReportError ("Can't setup event handler!"); return true; @} bool MainApp::Application () @{ if (!OpenApplication (object_reg)) return ReportError ("Error opening system!"); g3d = CS_QUERY_REGISTRY (object_reg, iGraphics3D); engine = CS_QUERY_REGISTRY (object_reg, iEngine); loader = CS_QUERY_REGISTRY (object_reg, iLoader); vfs = CS_QUERY_REGISTRY (object_reg, iVFS); vc = CS_QUERY_REGISTRY (object_reg, iVirtualClock); kbd = CS_QUERY_REGISTRY (object_reg, iKeyboardDriver); pl = CS_QUERY_REGISTRY (object_reg, iCelPlLayer); bl.AttachNew (new BehaviourLayer(pl)); // We also need to register it to the object registry. if (!object_reg->Register (bl, "iCelBlLayer")) return ReportError ("Can't register our behaviour layer!"); // Make sure the application dir is mounted at /cel vfs->Mount ("cel", "$.$/"); pl->RegisterBehaviourLayer (bl); if (!pl->LoadPropertyClassFactory ("cel.pcfactory.zonemanager")) return ReportError ("Error loading pczonemanager factory!"); if (!pl->LoadPropertyClassFactory ("cel.pcfactory.solid")) return ReportError ("Error loading pcsolid factory!"); if (!pl->LoadPropertyClassFactory ("cel.pcfactory.colldet")) return ReportError ("Error loading pccolldet factory!"); if (!pl->LoadPropertyClassFactory ("cel.pcfactory.defaultcamera")) return ReportError ("Error loading pcdefaultcamera factory!"); if (!pl->LoadPropertyClassFactory ("cel.pcfactory.mesh")) return ReportError ("Error loading pcmesh factory!"); if (!pl->LoadPropertyClassFactory ("cel.pcfactory.meshselect")) return ReportError ("Error loading pcmeshselect factory!"); if (!pl->LoadPropertyClassFactory ("cel.pcfactory.linmove")) return ReportError ("Error loading pclinmove factory!"); if (!pl->LoadPropertyClassFactory ("cel.pcfactory.pccommandinput")) return ReportError ("Error loading pccommandinput factory!"); if (!pl->LoadPropertyClassFactory ("cel.pcfactory.actormove")) return ReportError ("Error loading pcactormove factory!"); if (!pl->LoadPropertyClassFactory ("cel.pcfactory.inventory")) return ReportError ("Error loading pcinventory factory!"); if (!pl->LoadPropertyClassFactory ("cel.pcfactory.properties")) return ReportError ("Error loading pcproperties factory!"); if (!pl->LoadPropertyClassFactory ("cel.pcfactory.timer")) return ReportError ("Error loading pctimer factory!"); if (!LoadLevel ()) return ReportError ("Error loading level!"); if (!CreatePlayer ()) return ReportError ("Couldn't create player!"); Run (); return true; @} @end example @node Tutorial Behave Header, Tutorial Behave, Tutorial App, Tutorial WalkTut @subsubsection behave.h Here is the header for our behaviour layer. Here we define the behaviour layer and also the implementations of every behaviour. @example #ifndef BEHAVE_H #define BEHAVE_H #include #include #include #include #include #include #include #include @end example @code{BehaviourLayer} is our behaviour layer. It is a simple class that will create the right behaviour depending on the given name. @example class BehaviourLayer : public iCelBlLayer @{ private: iCelPlLayer* pl; public: BehaviourLayer (iCelPlLayer* pl); virtual ~BehaviourLayer (); SCF_DECLARE_IBASE; virtual const char* GetName () const @{ return "behaviourlayer"; @} virtual iCelBehaviour* CreateBehaviour (iCelEntity* entity, const char* name); @}; @end example @code{BehaviourCommon} is the common superclass for all our behaviours. It takes care of overriding the standard @code{SendMessage()} routine and to replace it with a version that uses an @sc{id} instead of a string. That is faster. @example class BehaviourCommon : public iCelBehaviour @{ protected: iCelEntity* entity; BehaviourLayer* bl; iCelPlLayer* pl; public: BehaviourCommon (iCelEntity* entity, BehaviourLayer* bl, iCelPlLayer* pl); virtual ~BehaviourCommon (); /** * This is a specific version of SendMessage() that accepts * an integer instead of a csStringID. Subclasses of ccBehaviourBase * will only have to override this version. The normal iCelBehaviour * versions of SendMessage() are implemented in this class and * will redirect to this version. */ virtual bool SendMessage (csStringID msg_id, iCelPropertyClass* pc, celData& ret, iCelParameterBlock* params, va_list arg); SCF_DECLARE_IBASE; virtual iCelBlLayer* GetBehaviourLayer () const @{ return bl; @} virtual bool SendMessage (const char* msg_id, iCelPropertyClass* pc, celData& ret, iCelParameterBlock* params, ...); virtual bool SendMessageV (const char* msg_id, iCelPropertyClass* pc, celData& ret, iCelParameterBlock* params, va_list arg); virtual void* GetInternalObject () @{ return 0; @} @}; @end example The level behaviour doesn't do much. @example class BehaviourLevel : public BehaviourCommon @{ public: BehaviourLevel (iCelEntity* entity, BehaviourLayer* bl, iCelPlLayer* pl) : BehaviourCommon (entity, bl, pl) @{ @} virtual ~BehaviourLevel () @{ @} virtual const char* GetName () const @{ return "level_behave"; @} virtual bool SendMessage (csStringID msg_id, iCelPropertyClass* pc, celData& ret, iCelParameterBlock* params, va_list arg); @}; @end example This is the behaviour for a box. This behaviour will be used for entities in the @samp{entities} file. @example class BehaviourBox : public BehaviourCommon @{ private: csStringID id_pcmeshsel_down; void GetPlayer (); csWeakRef pcmeshsel; iCelEntity* player; void PickUp (); public: BehaviourBox (iCelEntity* entity, BehaviourLayer* bl, iCelPlLayer* pl); virtual ~BehaviourBox () @{ @} virtual const char* GetName () const @{ return "box_behave"; @} virtual bool SendMessage (csStringID msg_id, iCelPropertyClass* pc, celData& ret, iCelParameterBlock* params, va_list arg); @}; @end example Another behaviour used for entities in the @samp{entities} file. @example class BehaviourBadOne : public BehaviourCommon @{ private: csStringID id_pctimer_wakeup; csStringID id_par_elapsedticks; csRef path; void ReadPath (); void Restart (); public: BehaviourBadOne (iCelEntity* entity, BehaviourLayer* bl, iCelPlLayer* pl); virtual ~BehaviourBadOne () @{ @} virtual const char* GetName () const @{ return "badone_behave"; @} virtual bool SendMessage (csStringID msg_id, iCelPropertyClass* pc, celData& ret, iCelParameterBlock* params, va_list arg); @}; @end example And finally the behaviour for our player. This one takes care of matching commands from the @samp{pccommandinput} property class to actual operations on @samp{pcactormove}. @example class BehaviourPlayer : public BehaviourCommon @{ private: csStringID id_pccommandinput_forward1; csStringID id_pccommandinput_forward0; csStringID id_pccommandinput_backward1; csStringID id_pccommandinput_backward0; csStringID id_pccommandinput_rotateleft1; csStringID id_pccommandinput_rotateleft0; csStringID id_pccommandinput_rotateright1; csStringID id_pccommandinput_rotateright0; csStringID id_pccommandinput_cammode1; csStringID id_pccommandinput_drop1; csStringID id_pcinventory_addchild; csStringID id_pcinventory_removechild; void GetActorMove (); csWeakRef pcactormove; void GetInventory (); csWeakRef pcinventory; void GetMesh (); csWeakRef pcmesh; void ShowInventory (); void Drop (); public: BehaviourPlayer (iCelEntity* entity, BehaviourLayer* bl, iCelPlLayer* pl); virtual ~BehaviourPlayer () @{ @} virtual const char* GetName () const @{ return "player_behave"; @} virtual bool SendMessage (csStringID msg_id, iCelPropertyClass* pc, celData& ret, iCelParameterBlock* params, va_list arg); @}; #endif @end example @node Tutorial Behave, , Tutorial Behave Header, Tutorial WalkTut @subsubsection behave.cpp Here is the source for our behaviour layer. The @code{CreateBehaviour()} function in @code{BehaviourLayer} will compare the given name parameter with the possible names for behaviours and then create the appropriate subclass of @code{BehaviourCommon}. @example #include #include #include #include #include #include "behave.h" SCF_IMPLEMENT_IBASE (BehaviourLayer) SCF_IMPLEMENTS_INTERFACE (iCelBlLayer) SCF_IMPLEMENT_IBASE_END BehaviourLayer::BehaviourLayer (iCelPlLayer* pl) @{ SCF_CONSTRUCT_IBASE (0); BehaviourLayer::pl = pl; @} BehaviourLayer::~BehaviourLayer () @{ SCF_DESTRUCT_IBASE (); @} iCelBehaviour* BehaviourLayer::CreateBehaviour (iCelEntity* entity, const char* name) @{ iCelBehaviour* behave = 0; if (!strcmp (name, "level_behave")) behave = new BehaviourLevel (entity, this, pl); else if (!strcmp (name, "player_behave")) behave = new BehaviourPlayer (entity, this, pl); else if (!strcmp (name, "box_behave")) behave = new BehaviourBox (entity, this, pl); else if (!strcmp (name, "badone_behave")) behave = new BehaviourBadOne (entity, this, pl); if (behave) @{ entity->SetBehaviour (behave); // There is now a reference in the entity. We destroy // our own reference here. behave->DecRef (); @} return behave; @} @end example In @code{SendMessage()} of @code{BehaviourCommon} we use the @code{FetchStringID()} function to @samp{intern} the message string to an @sc{id}. In the actual implementations of @code{SendMessage()} in the various behaviours we can then use integer comparisons instead of string compares. @example SCF_IMPLEMENT_IBASE (BehaviourCommon) SCF_IMPLEMENTS_INTERFACE (iCelBehaviour) SCF_IMPLEMENT_IBASE_END BehaviourCommon::BehaviourCommon (iCelEntity* entity, BehaviourLayer* bl, iCelPlLayer* pl) @{ SCF_CONSTRUCT_IBASE (0); BehaviourCommon::entity = entity; BehaviourCommon::bl = bl; BehaviourCommon::pl = pl; @} BehaviourCommon::~BehaviourCommon () @{ SCF_DESTRUCT_IBASE (); @} bool BehaviourCommon::SendMessage (const char* msg_id, iCelPropertyClass* pc, celData& ret, iCelParameterBlock* params, ...) @{ va_list arg; va_start (arg, params); bool rc = SendMessageV (msg_id, pc, ret, params, arg); va_end (arg); return rc; @} bool BehaviourCommon::SendMessageV (const char* msg_id, iCelPropertyClass* pc, celData& ret, iCelParameterBlock* params, va_list arg) @{ csStringID id = pl->FetchStringID (msg_id); return SendMessage (id, pc, ret, params, arg); @} bool BehaviourCommon::SendMessage (csStringID, iCelPropertyClass*, celData&, iCelParameterBlock*, va_list) @{ return false; @} @end example The level behaviour does not do anything in this tutorial. @example bool BehaviourLevel::SendMessage (csStringID msg_id, iCelPropertyClass* pc, celData& ret, iCelParameterBlock* params, va_list arg) @{ return BehaviourCommon::SendMessage (msg_id, pc, ret, params, arg); @} @end example The player behaviour is responsible for managing input (from @samp{pccommandinput}) and moving the actor in response to that input (with @samp{pcactormove}). First we request @sc{id}'s from the physical layer for the messages from the input property class that we want to respond too. For every command we defined in a @code{Bind()} in the @code{CreatePlayer()} function we get both a @samp{1} (when key is pressed) and a @samp{0} message (when key is released). We also respond to messages from the inventory system so that we can print out a message when an object is added or removed from the player inventory. The @code{ShowInventory()} function will show the inventory on standard output and the @code{Drop()} function will drop one item from the inventory. In @code{SendMessage()} we test which message we got. If it is one of the input messages we call the appropriate @sc{api} method in @samp{pcactormove} to move, rotate, or jump the player. If it is one of the inventory messages then we show the inventory contents. @example BehaviourPlayer::BehaviourPlayer (iCelEntity* entity, BehaviourLayer* bl, iCelPlLayer* pl) : BehaviourCommon (entity, bl, pl) @{ id_pccommandinput_forward1 = pl->FetchStringID ("pccommandinput_forward1"); id_pccommandinput_forward0 = pl->FetchStringID ("pccommandinput_forward0"); id_pccommandinput_backward1 = pl->FetchStringID ("pccommandinput_backward1"); id_pccommandinput_backward0 = pl->FetchStringID ("pccommandinput_backward0"); id_pccommandinput_rotateleft1 = pl->FetchStringID ("pccommandinput_rotateleft1"); id_pccommandinput_rotateleft0 = pl->FetchStringID ("pccommandinput_rotateleft0"); id_pccommandinput_rotateright1 = pl->FetchStringID ("pccommandinput_rotateright1"); id_pccommandinput_rotateright0 = pl->FetchStringID ("pccommandinput_rotateright0"); id_pccommandinput_cammode1 = pl->FetchStringID ("pccommandinput_cammode1"); id_pccommandinput_drop1 = pl->FetchStringID ("pccommandinput_drop1"); id_pcinventory_addchild = pl->FetchStringID ("pcinventory_addchild"); id_pcinventory_removechild = pl->FetchStringID ("pcinventory_removechild"); @} void BehaviourPlayer::GetActorMove () @{ if (!pcactormove) @{ pcactormove = CEL_QUERY_PROPCLASS_ENT (entity, iPcActorMove); @} @} void BehaviourPlayer::GetInventory () @{ if (!pcinventory) @{ pcinventory = CEL_QUERY_PROPCLASS_ENT (entity, iPcInventory); @} @} void BehaviourPlayer::GetMesh () @{ if (!pcmesh) @{ pcmesh = CEL_QUERY_PROPCLASS_ENT (entity, iPcMesh); @} @} void BehaviourPlayer::ShowInventory () @{ size_t count = pcinventory->GetEntityCount (); size_t i; for (i = 0 ; i < count ; i++) @{ iCelEntity* child = pcinventory->GetEntity (i); printf (" child %d is '%s'\n", i, child->GetName ()); @} @} void BehaviourPlayer::Drop () @{ GetInventory (); size_t count = pcinventory->GetEntityCount (); if (count <= 0) @{ printf ("Inventory is empty!\n"); return; @} iCelEntity* child = pcinventory->GetEntity (0); pcinventory->RemoveEntity (child); csRef pclinmove = CEL_QUERY_PROPCLASS_ENT ( child, iPcLinearMovement); if (pclinmove) @{ GetMesh (); // Now we get current position and orientation from player // mesh and from that we calculate a spot in front of the // player where we will drop down the item. csVector3 pos = pcmesh->GetMesh ()->GetMovable ()->GetTransform () .This2Other (csVector3 (0, 2, -2)); iSector* sector = pcmesh->GetMesh ()->GetMovable ()->GetSectors ()->Get (0); pclinmove->SetPosition (pos, 0, sector); pclinmove->SetVelocity (csVector3 (0, .1f, 0)); csRef pcmesh_child = CEL_QUERY_PROPCLASS_ENT (child, iPcMesh); if (pcmesh_child) pcmesh_child->Show (); @} @} bool BehaviourPlayer::SendMessage (csStringID msg_id, iCelPropertyClass* pc, celData& ret, iCelParameterBlock* params, va_list arg) @{ GetActorMove (); if (msg_id == id_pccommandinput_forward1) pcactormove->Forward (true); else if (msg_id == id_pccommandinput_forward0) pcactormove->Forward (false); else if (msg_id == id_pccommandinput_backward1) pcactormove->Backward (true); else if (msg_id == id_pccommandinput_backward0) pcactormove->Backward (false); else if (msg_id == id_pccommandinput_rotateleft1) pcactormove->RotateLeft (true); else if (msg_id == id_pccommandinput_rotateleft0) pcactormove->RotateLeft (false); else if (msg_id == id_pccommandinput_rotateright1) pcactormove->RotateRight (true); else if (msg_id == id_pccommandinput_rotateright0) pcactormove->RotateRight (false); else if (msg_id == id_pccommandinput_cammode1) pcactormove->ToggleCameraMode (); else if (msg_id == id_pccommandinput_drop1) Drop (); else if (msg_id == id_pcinventory_addchild) @{ GetInventory (); printf ("Got a new object! Objects in inventory:\n"); ShowInventory (); @} else if (msg_id == id_pcinventory_removechild) @{ GetInventory (); printf ("Object removed from inventory! Objects in inventory:\n"); ShowInventory (); @} else return BehaviourCommon::SendMessage (msg_id, pc, ret, params, arg);; return true; @} @end example The box behaviour supports picking up and putting itself in the player inventory. To do that entities that use this behaviour need to use the @samp{pcmeshselect} property class so that they get a message whenever the entity is selected. In this behaviour we wait for that message and when we get it we will add ourselves to the inventory of the player. @example BehaviourBox::BehaviourBox (iCelEntity* entity, BehaviourLayer* bl, iCelPlLayer* pl) : BehaviourCommon (entity, bl, pl) @{ id_pcmeshsel_down = pl->FetchStringID ("pcmeshsel_down"); GetPlayer (); @} void BehaviourBox::PickUp () @{ if (!player) return; csRef pcinv = CEL_QUERY_PROPCLASS_ENT (player, iPcInventory); if (pcinv) @{ pcinv->AddEntity (entity); csRef pcmesh = CEL_QUERY_PROPCLASS_ENT (entity, iPcMesh); if (pcmesh) pcmesh->Hide (); @} @} void BehaviourBox::GetPlayer () @{ if (!pcmeshsel || !player) @{ pcmeshsel = CEL_QUERY_PROPCLASS_ENT (entity, iPcMeshSelect); player = pl->FindEntity ("player"); if (!player) return; csRef pccamera = CEL_QUERY_PROPCLASS_ENT (player, iPcCamera); if (pccamera) pcmeshsel->SetCamera (pccamera); @} @} bool BehaviourBox::SendMessage (csStringID msg_id, iCelPropertyClass* pc, celData& ret, iCelParameterBlock* params, va_list arg) @{ if (msg_id == id_pcmeshsel_down) @{ PickUp (); return true; @} return BehaviourCommon::SendMessage (msg_id, pc, ret, params, arg); @} @end example This behaviour represents an entity that moves around on a path. We use catmull rom splines to calculate a smooth path. The @code{csPath} class from Crystal Space helps with this. In the @code{ReadPath()} function we read the path from properties that are assigned to the entity (using the general @samp{pcproperties} property class). Entities that use this behaviour also need to have a @samp{pctimer} property class. This is used to be able to restart the movement path after a certain time has expired. @example BehaviourBadOne::BehaviourBadOne (iCelEntity* entity, BehaviourLayer* bl, iCelPlLayer* pl) : BehaviourCommon (entity, bl, pl) @{ id_pctimer_wakeup = pl->FetchStringID ("pctimer_wakeup"); id_par_elapsedticks = pl->FetchStringID ("cel.parameter.elapsedticks"); ReadPath (); @} static bool GetPropVector (iPcProperties* pcprop, const char* prefix, int i, csVector3& v) @{ csString propname = prefix; propname += i; size_t idx = pcprop->GetPropertyIndex (propname); if (idx == csArrayItemNotFound) return false; if (!pcprop->GetPropertyVector (idx, v)) return false; return true; @} static bool GetPropLong (iPcProperties* pcprop, const char* prefix, int i, long& l) @{ csString propname = prefix; propname += i; size_t idx = pcprop->GetPropertyIndex (propname); if (idx == csArrayItemNotFound) return false; l = pcprop->GetPropertyLong (idx); return true; @} void BehaviourBadOne::ReadPath () @{ csRef pcprop = CEL_QUERY_PROPCLASS_ENT (entity, iPcProperties); // Count the number of points we have. int count = 0; for (;;) @{ csVector3 v; if (!GetPropVector (pcprop, "pos", count+1, v)) break; count++; @} path.AttachNew (new csPath(count)); int i = 0; long totaltime = 0; for (i = 0 ; i < count ; i++) @{ csVector3 pos, forward, up; GetPropVector (pcprop, "pos", i+1, pos); if (!GetPropVector (pcprop, "forward", i+1, forward)) forward.Set (1, 0, 0); if (!GetPropVector (pcprop, "up", i+1, up)) up.Set (0, 1, 0); long time; if (!GetPropLong (pcprop, "time", i+1, time)) time = 1000; path->SetPositionVector (i, pos); path->SetUpVector (i, up); path->SetForwardVector (i, forward); path->SetTime (i, float (totaltime) / 1000.0); totaltime += time; @} csRef pclinmove = CEL_QUERY_PROPCLASS_ENT ( entity, iPcLinearMovement); if (pclinmove) @{ for (i = 0 ; i < count ; i++) @{ pclinmove->SetPathAction (i, "default"); @} @} @} void BehaviourBadOne::Restart () @{ csRef pclinmove = CEL_QUERY_PROPCLASS_ENT ( entity, iPcLinearMovement); if (pclinmove) @{ pclinmove->SetPath (path); pclinmove->SetPathTime (0); @} @} bool BehaviourBadOne::SendMessage (csStringID msg_id, iCelPropertyClass* pc, celData& ret, iCelParameterBlock* params, va_list arg) @{ if (msg_id == id_pctimer_wakeup) @{ Restart (); return true; @} return BehaviourCommon::SendMessage (msg_id, pc, ret, params, arg); @} @end example