| Introduction |
Crystal Space is a package of components and libraries which can all be useful for creating computer games. Although some of the packages are intended more for 3D gaming this is not as such a restriction of the package. Components as the sound driver function just as well in a 2D gaming environment and the networking component can even be used in a text oriented environment. This hilights one of the important characteristics of Crystal Space: the components and libraries are more or less independent of each other. If you don't want networking, then just don't use the networking drivers. If you don't want scripting then don't include that. All packages are designed to be functional on their own or with a minimal number of other packages. There are some dependencies of course. For example, the 3D Engine requires a 3D Rasterizer to display its output. On the other hand, you could very well use the 3D Rasterizer without the 3D Engine.
Although there is a high level of independence, there is also a high level of integration. The components and libraries were also designed to be used together and as such offer a flexible scheme of integration.
In the previous paragraphs we frequently mentioned components and libraries. In some contexts these words can mean the same. A component can be the contents of a library for example. You can also have libraries of components. In the case of Crystal Space we place a special distinction between the two. When we speak of a component we generally refer to a library which uses the COM mechanism for communication. This means that it follows a very strict set of rules which should be followed when creating, using, and destroying components. Libraries, on the other hand, are just libraries which offer a collection of C++ classes. Libraries can also export COM interfaces if needed but they are not native COM components.
In the rest of this document we will often say modules to refer to both the components and libraries.
We will not go into much detail over why we choose COM and why not CORBA for example. That's a matter of much debate and not very useful at this stage. I will just mention a few reasons why we choose COM instead of plain C++/C libraries:
One way to view the integration through COM is as a communications network with the interfaces defining the protocol. Components can only speak to other components by using the protocol or the interface.
| What Is In It? |
Crystal Space currently contains the following modules:
| Getting Started |
When you downloaded the Crystal Space package it included an 'include' directory. This directory contains all header files you need to be able to use the Crystal Space components and libraries in your own applications. On one hand, the include directory contains COM interface definitions. We have definitions in IDL (Interface Definition Language) and as plain C++ header files. These general start with 'i' (like 'iworld.h'). On the other hand we also have plain C++ include files for interfacing with the libraries of Crystal Space. You can move this include directory somewhere on your system. On Linux you could move it to /usr/include/cs for example. It's your choice.
Crystal Space also has a reference manual. If you downloaded the 'csapi' package the you should have it and this link may work to get you to the reference.
| Tutorials |
In this part various tutorials are given. The ultimate purpose of this document is to provide tutorials for all major areas of Crystal Space. In all the tutorial news code is marked bold while code repeated from a previous section uses plain style.
| Tutorial 1: Basic Usage of the Engine |
In this tutorial I give a step-by-step explanation on how you can use the 3D Engine from within your application. This tutorial is mainly based on the Simple application which you can compile and run to see what it does. There are various ways to use Crystal Space. The Simple application uses inheritance to create a subclass of the system dependent main class (SysSystemDriver) but you don't need to do it like this.
Side-note: The API of Crystal Space is still maturing. There are various things which will likely change in the future and you will often encounter ugly things and requirements. For example, in the code below you will find that you need to have the functions debug_dump() and cleanup() defined in your application. Crystal Space depends on them. This will change in the future.
#include "cssys/common/sysdriv.h"
class csWorld;
class Simple : public SysSystemDriver
{
public:
csWorld* world;
public:
Simple ();
virtual ~Simple ();
void InitApp ();
};
In the source file 'simple.cpp' we put the following:
#include "sysdef.h"
#include "simple.h"
#include "csengine/world.h"
#include "csengine/sector.h"
#include "csengine/camera.h"
#include "csengine/csview.h"
#include "csengine/light/light.h"
#include "csengine/polygon/polygon.h"
#include "cssys/common/system.h"
#include "igraph3d.h"
#include "itxtmgr.h"
Simple* Sys = NULL;
Simple::Simple ()
{
world = NULL;
}
Simple::~Simple ()
{
delete world;
}
void cleanup ()
{
delete Sys;
}
void debug_dump ()
{
}
void Simple::InitApp ()
{
if (!Open ("Simple Crystal Space Application"))
{
Sys->Printf (MSG_FATAL_ERROR, "Error opening system!\n");
cleanup ();
exit (1);
}
world->Initialize (GetISystemFromSystem (this), piGI, NULL);
}
int main (int argc, char* argv[])
{
Sys = new Simple ();
Sys->world = new csWorld ();
if (!Sys->Initialize (argc, argv, Sys->world->GetEngineConfigCOM ()))
{
Sys->Printf (MSG_FATAL_ERROR, "Error initializing system!\n");
cleanup ();
exit (1);
}
Sys->InitApp ();
Sys->Loop ();
cleanup ();
}
This is almost the simplest possible application and it is absolutely useless :-) Also don't run it on an operating system where you can't kill a running application because it has no way to exit.
Before we start making this application more useful lets have a look at what actually happens here. The main routine first creates an instance of our Simple class. The next step is the creation of the world. The world is actually the main 3D Engine class and is one of the most important classes for interfacing with the engine (see csWorld reference for more information).
Only after the world is created can you initialize the system. The reason is that the system needs to know the world to be able to parse command line options for it (@@@ It is possible that this changes slightly in the future). We call Initialize() on our Simple instance. This is a function inherited from SysSystemDriver and is responsible for initializing all needed graphics stuff. It is also responsible for parsing the command line and feeding all the options to the appropriate handlers. Note that you should always test for failure of such routines. There may be various reasons for failure (@@@ In the future we will provide routines to query the failure reason). The function GetEngineConfigCOM() returns a COM interface (IConfig) that the system can use to query all settings that the 3D Engine supports. This can then be used for controlling those settings by the command line and other stuff.
Then we call InitApp() which is one of our own functions. This will initialize everything for our application (more on this later).
The call to Loop() puts Crystal Space in the main event loop. This is where the application really starts running and interacting with the user. This call returns as soon as an exit message was received by the system at which point we cleanup() everything.
The InitApp() function initializes the rest of the application. First it opens the system which basicly means that all graphics subsystems are opened (the window will be opened). This function can also fail so again test for this.
Finally it initializes the world for the given graphics system. The first parameter to csWorld::Intialize() should be a pointer to the system dependent COM interface (ISystem). The 3D Engine will use this for various things like output messages on the console. The GetISystemFromSystem() macro converts a pointer to a SysSystemDriver instance to an interface pointer suitable for COM. piGI is a public member of SysSystemDriver which is given to the 3D Engine to query the graphics subsystem (for example, to ask what depth the display is using). It is a COM interface (IGraphicsInfo). The last argument is not used in this application and will be removed in the future.
#include "cssys/common/sysdriv.h"
class csWorld;
class Simple : public SysSystemDriver
{
public:
csWorld* world;
public:
Simple ();
virtual ~Simple ();
void InitApp ();
virtual void NextFrame (long elapsed_time, long current_time);
void eatkeypress (int key, bool shift, bool alt, bool ctrl);
};
NextFrame() is a function which is called every frame by the system. It can be used for drawing the world but for now we will only use it to do event handling. eatkeypress() is our own function which does key handling.
We add the following two definitions to 'simple.cpp':
void Simple::eatkeypress (int key, bool shift, bool alt, bool ctrl)
{
(void)shift; (void)alt; (void)ctrl;
switch (key)
{
case CSKEY_ESC: Shutdown = true; break;
}
}
void Simple::NextFrame (long elapsed_time, long current_time)
{
SysSystemDriver::NextFrame (elapsed_time, current_time);
// Handle all events.
csEvent *Event;
while ((Event = EventQueue->Get ()))
{
switch (Event->Type)
{
case csevKeyDown:
eatkeypress (Event->Key.Code, Event->Key.ShiftKeys & CSMASK_SHIFT,
Event->Key.ShiftKeys & CSMASK_ALT, Event->Key.ShiftKeys & CSMASK_CTRL);
break;
case csevMouseDown:
break;
case csevMouseMove:
break;
case csevMouseUp:
break;
}
delete Event;
}
}
Every frame NextFrame() is called. Several events (like keyboard and mouse events) could have been accumulated in the mean time. You can query all accumulated events with the code shown above. In case it is a key press we call eatkeypress() to perform the appropriate actions. Currently eatkeypress() only listens to the escape key to quit the application. So compile this application and run it. You should now be able to quit it with the 'esc' key.
void Simple::InitApp ()
{
if (!Open ("Simple Crystal Space Application"))
{
Sys->Printf (MSG_FATAL_ERROR, "Error opening system!\n");
cleanup ();
exit (1);
}
world->Initialize (GetISystemFromSystem (this), piGI, NULL);
csTextureHandle* tm = CSLoader::LoadTexture (world, "stone", "stone4.gif");
csSector* room = world->NewSector ();
csPolygon3D* p;
p = room->NewPolygon (tm);
p->AddVertex (-5, 0, 5);
p->AddVertex (5, 0, 5);
p->AddVertex (5, 0, -5);
p->AddVertex (-5, 0, -5);
p->SetTextureSpace (p->Vobj (0), p->Vobj (1), 3);
p = room->NewPolygon (tm);
p->AddVertex (-5, 20, -5);
p->AddVertex (5, 20, -5);
p->AddVertex (5, 20, 5);
p->AddVertex (-5, 20, 5);
p->SetTextureSpace (p->Vobj (0), p->Vobj (1), 3);
p = room->NewPolygon (tm);
p->AddVertex (-5, 20, 5);
p->AddVertex (5, 20, 5);
p->AddVertex (5, 0, 5);
p->AddVertex (-5, 0, 5);
p->SetTextureSpace (p->Vobj (0), p->Vobj (1), 3);
p = room->NewPolygon (tm);
p->AddVertex (5, 20, 5);
p->AddVertex (5, 20, -5);
p->AddVertex (5, 0, -5);
p->AddVertex (5, 0, 5);
p->SetTextureSpace (p->Vobj (0), p->Vobj (1), 3);
p = room->NewPolygon (tm);
p->AddVertex (-5, 20, -5);
p->AddVertex (-5, 20, 5);
p->AddVertex (-5, 0, 5);
p->AddVertex (-5, 0, -5);
p->SetTextureSpace (p->Vobj (0), p->Vobj (1), 3);
p = room->NewPolygon (tm);
p->AddVertex (5, 20, -5);
p->AddVertex (-5, 20, -5);
p->AddVertex (-5, 0, -5);
p->AddVertex (5, 0, -5);
p->SetTextureSpace (p->Vobj (0), p->Vobj (1), 3);
csStatLight* light;
light = new csStatLight (-3, 5, 0, 10, 1, 0, 0, false);
room->AddLight (light);
light = new csStatLight (3, 5, 0, 10, 0, 0, 1, false);
room->AddLight (light);
light = new csStatLight (0, 5, -3, 10, 0, 1, 0, false);
room->AddLight (light);
world->Prepare (piG3D);
}
This extra code first loads a texture with LoadTexture(). The first parameter is the name of the texture as it will be known in the engine. The second parameter is the actual filename (note, if you don't have the stone4.gif texture you can use another one. The only requirement is that it must have sizes which are a power of 2 (e.g. 64x64)). The resulting csTextureHandle can be given to polygons or other engine objects which require textures.
Then we create our room with NewSector(). This room will initially be empty. A room in Crystal Space is represented by csSector which is basicly a convex set of polygons.
Now we create the six walls of our room. To do this we call NewPolygon() for every wall. This call expects one parameter: the texture to use. NewPolygon() returns a pointer to a csPolygon3D. This polygon will be empty so you need to add vertices and also define how the texture should be mapped on that polygon. To add vertices we use AddVertex() which expects a location in object space. To define how the texture is mapped on the polygon we use SetTextureSpace(). There are several versions of this function. The one we use in this tutorial is one of the simplest but it offers the least control. In this particular case we take the first two vertices of the polygon and use that for the u-axis of the texture. The v-axis will be calculated perpendicular to the u-axis. The parameter 3 indicates that the texture will be scaled so that one texture tile is exactly 3x3 world units big.
Finally we create some lights in our room to make sure that we actually are able to see the walls. The class csStatLight represents a static light (cannot move and change intensity) which perfectly suits our needs. We create three such lights and add them to the room with AddLight(). When creating a light we use eight parameters. The first three are the location of the light in the world. Then follows a radius. The light will not affect polygons which are outside the sphere described by the center of the light and the radius. The three following parameters are the color of the light in RGB format (1,1,1 means white and 0,0,0 means black). The last parameter indicates wether or not we want to have a pseudo-dynamic light. A pseudo-dynamic light still cannot move but it can change intensity. There are some performance costs associated with pseudo-dynamic lights so it is not enabled by default.
The last call to Prepare() prepare the world for rendering your scene. It will prepare all textures and create all lightmaps if needed. Only after this call can you start rendering your world. The piG3D parameter to Prepare() is a COM interface pointer to the 3D rasterizer (IGraphics3D). piG3D is a public member of SysSystemDriver. It is used by Prepare() because the lightmaps may have to be converted to a format more suitable for the chosen 3D renderer.
Ok, now we have created our room and properly initialized it. If you would compile and run this application you would still see a black screen. Why? Because we have not created a camera through which you can see.
#include "cssys/common/sysdriv.h"
class csWorld;
class csView;
class Simple : public SysSystemDriver
{
public:
csWorld* world;
csView* view;
public:
Simple ();
virtual ~Simple ();
void InitApp ();
virtual void NextFrame (long elapsed_time, long current_time);
void eatkeypress (int key, bool shift, bool alt, bool ctrl);
};
Then edit 'simple.cpp' and make the following changes to the constructor and destructor of Simple:
Simple::Simple ()
{
world = NULL;
view = NULL;
}
Simple::~Simple ()
{
delete view;
delete world;
}
At the end of our InitApp() function we add the following:
void Simple::InitApp ()
{
...
world->Prepare (piG3D);
view = new csView (world, piG3D);
view->SetSector (room);
view->GetCamera ()->SetPosition (csVector3 (0, 5, 0));
view->SetRectangle (2, 2, FrameWidth - 4, FrameHeight - 4);
ITextureManager* txtmgr;
piG3D->GetTextureManager (&txtmgr);
txtmgr->AllocPalette ();
}
So first we create a view for our world and 3D graphics renderer. The view has a current sector which is passed to the camera and is set by SetSector(). The camera also has a position in that sector which you can set by first getting the camera with GetCamera() and then setting the position (which is a csVector3) with SetPosition(). The view also holds a clipping region which corresponds to the area on the window that is going to be used for drawing the world. Crystal Space supports convex polygons to be used as viewing areas, but in case we use a simple rectangle which has almost the size of the window. We set this viewing rectangle with SetRectangle().
The last code we added allocates the palette. Note that is needed even if you are running on a truecolor display which has no palette. That's because the code does some other things beside setting up a palette.
Now, this still isn't enough. We have a camera but the camera is not used. We still need to enhance NextFrame() so that every frame the world is drawn through the camera. Change NextFrame() as follows:
void Simple::NextFrame (long elapsed_time, long current_time)
{
SysSystemDriver::NextFrame (elapsed_time, current_time);
...
if (piG3D->BeginDraw (CSDRAW_3DGRAPHICS) != S_OK) return;
view->Draw ();
piG3D->FinishDraw ();
piG3D->Print (NULL);
}
First we indicate to the 3D rasterizer that we want to start drawing 3D graphics. This calls makes sure that the needed buffers are set up and performs all necessary initialization. Then we draw through our view by calling Draw() which updates the view area with the 3D world data as seen through the camera. After this we finish 3D drawing with FinishDraw() and then update the display by calling Print(). The NULL pointer given to Print() is the area that you want to update (it is a rectangle). If null the whole window is updated.
Compile and run this example. For the first time you should see something. A solid wall!! Congratulations, you have created your first almost useful Crystal Space application :-)
void Simple::eatkeypress (int key, bool shift, bool alt, bool ctrl)
{
(void)shift; (void)alt; (void)ctrl;
float speed = .03;
switch (key)
{
case CSKEY_ESC: Shutdown = true; break;
case CSKEY_RIGHT: view->GetCamera ()->Rotate (VEC_ROT_RIGHT, speed); break;
case CSKEY_LEFT: view->GetCamera ()->Rotate (VEC_ROT_LEFT, speed); break;
case CSKEY_UP: view->GetCamera ()->Rotate (VEC_TILT_UP, speed); break;
case CSKEY_DOWN: view->GetCamera ()->Rotate (VEC_TILT_DOWN, speed); break;
}
}
That's all! With this simple change you can rotate the camera with the arrow keys. Try it out to see the effect. To rotate the camera we use Rotate() which expects a vector to rotate along and an angle given in radians (the speed parameter). There are a number of predefined vectors which you can use. Four of them are used in this example.
That's it for now. In this tutorial you learned how to setup the Crystal Space system for use. How to create a simple room with some lights and how to handle some basic camera operations.