Game Developer's Guide
Game Developer's Guide
Game Developer's Guide
AM
FL
Y
Official Butterfly.net®
Game Developer’s
Guide
Andrew Mulholland
Mulholland, Andrew.
Official Butterfly.net game developer's guide / by Andrew Mulholland.
p. cm.
Includes index.
ISBN 1-55622-044-8 (pbk.)
1. Computer games—Programming. I. Title.
QA76.76.C672M853 2004
794.8'1526—dc22 2004011572
ISBN 1-55622-044-8
10 9 8 7 6 5 4 3 2 1
0406
This book is sold as is, without warranty of any kind, either express or implied, respecting the contents of this
book and any disks or programs that may accompany it, including but not limited to implied warranties for the
book’s quality, performance, merchantability, or fitness for any particular purpose. Neither Wordware
Publishing, Inc. nor its dealers or distributors shall be liable to the purchaser or any other person or entity with
respect to any liability, loss, or damage caused or alleged to have been caused directly or indirectly by this book.
All inquiries for volume purchases of this book should be addressed to Wordware Publishing, Inc.,
at the above address. Telephone inquiries may be made by calling:
(972) 423-0090
Contents | iii
Contents
Chapter 1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
What Is Massively Multiplayer? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Shard Worlds. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
What Is the Butterfly Grid? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Key Aspects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Client Libraries. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Gateway Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Datastore/Game Server. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Technology Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
The Grid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Network Protocol Stack (NPS). . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Locale System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Dead Reckoning . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Scripting Game Logic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Summary. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407
About the Author
Andrew Mulholland has a BSc (Hons) in Computer Games
Technology and is a partner in a games development company
based in Scotland called Hunted Cow Studios Ltd.
(www.huntedcow.com). Hunted Cow’s current project is an
online gaming website called CowPlay.com, which currently
offers free multiplayer games.
2 | Chapter 1
Shard Worlds
MMORPGs usually support at least 2,000 players per server. Each player in the
game has some form, such as a character or spaceship (or both), within a persis-
tent world. The aim of these games is generally to be the best. There is no
defined end point for the game — it just goes on forever. As you progress
through the game, you get opportunities to “upgrade” your character or ship
within the game, using some form of experience system. RuneScape is one such
MMORPG, and is available in both free and subscription versions.
' http://www.runescape.com
One very long running MMORPG is Ultima Online, published by ORIGIN. Its
graphics look a little dated now, but a new version is under development that has
slick 3D graphics. A sample screen shot can be seen in Figure 1-2.
' http://www.uo.com/
Shard Worlds
Both RuneScape and Ultima Online have shard worlds. A shard world is basi-
cally a duplicate of the online world on a different server. For some games, such
as Ultima, when you create your character on a particular shard, it is fixed to
that shard and cannot be played on a different shard. For RuneScape, your char-
acter can be played on any shard, but you can only see the players that are also
logged into that shard.
Introduction | 3
What Is the Butterfly Grid?
Your question now is, presumably, why? The reason for these shard worlds is
purely because of resources. If a game server is handling above x amount of
players, the game is going to take a performance hit and reduce the experience
for all players connected to it.
So, because of this, there is not truly one world, but rather several shard
worlds that are mostly unrelated to each other.
Key Aspects
Let’s look now at the key aspects that make the Butterfly Grid tick.
Client Libraries
Also known as the Object Management System, the Client Libraries provide the
programming interface to the Butterfly Grid in the form of an API (application
programming interface). The OMS (Object Management System) provides the
interface with the server from within the game to send and receive information
(via the Butterfly Grid Network Protocol Stack — discussed later).
In simple terms, if a client’s player moves, that client will inform the OMS of
the event, i.e., where the player has moved to. The OMS will then transmit this
information to the Gateway Server (the middle tier — see below)
The OMS will be the main focus of this book as we focus on the integration
of it with the open source 3D engine Crystal Space.
Gateway Server
The Gateway Server is the middle tier of the Butterfly Grid and is used to trans-
late the data objects and communication protocols into the correct format for the
current platform. On more advanced and complex platforms, this layer has little
to no work to do. However, on more humble platforms, the Gateway can be
increasingly complex and hence cause data that is not suitable for the current
platform to be lost.
4 | Chapter 1
Technology Overview
Datastore/Game Server
Once the data objects and communication protocols have been translated by the
Gateway, they are sent to the back end for processing, which consists of the
Datastore and Game Server. This layer contains the objects that represent the
players within the world and also determines what data is relevant to which
client.
Technology Overview
Here we will look briefly at the different areas of the Butterfly Grid that contrib-
ute to the overall technology.
The Grid
The Butterfly Grid provides the means to have a single world without any
shards, giving the player the feeling of a seamless world, which is currently
uncommon — if even in existence — with current technology. So why can the
Butterfly Grid handle an unlimited number of players within one game? The
answer is simple — behind the scenes, there are multiple servers, in the form of
a fully meshed server grid, handling all the connected players. So when a player
connects, he or she is in fact connecting to a Gateway Server, which translates
the incoming and outgoing data for that client and sends the messages to the
back end servers.
In the past, each shard world (A though D) would have, for example, 2,000
players and be completely unconnected to the others as shown in Figure 1-3.
So what this means is that sections of the game world are handled by differ-
ent servers. This will be transparent to the end user and also to the developer
once the system is in place.
As the server is in fact a collection of meshed servers, you can think of each
individual server on the network as a node and the network as being the server.
Y
Locale System
FL
A Locale within the Butterfly Grid is a convex area within the game world,
AM
defined as a convex polygon within the game database, that is then assigned to
the servers within a server configuration file. This is basically what we saw
before. We would define each of the shards A through D in Figure 1-4 as a
Locale, as each can contain players; then we could assign each of the shards to a
TE
different server. For example, if we felt that shards A and B would be much less
populated than C and D, we could combine A and B into a single locale and
assign them to a single server, maintaining C and D each on their own servers.
Note that a player must always be located within a Locale.
Dead Reckoning
The dead reckoning system within the Butterfly Grid is used simply to reduce
the amount of bandwidth consumed for transmitting state information between
players. Each player has his or her own state (position, etc.) recorded and also
other nearby players’ perception of the player’s state. If other players’ percep-
tion of the player is off by much, the player’s state is transmitted to the
applicable players. Again, all this is handled internally on the server side and
also the client side (via the OMS).
' http://www.butterflyguide.net
Team-Fly®
6 | Chapter 1
Summary
Summary
If the advantages of using the Butterfly Grid are not yet clear, hopefully they
will be by the end of this book after you have performed your first integration of
the OMS and started your own experiments using the Butterfly Grid technology.
In the next chapter, we will look at how to set up a Butterfly.net account and
how to access it via SSH and the web. Then we will look at the basics of the
Crystal Space 3D engine. Once we have mastered that, we will look at integrat-
ing the OMS with the 3D engine, and finally we will move on to developing a
mini-game project from scratch.
Chapter 2
First Steps
Introduction
Before you can actually use the Butterfly Grid, you’ll need to create an account
with Butterfly.net, Inc. To do this, visit the following web site and fill in your
details.
' http://www.butterfly.net/contact/registerform.html
Ü hear
Important When signing up, be sure to select “Other” in the “How did you
about us?” field and note in the Comments box below that you purchased
this book.
One of the Butterfly.net, Inc. sales representatives will contact you within 14
days and help you though the signup process.
7
8 | Chapter 2
Accessing Your Account via the Internet
Once logged in, using the username and password boxes at the top right, you
will see a browser page similar to the following.
As you can see from the preceding screen shot, the splash page gives you links
to your own project and also any projects you’re currently watching. From here,
click on your own project, which will take you to a screen similar to Figure 2.2.
On the project screen, you have access to the following tools:
n Membership — The membership section allows you to invite and add new
members to your project, as well as assign roles to the new members.
n Mailing lists — The mailing lists section does just what it says — it allows
you to create and manage project mailing lists for different aspects of the
project.
n Source code — The source code section lets you view your current CVS of
the project and also gives the access details for the project. Note there is a
short tutorial in the next chapter on using CVS.
n Issue tracking — This section provides a bug and feature tracking mecha-
nism for your project. You can also assign bugs and features to specific
members of the group and leave details regarding them. Highly useful!
First Steps | 9
Accessing Your Account via SSH
n File sharing — Allows the upload of files to a shared folder. Good for plac-
ing coding styles, design documents, etc., that do not readily fit into the
CVS.
n News — Allows the posting of project news.
n Discussion forums — Standard forum system that can be useful for discus-
sion of project ideas and features before they are committed.
' http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html
The download you want is the standard PuTTY program, putty.exe. Once down-
loaded, run the executable and you will be presented with the configuration
screen. Here you need to first enter the host name of your Butterfly account.
Ours is “wordware.butterfly.net” so we’ve entered that. Then you need to ensure
the SSH radio button is selected.
10 | Chapter 2
Accessing Your Account via SSH
After this, click the Open button on the bottom right of the configuration screen.
A connection will then be established to your Butterfly.net account and you will
be asked for your login and password details. Note that these are not the same as
your lab username and password. You will need to obtain them from the Butter-
fly Technical Support team. The login request screen can be seen in the
following figure:
Once you have entered your correct credentials, the screen will look as follows:
If you now perform the ls command, you will see there are three folders and a
sql log file.
Within the schema folder you will find a SQL file, which is the file used to cre-
ate the datastore for your game. In our case, this file is called wordware.sql, but
it will vary depending on the name of your project. Note that the Oracle data-
base must be recreated with any changes before they will become live within the
datastore. For more information on the database schema, please refer to the
Manifesto provided on the CD-ROM. Note also that we will be looking at a
recently implemented XML format for creating the schema in the final chapter
of this book.
Summary
In this chapter, we looked at the basics of connecting to your new Butterfly
account via the web and SSH. In the next chapter we get into the code by start-
ing to look at the basics of the Crystal Space engine in preparation for our OMS
integration.
Chapter 3
Introduction
In this chapter, you will gain a basic knowledge of how to set up and get started
with the Crystal Space 3D engine. The reason we are going to be using Crystal
Space is because it integrates well with the OMS (Object Management System)
and it is free (which is always a good thing!).
' http://crystal.sourceforge.net/cvs-snapshots/zip/
However, it is recommended that you use a CVS client to ensure you always
have the latest version.
Getting WinCVS
To retrieve the latest snapshot of the Crystal Space source code, we need to set
up a CVS client to connect to the CVS server. Therefore, the first step is to
download and install a suitable client. In this book, we use the WinCVS client,
which is available from the following web link (and is also available on the
companion CD-ROM).
' http://www.collab.net/developers/tools/
Ü you
NOTE There are other tools available from the above link. While it is up to
to decide which one to use, we will only be covering the use of WinCVS here.
13
14 | Chapter 3
Using the CVS Client
Once you download the zip file (called WinCvs120.zip), simply extract it to a
temporary directory and run setup.exe, following the standard installation.
Configuring WinCVS
Before you run WinCVS, you need to create a directory to store the administra-
tive files that WinCVS will generate. To do this simply create a folder called
cvshome on your c:\ drive:
c:\cvshome
Once this is done, run the WinCVS client. When it starts up, you should see the
following preferences window:
If this screen is not visible when you run WinCVS (i.e., it is not the first time
you have run it), simply go to the Admin menu and select the Preferences…
option.
For the CVSROOT, we need to enter the command to connect to the Crystal
Space CVS server, which is:
:pserver:anonymous@cvs.crystal.sourceforge.net:/cvsroot/crystal
Then from the Authentication pull-down list, we need to select the “passwd” file
on cvs server option.
Once this is done, the preferences should look as follows:
Getting Started with Crystal Space | 15
Using the CVS Client
Y
FL
Figure 3-2: The WinCvs Preferences window – updated General tab
AM
Next, click on the Globals tab. This will display the following options by
default.
TE
Here we need to deselect the “Checkout read-only” option and select the “Use
TCP/IP compression” option. Also, set the compression level to 4 instead of 9.
We do this to make the files readable and writeable once downloaded and to
increase the speed at which the data transfers, respectively.
Once this is done, the preferences should look as follows:
Team-Fly®
16 | Chapter 3
Using the CVS Client
Next, we need to set the location where the administrative information for
WinCVS should be saved. Click on the rightmost tab, WinCvs. All we need to
do here is type in (or browse for) our c:\cvshome directory, which we created
earlier, into the HOME folder field. Once this is done, the window should look
as follows:
Now simply click OK. Providing all the steps have been done correctly, the out-
put area of the WinCVS client should look like the following figure.
Getting Started with Crystal Space | 17
Using the CVS Client
As we’re not actually going to be making any changes to the source code on the
server, only retrieving it, no password is required. Simply click the OK button
without entering a password. Once you do this the output area of WinCVS
should display the following:
cvs -z4 login
(Logging in to anonymous@cvs.crystal.sourceforge.net)
Once we are connected, we need somewhere for Crystal Space to go, so let’s
now create a folder called crystal on the c:\ drive:
c:\crystal
Now that we have somewhere for Crystal Space to go, we need to point
WinCVS to that directory. This is accomplished by clicking on View in the main
menu, then selecting the Browse Location option and finally the Change…
option. A dialog will then appear, allowing you to browse for a folder. In this
dialog, simply select your newly created crystal directory on the c:\ drive and
click the OK button.
Once this is done, you should see the crystal folder displayed in the
workspace area to the left, as shown in the following screen shot.
18 | Chapter 3
Using the CVS Client
The next stage is to actually start the transfer from the CVS repository. To do
this, right-click on the crystal folder shown in the left-hand window and then
select the Checkout module… option. Once this is done, the following window
will be visible:
The module name we need to enter in the top text field is “crystal.” Once you
enter this click on the Checkout options tab. All the check boxes on this page
should initially be blank. The only one we need to check here is “If no matching
revision is found, use the most recent one.” And that’s it! Ensure you have an
active connection to the Internet and click the OK button.
You should now start seeing some action in the bottom output area. Note that
this process will take a while (depending on your Internet connection), as, at the
time of writing, the source is around 20MB for Crystal Space.
Once the process is completed, if you expand the crystal folder, followed by
the newly created cs folder, you will notice the workspace window (left-hand
window) now contains several folders.
On this window, first check the “Create missing directories that exist in reposi-
tory” option and then click on the Sticky options tab. In the Sticky options tab
all you need to do is check the “If no matching revision is found, use the most
recent one” option. Then simply click OK and WinCVS will update your local
version of the Crystal Space source code with any applicable changes in the
remote CVS repository.
' http://crystal.sourceforge.net/
So where do we start? The first step is to open up Visual Studio 6 and select
File, then Open Workspace…. Next, use the browse dialog to go to the follow-
ing folder:
c:\crystal\CS\mk\visualc
Open this workspace now, as it contains all the projects for the libraries,
plug-ins, and examples that are included in the Crystal Space package.
Before we attempt to compile anything though, there are a few prerequisites.
The first is the zip file that contains all the third-party libraries required by
Crystal Space. This zip file is available on the CD-ROM (cs_current_snap-
shot.tar.bz2) and from the following link.
' ftp://ftp.sunsite.dk/projects/crystal/support/win32/msvc_libs_0.96.zip
Once you have acquired the additional libraries, all you need to do is extract it to
the root Crystal Space directory, which is the following if you are using the sug-
gested names in the book.
c:\crystal\CS
You will also need to install the DirectX 9 SDK, which is available from the fol-
lowing link and on the CD-ROM.
' http://www.msdn.microsoft.com/directx
After this is done, we can compile Crystal Space. However, there are many extra
plug-ins in there that we will not be using, some of which require extra libraries,
etc., that need to be downloaded and installed first. We will unload the following
projects from the workspace before we attempt to compile it:
appcaltocs
appzoo
plgcspython
plgfreefnt2
plgiso
To unload the projects, first switch the workspace to show the FileView of all
the projects by clicking the tab at the bottom of the workspace panel.
22 | Chapter 3
Compiling Crystal Space
Once on this view, simply right-click the project you wish to unload and select
the Unload Project option from the pop-up menu. For example, when you do
this to the plgcspython project, it should then read “plgcspython – not loaded”
and have a grayed-out folder to the left of it.
Once we have unloaded the five projects that are not required, we need to set
the active project to be grpall, as when we compile this it will compile all the
projects in the Crystal Space workspace. To set it as the active project, simply
right-click on the grpall files in the list and then select Set as Active Project
from the pop-up menu.
Next, we have to set the active configuration for the project correctly. This is
done by selecting Build from the main menu, followed by the Set Active Con-
figuration… option. Once this is selected, a dialog will appear on which we
want to select the grpall - Win32 Debug configuration.
We’re ready to compile now, so click on the Build option from the main menu,
and then select the Build option. Then comes the long wait while Visual Studio
compiles all the projects (depending on the speed of your computer, of course).
Depending on the current version you downloaded from the CVS, you may
get some warning messages but as long as there were no errors, everything
should be fine.
The first of the four is the include directory of the DirectX SDK and may look
slightly different, depending upon which version you installed. However, the
following three are standard and contain the standard include files for Visual
Studio.
What we need to do here is add the include directory for Crystal Space to this
list, so add the following directory to the bottom of the list:
c:\crystal\CS\INCLUDE
Next, we need to set the directory where the static libraries for CS can be found.
To do this, change the Show directories for pull-down to “Library files.” The list
should now contain something similar to the following:
Getting Started with Crystal Space | 25
Creating Your Own Project
c:\MSSDK\LIB
c:\Program Files\Microsoft Visual Studio\VC98\LIB
c:\Program Files\Microsoft Visual Studio\VC98\MFC\LIB
You may at first think the obvious directory to add would be c:\crystal\CS\libs;
however, this directory only contains the source code for the libraries. The actual
directory we need to add here is the following:
c:\crystal\CS\MK\VISUALC\CSDEBUG\BIN\LIBS
After we have added this, simply click OK. Note that this step to set the directo-
ries will only ever need to be done once, as the settings are global to all projects
created in the Visual Studio IDE.
Y
Creating and Setting Up the Project
FL
Now we are ready to create our actual project. To do this, first open up the
csall.dsw workspace located in the c:\crystal\CS\mk\visualc folder (just as we
did at the start of the chapter when we were compiling the Crystal Space
AM
engine).
The easiest and best way to create your project is to include it in the Crystal
Space workspace, so once the Crystal Space workspace is loaded, click File
from the main menu and select New….
TE
Next, select the Projects tab and then click on Win32 Application in the list to
the left. We now need to give the project a name, so enter appbutterfly in the
Project name field.
Now set the Location field to read:
c:\crystal\CS\mk\visualc
This will ensure that our project file will be kept along with all the others in the
Crystal Space workspace.
Then we want to select the Add to current workspace option (as opposed to
the Create new workspace option). Once these steps are complete, the window
should look as follows:
Next, click the OK button; the following screen should now be visible:
On this window, ensure that the “An empty project” option is selected and then
click the Finish button.
A New Project Information window will then appear. Simply click OK on
this and your new project will be created for you.
In the workspace FileView to the left, you should now see your new
appbutterfly project listed and it should contain three folders — Source Files,
Header Files, and Resource Files.
At the moment it is just a standard project, although there are many settings for
the project we need to change to enable it to work correctly with Crystal Space.
We’ll go through these settings one at a time. Be sure not to miss any as one lit-
tle mistake can cause you a world of pain (as I found out the hard way).
Getting Started with Crystal Space | 27
Creating Your Own Project
The first thing to do is right-click on the appbutterfly files text and then click
on the Settings… option that appears in the right-click pop-up. Once you click
on this, the Project Settings window will become visible.
First, ensure that the Settings For pull-down is set to “Win32 Debug” and
also that you are looking at the General tab on the right-hand side.
In the General tab, check that the Microsoft Foundation Classes pull-down is
set to “Not Using MFC.” For the Intermediate files field we want to enter the
following to replace what is currently there:
csdebug\temp\appbutterfly
In addition, we want to enter this into the Output files field to replace the exist-
ing entry.
Once we have done this, the General tab should look as follows.
Next, click on the Debug tab. All we need to do on this tab is set the Working
directory field to:
c:\crystal\CS
This will enable us to execute the compiled applications from within Visual Stu-
dio as they need to be executed from the root directory of Crystal Space, simply
because the root folder contains all the DLLs for the plug-ins, etc. Once we
make this change, the Debug tab settings should look as follows:
28 | Chapter 3
Creating Your Own Project
Next, click on the C/C++ tab and ensure that the Category pull-down is set to
“General.” Here, we need to change the Preprocessor definitions field by delet-
ing the current contents and replacing it with the following:
_DEBUG,_MT,WIN32,_CONSOLE,_MBCS,WIN32_VOLATILE,__CRYSTAL_SPACE__,CS_DEBUG,
CS_STRICT_SMART_POINTERS
As you can see from the preceding screen shot, the Project Options text area is
automatically updated with the modified preprocessor definitions, so there are
no manual changes necessary.
Getting Started with Crystal Space | 29
Creating Your Own Project
Now change the Category pull-down to the “Precompiled Headers” option and
then select the “Not using precompiled headers” option that is now visible.
..\..\plugins,..\..,..\..\include\cssys\win32,..\..\include,..\..\libs,
..\..\support,..\..\apps
That concludes all the changes to the C/C++ tab, so now click on the Link tab
and ensure that the Category pull-down in this tab is set to the “General” option.
Here we first want to delete the contents of the current Output file name field
and replace it with the following:
csdebug\temp\appbutterfly\butterfly.exe
Then we want to delete everything from the Object/library modules field and
instead enter the following libraries (as this is all we require of the standard
libraries):
shell32.lib gdi32.lib user32.lib advapi32.lib
Again, once this is done the Project Options text area at the bottom will be
updated automatically. This can be seen in the following screen shot.
Getting Started with Crystal Space | 31
Creating Your Own Project
Next, we need to change the Category to the “Input” option. All we need to do
here is add the following to the Additional library path field:
..\..\libs\cssys\win32\libs
We are finished with the Link tab, so click on the Resources tab. Here we need
to first set the Resource file name field to the following:
.\csdebug\temp\appbutterfly\appbutterfly.res
Then we need to set the Additional resource include directories field to read as
follows:
..\..\include\cssys\win32;..\..\include
32 | Chapter 3
Creating Your Own Project
Once you have followed these steps, the Resources tab should look like this:
Nearly there! All we need to do now is go to the last tab in the list, called
Post-build step, and add the following four post-build commands:
echo Moving output to CS root.
copy "$(TargetPath)" ..\..
echo Moving output to MSVC Debug Bin.
copy "$(TargetPath)" csdebug\bin
All this does is move the executable to the root Crystal Space folder (where all
the DLLs are) and the csdebug\bin directory once your application is compiled.
Once this is done correctly, the window should look as follows:
Once this is done, simply click OK. Then, close all the document windows that
are open within Visual Studio, and drag the appbutterfly.rc file from the Source
files folder into the Resource files folder.
34 | Chapter 3
Creating Your Own Project
Next, we’re going to add the relevant information to the resource file. To do this,
first select the ResourceView tab at the bottom of the workspace view.
Once in this view, right-click on appbutterfly resources and then click the
Insert… option from the pop-up menu.
When this is done, you will be presented with the Insert Resource dialog. Here
you should select the Version option from the left-hand side and then simply
click the New button.
Y
FL
AM
Figure 3-34: Adding a “Version” resource
Team-Fly®
36 | Chapter 3
Creating Your Own Project
As you can see from the preceding figure, I have filled in some sample informa-
tion; of course, it is up to you what you want to put here. Once you have
finished editing this information, save it, close it, and return to the FileView tab
of the workspace.
Next, go back to Visual Studio and click File, followed by New… and this time,
select C++ Source File from the left-hand list and enter the name of this file as
butterfly in the File name field on the right-hand side. Then, before clicking OK,
change the location to be the folder we just created:
c:\crystal\CS\apps\butterfly
Click OK and you will notice that our source file has been added into the Source
Files folder within the appbutterfly project in the workspace pane.
Getting Started with Crystal Space | 37
Creating Your Own Project
We then want to repeat the process for adding the C/C++ Header File, which
will also be called butterfly and located in the folder we just created.
Once these steps are completed, our project should now look as follows:
Figure 3-37: Project complete with source, header, and resource files
The final step before we actually see some code is to add the dependencies to
the project. To do this, click on Project in the main menu and then select the
Dependencies… option.
The Project Dependencies window will appear. Here you want to check all
the Crystal Space library files, which are as follows:
libcsengine
libcsgeom
libcsgfx
libcssys
libcstool
libcsutil
libcsws
So, for reference, the window should now look similar to the following.
If you now click the OK button, you should see that they have been added as
what look similar to links under the Resource Files folder.
Now we are ready for the code. What we are going to do is look at the complete
source code for the example, compile it and run it, then look in detail at the code
to see how it all works.
Following are the complete listings for the butterfly.cpp and butterfly.h files.
Listing 3-1: butterfly.cpp
#include "cssysdef.h"
#include "cssys/sysfunc.h"
#include "iutil/vfs.h"
#include "csutil/cscolor.h"
#include "cstool/csview.h"
#include "cstool/initapp.h"
#include "cstool/cspixmap.h"
#include "butterfly.h"
#include "iutil/eventq.h"
#include "iutil/event.h"
#include "iutil/objreg.h"
#include "iutil/csinput.h"
#include "iutil/virtclk.h"
#include "iengine/sector.h"
#include "iengine/engine.h"
#include "iengine/camera.h"
#include "iengine/light.h"
#include "iengine/statlght.h"
#include "iengine/texture.h"
#include "iengine/mesh.h"
Getting Started with Crystal Space | 39
Creating Your Own Project
#include "iengine/movable.h"
#include "iengine/material.h"
#include "imesh/thing/polygon.h"
#include "imesh/thing/thing.h"
#include "imesh/object.h"
#include "ivideo/graph3d.h"
#include "ivideo/graph2d.h"
#include "ivideo/txtmgr.h"
#include "ivideo/texture.h"
#include "ivideo/material.h"
#include "ivideo/fontserv.h"
#include "igraphic/image.h"
#include "igraphic/imageio.h"
#include "imap/parser.h"
#include "ivaria/reporter.h"
#include "ivaria/stdrep.h"
#include "csutil/cmdhelp.h"
CS_IMPLEMENT_APPLICATION
// Application Specific...
csPixmap* logoImg;
int logo_x, logo_y;
csRef<iFont> font;
// [END] Application Specific
Butterfly::~Butterfly ()
{
}
bool Butterfly::LoadPixMaps ()
{
return true;
}
40 | Chapter 3
Creating Your Own Project
void Butterfly::SetupFrame ()
{
// Check input...
if (kbd->GetKeyState (CSKEY_LEFT))
{
if(logo_x > 1)
logo_x-=2;
}
if (kbd->GetKeyState (CSKEY_RIGHT))
{
if(logo_x < 639-logoImg->Width())
logo_x+=2;
}
if (kbd->GetKeyState (CSKEY_UP))
{
if(logo_y > 1)
logo_y-=2;
}
if (kbd->GetKeyState (CSKEY_DOWN))
{
if(logo_y < 479-logoImg->Height())
logo_y+=2;
}
// Begin 2D rendering...
if (!g2d->BeginDraw ())
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Failed to begin 2D frame");
return;
}
g2d->Clear(0);
if(logoImg)
{
logoImg->DrawScaled (g3d, logo_x, logo_y, logoImg->Width(),
logoImg->Height());
}
// draw text...
int fntcol = g2d->FindRGB (255, 255, 0);
char buf[256];
sprintf(buf, "Butterfly Grid");
g2d->Write(font, 10,10, fntcol, -1, buf);
Getting Started with Crystal Space | 41
Creating Your Own Project
void Butterfly::FinishFrame ()
{
g2d->FinishDraw ();
g2d->Print (NULL);
}
return false;
}
bool Butterfly::Initialize ()
{
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))
{
42 | Chapter 3
Creating Your Own Project
// 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.butterfly",
"Error opening system!");
return false;
}
font = g2d->GetFontServer()->LoadFont(CSFONT_LARGE);
return true;
}
void Butterfly::Start ()
{
csDefaultRunLoop (object_reg);
}
font = NULL;
csInitializer::DestroyApplication (object_reg);
return 0;
}
#include <stdarg.h>
#include "csutil/ref.h"
struct iObjectRegistry;
struct iEngine;
struct iLoader;
struct iGraphics2D;
struct iGraphics3D;
struct iKeyboardDriver;
struct iVirtualClock;
struct iEvent;
struct iView;
struct iTextureManager;
class Butterfly
{
private:
iObjectRegistry* object_reg;
csRef<iEngine> engine;
csRef<iLoader> loader;
csRef<iGraphics2D> g2d;
csRef<iGraphics3D> g3d;
csRef<iKeyboardDriver> kbd;
csRef<iVirtualClock> vc;
csRef<iView> view;
csRef<iTextureManager> txtmgr;
#endif // __BUTTERFLY_H__
Yes, it is quite a lot of code, but 99% of it is for setting up Crystal Space. This
will compile now but before you execute it, you will need to copy butterfly.zip
from the companion CD into the following folder (don’t worry — I’ll explain
later what it is and how it works):
Y
c:\crystal\CS\data
FL
Additionally, you will need to add the following two lines to the vfs.cfg file,
located in the root Crystal Space folder (c:\crystal\CS).
AM
; Added for Butterfly
VFS.Mount.lib/butterfly = $@data$/butterfly.zip
Once this is done, you can either execute it from the root Crystal Space directory
(c:\crystal\CS\butterfly.exe) or simply run it from Visual Studio. Either way, you
TE
should get something that looks similar to the following on the screen.
Try moving the logo around with the cursor keys and note how the position of
the logo is updated in the top left corner of the screen.
Team-Fly®
46 | Chapter 3
Creating Your Own Project
It’s time now to look into the code, so we will start by looking at the contents
of the header file, butterfly.h.
First, we include stdarg.h, which as you probably know contains the macros
(such as va_start) to access arguments of functions that have an undefined
amount of parameters. Then we include the ref.h header, which is contained
within the csutil folder. This can be seen here:
#include <stdarg.h>
#include "csutil/ref.h"
The reason for the ref.h file is so we can use a feature that is relatively new to
Crystal Space (since version 0.95) called “smart pointers.”
Before the advent of smart pointers, it was your responsibility to ensure the
internal reference counting was accurate. This was achieved by means of meth-
ods called IncRef and DecRef (if you have previous experience with DirectX,
you may be familiar with this type of system when working with COM objects).
So, in previous versions, to create a reference to a graphics object you would
have had to write something similar to this for the definition of your class:
class MainClass
{
iGraphics3D g3d;
}
MainClass::MainClass()
{
// constructor…
g3d = NULL;
}
MainClass::~ MainClass ()
{
// destructor…
if (engine) engine->DecRef ();
}
Then when you came to acquire the iGraphics3D interface, you would have used
the following code:
g3d = CS_QUERY_REGISTRY (object_reg, iGraphics3D);
if (g3d == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iGraphics3D plugin!");
return false;
}
The key points to note here are the initial setting of the g3d variable to NULL in
the constructor and the call to the DecRef method in the destructor.
When using smart pointers, this is not required. Instead of declaring the g3d
object in our class like this:
iGraphics3D g3d;
Getting Started with Crystal Space | 47
Creating Your Own Project
Doing it this way means there is no need to initially set it to NULL and there is
no need to call the DecRef method when cleaning up, as this is handled automat-
ically when the class that contains it is deleted. Note also here that it is possible
to use a smart pointer just as you would an ordinary pointer, in that the code to
actually acquire the iGraphics3D interface would still be the same as before.
Don’t be too concerned about smart pointers; just be aware of them as they
do make life a lot easier, especially when your application has multiple exit
points.
So back to the code — after the include files, we then declare all the struc-
tures we will be using in our class and main code using the following lines:
struct iObjectRegistry;
struct iEngine;
struct iLoader;
struct iGraphics2D;
struct iGraphics3D;
struct iKeyboardDriver;
struct iVirtualClock;
struct iEvent;
struct iView;
struct iTextureManager;
The first is the iObjectRegistry, which acts as a registry for all other objects (and
plug-ins) within your applications. We will see how this is created and used
when we look at the butterfly.cpp source file.
Next, we have the iEngine interface, which is used to control the 3D engine
(although we will not be making use of this interface in this example as we will
be sticking to 2D for the moment).
Then we have the iLoader interface, which is used to handle the loading of
maps, textures, images, and sounds within a Crystal Space application.
The iGraphics2D and iGraphics3D interfaces handle all 2D and 3D rendering
as we will see later in this section.
The iKeyboardDriver, not surprisingly, handles keyboard input or, more accu-
rately, places keyboard events into the event queue that again we will see later in
this section.
Next, we have the iVirtualClock interface, which provides a high-resolution
virtual game clock for use within your application. (We will not directly use this
in this example, but we will in future examples in this book.)
Then we have iEvent, which is an interface used to handle all hardware and
software events, such as the mouse and keyboard input.
The iView interface is not used in this example. However, it is the top-level
rendering interface and allows access to facilities such as the camera and 2D
screen-clipping rectangle.
Finally, we have the iTextureManager, which is used to prepare all the images
loaded into the correct format for the 3D renderer and similar tasks, such as
mipmap generation.
48 | Chapter 3
Creating Your Own Project
Now that we have defined the required structures, we then start the definition
of our main class, which in this example is called Butterfly. In the class defini-
tion, we first create smart pointer references for all the interfaces we just
defined, with the exception of iObjectRegistry, which works a little different.
The start of the class definition can be seen here:
class Butterfly
{
private:
iObjectRegistry* object_reg;
csRef<iEngine> engine;
csRef<iLoader> loader;
csRef<iGraphics2D> g2d;
csRef<iGraphics3D> g3d;
csRef<iKeyboardDriver> kbd;
csRef<iVirtualClock> vc;
csRef<iView> view;
csRef<iTextureManager> txtmgr;
Next, we add two function prototypes that will be implemented to provide event
handling within our application. Note the first is a static method called
SimpleEventHandler, as the Crystal Space event handler expects to call a C-style
function. This function is then used to call our non-static HandleEvent method.
These two prototypes can be seen in the following two lines of code:
static bool SimpleEventHandler (iEvent& ev);
bool HandleEvent (iEvent& ev);
We then define another two non-static methods that will be called directly before
and after the rendering process. These methods are called SetupFrame and
FinishFrame and can be seen in the following two lines of code:
void SetupFrame ();
void FinishFrame ();
The final two private methods of our application class are LoadPixMaps and
DrawFrame2D. The first is used to load in the 2D image (known as a pixmap in
Crystal Space) of the Butterfly Grid logo that we are going to move around the
screen and the second is used to actually render it to the screen. The prototypes
for these methods can be seen here:
bool LoadPixMaps ();
void DrawFrame2D ();
Now that we have prototyped the private methods, let’s look at the public meth-
ods. First, we have the constructor and destructor methods, which are defined as
follows:
public:
Butterfly(iObjectRegistry* object_reg);
~Butterfly();
bool Initialize();
void Start();
After we have defined that this is a Crystal Space application, we then add some
variables specific to this example. The first is a pointer to a csPixmap class
called logoImg. As briefly mentioned before, the csPixmap class contains useful
inline methods for dealing with simple 2D sprites. We will be using this logoImg
pixmap to hold the Butterfly Grid logo. Then we declare integer variables to
hold the current x, y position of the logo on the screen. This can be seen here:
int logo_x, logo_y;
Finally, we declare a smart pointer reference to the iFont interface called font,
which we will use when we want to render text to the screen. Here is the decla-
ration of the font variable:
csRef<iFont> font;
Let’s now jump to the application’s entry-point (i.e., the main method) and see
how a standard Crystal Space application flows.
We start with a standard main method, which takes the usual argv and argc
parameters:
50 | Chapter 3
Creating Your Own Project
The first step is to create the environment by calling the static CreateEnviron-
ment method of the csInitializer class, which does all the required initial setup of
the Crystal Space engine and then returns a pointer to the object registry for our
application, which we store in a temporary variable called object_reg. This can
be seen here:
iObjectRegistry* object_reg = csInitializer::CreateEnvironment (argc, argv);
Once we have the environment set up, we then proceed by creating an instance
of our application class Butterfly by passing the iObjectRegistry pointer we
obtained from the CreateEnviroment method into the constructor of our class:
butterfly = new Butterfly (object_reg);
As you can see, all we are doing here is storing a local copy of the passed-in
object registry pointer in our private class member object_reg.
So after our butterfly object is created, we call the Initialize method as
follows:
if(butterfly->Initialize())
Let’s now take a look into the Initialize method that we have defined.
The first thing we do in the Initialize method is make a call to the
RequestPlugins method, which is a static member of the csInitializer class. What
this method actually does is load in the most standard Crystal Space plug-ins,
read in the standard configuration file and command line, and load in any
plug-ins specified in the argument list. Each plug-in required must be specified
by its name, Shared Class Facility ID number, and its version number. However,
there are useful macros that allow this to be done in a very neat fashion.
Before we look at the plug-ins we have loaded in, now is really a good time
to take a step to the side and find out about the Shared Class Facility.
Getting Started with Crystal Space | 51
Creating Your Own Project
This would be saved into a header file named ialien.h. Next, we would
create an implementation for this structure (interface) as follows:
#include "ialien.h"
public:
virtual void SetPhrase(char *phrase);
virtual void Speak();
}
void Alien::Speak()
{
printf("%s", phrase);
}
We now have our implementation of the iAlien interface, but this will
never be included in any application; only the iAlien interface (structure)
will be included. In this interface, we also need to provide a static method
to create a new Alien object. This is commonly known as the class factory
and would look similar to the following:
static iAlien* Create_iAlien()
{
return new Alien();
}
52 | Chapter 3
Creating Your Own Project
LoadLibrary("alienlibrary.dll");
iAlien (*Create_iAlien)() = GetLibrarySymbol("Create_iAlien");
iAlien* myAlien = Create_iAlien();
myAlien->SetPhrase("Hi! I’m an alien");
myAlien->Speak();
There is too much information on the Shared Class Facility to cover in this
book, but if you refer to the section on it in the Crystal Space documenta-
tion, there is much more detail on its inner workings. However, as I
mentioned before, unless you intend to contribute to the Crystal Space
project, there is no need to look into it in detail.
Now let’s go back to the code where we left off. For the RequestPlugins method,
we first pass it our object_reg pointer, followed by a list of plug-ins that we
require for our application. The plug-in macros are as follows:
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
The first, CS_REQUEST_VFS, requests the Virtual File System (VFS) plug-in,
which we will be using to load in our image (and in later examples to load in a
3D level). We will look into the VFS in detail later in this section.
Next, we have CS_REQUEST_SOFTWARE3D, which is the software ren-
dering plug-in.
The CS_REQUEST_ENGINE requests the core Crystal Space engine (but is
not actually used in this example).
Then we have CS_REQUEST_FONTSERVER, which allows us to access
fonts from within the application.
Next up we request the CS_REQUEST_IMAGELOADER and CS_RE-
QUEST_LEVELLOADER plug-ins, which provide functionality to load in
images and levels (pretty obvious really ;)).
Then finally we have CS_REQUEST_REPORTER and CS_REQUEST_
REPORTERLISTENER, which allow us to report error and information mes-
sages to the console window with ease.
Getting Started with Crystal Space | 53
Creating Your Own Project
Note that the final macro in the list must always be CS_REQUEST_END;
otherwise, this method will break.
If this method fails, we can report the error by calling the csReport macro
(defined in the header file for the csReporterHeader class), passing in the object
registry followed by the severity of the report, using one of the following
macros:
CS_REPORTER_SEVERITY_BUG
CS_REPORTER_SEVERITY_ERROR
CS_REPORTER_SEVERITY_WARNING
CS_REPORTER_SEVERITY_NOTIFY
CS_REPORTER_SEVERITY_DEBUG
This is followed by a message ID in the next parameter (i.e., where the message
originated from) and finally we pass in a description of what needs to be
reported. In this case, we call the macro as follows:
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Can't initialize plugins!");
return false;
}
This means it’s an error that occurred within the butterfly application stating that
the plug-ins could not be initialized. If we force this to occur by making the if
statement fail, you should see something similar to the following screen shot:
Next, we attempt to obtain a pointer to the virtual clock. This is done by means
of the CS_QUERY_REGISTRY macro by passing in the object registry
(object_reg) and the interface we wish to obtain a pointer to. So for the virtual
clock we use the following code:
vc = CS_QUERY_REGISTRY (object_reg, iVirtualClock);
if (vc == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Can't find the virtual clock!");
return false;
}
Note that because we are using smart pointers, we do not have to worry if it
failed, as the references are automatically decremented (by means of the DecRef
method).
After we have obtained the virtual clock, we attempt to acquire pointers to
the rest of the modules by using a similar technique.
Once we have acquired pointers to all that we require, we call the static
OpenApplication method, which is a member of the csInitializer class, passing
in the pointer to our object registry. This is done with the following segment of
code:
if (!csInitializer::OpenApplication (object_reg))
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Error opening system!");
Getting Started with Crystal Space | 55
Creating Your Own Project
return false;
}
Then, the next step is to acquire a pointer to the texture manager, which we will
be using in a moment to load in our image. We can do this by calling the
GetTextureManager method of the iGraphics3D interface, which we have an
object of as a member of our class called g3d. This can be seen here:
txtmgr = g3d->GetTextureManager ();
Next, we make a call to our LoadPixMaps method, which will be used to load in
our Butterfly Grid logo. Let’s take a look at this method now.
In this method, we first call the LoadImage method of the loader object,
which we acquired a pointer to in the Initialize method. This method returns a
Y
pointer to an iImage interface that we create a local smart pointer reference to
called ifile. This can be seen here:
FL
bool Butterfly::LoadPixMaps ()
{
AM
csRef<iImage> ifile = loader->LoadImage ("/lib/butterfly/logo.jpg");
Here is the important part that confused me at first, although it is actually really
simple. Notice the path of the file we have specified here — /lib/butterfly/
TE
logo.jpg.
What we are actually doing here is accessing the image from the Virtual File
System. The actual logo.jpg is stored in a zip file called butterfly.zip, which we
have placed in the c:\crystal\CS\data directory. The reason it can find the file
with the path specified is because we have made /lib/butterfly/ a mount point for
the Virtual File System.
If you remember earlier in this chapter we appended the vfs.cfg file (located
in the root CS directory) with the following two lines:
; Added for Butterfly
VFS.Mount.lib/butterfly = $@data$/butterfly.zip
As you probably guessed, the first line is simply a comment. However, the sec-
ond line makes it so that our butterfly.zip file is virtually mounted under a
lib/butterfly folder, meaning we can access our compressed data file as easily as
we would a normal directory.
So, back to the code now. Once we have loaded in our image, we register it as
a 2D texture by calling the RegisterTexture method of the texture manager,
which we previously acquired in the Initialize method:
csRef<iTextureHandle> txt = txtmgr->RegisterTexture (ifile, CS_TEXTURE_2D);
As you can see, this method returns our texture as an iTextureHandle interface.
Once we have this, we call the Prepare method as follows:
txt->Prepare();
And finally we can create our actual pixmap by passing this iTextureHandle, txt,
into the constructor of the csSimplePixmap class:
logoImg = new csSimplePixmap (txt);
Team-Fly®
56 | Chapter 3
Creating Your Own Project
So, after the Initialize method finished successfully (i.e., returns true), the Start
method of our Butterfly class is called. Let’s look at the Start method now.
Well, there isn’t much to look at actually as there is only one single line. The
entire method can be seen here:
void Butterfly::Start ()
{
csDefaultRunLoop (object_reg);
}
All we are doing here is starting the main loop of the Crystal Space application.
From here on the application is completely event driven, so let’s now look at
how the event queue works.
If you remember when we looked at the Initialize method, we specified that
our SimpleEventHandler method would be the static method, which would han-
dle all the events, using the following code segment:
if (!csInitializer::SetupEventHandler (object_reg, SimpleEventHandler))
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Can't initialize event handler!");
return false;
}
When a hardware or software event occurs, it is passed into the event handler
method (which in our example is SimpleEventHandler) as a reference to an
iEvent interface object. Once an event arrives into this method, we call the
instance method of our class called HandleEvent, passing in the reference to the
iEvent object. This is done simply to allow access to the instance members of
our Butterfly class, as they would obviously be inaccessible from a static
method. Here you can see the complete definition for the SimpleEventHandler
method:
bool Butterfly::SimpleEventHandler (iEvent& ev)
{
return butterfly->HandleEvent(ev);
}
Let’s now take a look at the HandleEvent method, which actually deals with the
incoming events. The first thing we should examine when an event arrives is the
type of event it is. Table 3-1 lists possible event types that can occur. These
events are discussed later in the chapter.
Getting Started with Crystal Space | 57
Creating Your Own Project
Once the event type is determined, it is possible to find out more information
specific to the event. This is possible because the csEvent class (which uses the
iEvent interface) contains many structures all contained within a union.
The first event type we handle is a csevBroadcast event, which is a command
event. So since we know that it is a command, we then examine the Code value
within the Command structure and test this against the value cscmdProcess. As
mentioned later in Table 3-4, the cscmdProcess command is sent every time a
frame should be rendered to the screen. So when this command event is
received, we simply call the SetupFrame method of our Butterfly class and then
return true to show that the event has been consumed (i.e., dealt with). This if
statement can be seen here:
if (ev.Type == csevBroadcast && ev.Command.Code == cscmdProcess)
{
butterfly->SetupFrame ();
return true;
}
We’ll look into the SetupFrame method in a moment. Next in the events that we
handle is another command event — this time the cscmdFinalProcess command,
which is always sent after the cscmdPostProcess (which is always sent after the
cscmdProcess command).
58 | Chapter 3
Creating Your Own Project
Next, let’s look into the SetupFrame method. In this method, we first check the
states of the arrow keys by making calls to the GetKeyState method of the
iKeyboardDriver interface we created in the Initialize method called kbd. By
using this method, we can determine if a key is down or up by examining the
true or false return value, respectively.
We first check the state of the left arrow key, which is defined by the macro
CSKEY_LEFT. If the key is found to be pressed, we then check whether the
logo is still within the bounds of the application; if so, we move it 2 pixels to the
left. This can be seen in the following segment of code:
if(kbd->GetKeyState (CSKEY_LEFT))
{
if(logo_x > 1)
logo_x-=2;
}
We then repeat this for the other three directions using the following code:
if (kbd->GetKeyState (CSKEY_RIGHT))
{
if(logo_x < 639-logoImg->Width())
logo_x+=2;
}
Getting Started with Crystal Space | 59
Creating Your Own Project
if (kbd->GetKeyState (CSKEY_UP))
{
if(logo_y > 1)
logo_y-=2;
}
if (kbd->GetKeyState (CSKEY_DOWN))
{
if(logo_y < 479-logoImg->Height())
logo_y+=2;
}
After we have adjusted the coordinates of the logo based upon the keyboard
input, we proceed by calling the BeginDraw method of the iGraphics2D inter-
face, which we have called g2d. This method will return true if the graphics
context is ready to start being drawn on. The call to this method can be seen
here:
if(!g2d->BeginDraw())
{
csReport(object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Failed to begin 2D frame");
return;
}
After this, our canvas should be ready to draw on, so the next call we make is to
the Clear method of the g2d interface, which basically clears the back buffer
reading for drawing to (note that you can pass a color into this method; however,
0 represents black, which is usually suitable for using to clear the back buffer).
This method can be seen here:
g2d->Clear(0);
Next, we ensure that our logoImg csPixmap pointer is valid, and then we call the
DrawScaled method of the csPixmap class, which will draw the image to the
current canvas (back buffer). This can be seen here:
if(logoImg)
{
logoImg->DrawScaled (g3d, logo_x, logo_y, logoImg->Width(), logoImg->Height(),
100);
}
Note that the first parameter is a reference to the iGraphics3D interface, which is
then followed by the x, y screen coordinates at which to draw the image. Then
finally we have the scaled width and height at which the image should be drawn.
Note how we have used the Width and Height methods of the csPixmap class to
obtain the original width and height of the image.
In addition to this method, there are several other useful methods that can be
used within the csPixmap class. If you do not require scaling, you can simply
use the draw method, which has the following prototype:
Draw(iGraphics3D *g3d, int sx, int sy, uint8 Alpha=0)
60 | Chapter 3
Creating Your Own Project
In addition, there is another useful method that allows you to tile your image
over a specified area. This method can be seen here:
DrawTiled(iGraphics3D *g3d, int sx, int sy, int w, int h, uint8 Alpha=0)
Note also that each of these methods can take an alpha parameter in the range of
0 to 255, where 0 is fully opaque and 255 is fully transparent.
So, after our image is rendered to the back buffer, we proceed by outputting
some useful information, such as the logo’s x, y position, to the screen.
To do this, we first get a suitable color to draw our text in. We do this by call-
ing the FindRGB method of the iGraphics2D interface, which returns an integer
representation of the color we requested. In our example we are retrieving a yel-
low color and storing it in a variable called fntcol. This can be seen in the
following line of code:
int fntcol = g2d->FindRGB (255, 255, 0);
Next, we create a char buffer of length 256 to temporarily hold a string, then we
use the sprintf function to write our string to the buffer, which we have called
buf. We then make a call to the Write method of the iGraphics2D interface.
The Write method takes the font as the first parameter, which we previously
acquired in the Initialize method and stored in a pointer to an iFont interface
called font. So we pass this in, followed by the x, y coordinate of where to draw
the string, then the foreground color of the font (which we retrieved with the
FindRGB method of the iGraphics2D interface). Next is the background color
(which we have set to –1, which tells the method not to draw a background) and
finally we pass in the actual string to draw (which is stored in our buf variable).
This can all be seen in the following segment of code:
char buf[256];
sprintf(buf, "Butterfly Grid");
g2d->Write(font, 10,10, fntcol, -1, buf);
As you can see, all we printed there was the text “Butterfly Grid.” In the next
segment, we actually print the coordinates of the logo. All we need to do is use
sprintf to generate the correct string from the values and then output the text
slightly below the other text by moving the y coordinate down. This can be seen
in the following code:
sprintf(buf, "Logo at (%i,%i)", logo_x, logo_y);
g2d->Write(font, 10,25, fntcol, -1, buf);
This concludes the SetupFrame method, so let’s now look at the FinishFrame
method, as it will be called directly after the SetupFrame method due to the way
our event handling is structured.
All we actually do in the FinishFrame method is first make a call to the
FinishDraw method of the iGraphics2D interface, which can be seen here:
g2d->FinishDraw();
Then we complete the rendering process by flipping the video page, making
what we have just rendered to the back buffer visible on the screen. This is
achieved by making a call to the Print method of the iGraphics2D interface,
passing in NULL as a parameter to signify that we wish to flip the entire area:
Getting Started with Crystal Space | 61
Creating Your Own Project
g2d->Print(NULL);
Finally, now that we have seen all the methods, let’s take a look at what happens
once the cscmdQuit command has been broadcast.
Recall from earlier that when the Start method is called, we call the following
Crystal Space method:
csDefaultRunLoop(object_reg);
This handles the main loops and controls the event queue. So when the
cscmdQuit command is broadcast, this method returns and the execution then
resumes in the main method, where the next line of code is:
delete butterfly;
As you know, this deallocates the memory assigned to our class when it was cre-
ated, and because we are using smart pointers for the interfaces, the appropriate
DecRef method is called on all our class instance smart pointers.
Next, if you remember we also used a smart pointer for the iFont interface;
however, we declared this outside the class, so we can deal with this simply by
setting its value to NULL, as this will make the smart pointer decrement the ref-
erence count internally. This can be seen here:
font = NULL;
Key Events
First we have the csevKeyDown and csevKeyUp events. If either of these
occurs, we can find out more information by using the Key structure defined in
the iEvent interface as follows:
struct
{
int Code; // Key code
int Char; // Character code
int Modifiers; // Control key state
} Key;
So, if you wanted to test if the “p” key was pressed, you could use the following
if statement:
if (ev.Type == csevKeyDown && ev.Key.Code == 'p')
As you can see, we first check if the Type of the event is csevKeyDown; if it is,
we know the Key struct will contain the information we require, so we test the
value of the Key.Code against the character value of “p”.
As there is no character to represent certain keys, such as the Escape key and
the cursor keys, we can use any of the following macros for comparing to the
62 | Chapter 3
Creating Your Own Project
Key.Code value to determine which key has been pressed. Table 3-2 lists the
available control key codes.
Table 3-2: Key code macros
Key Code Macro Actual Key
CSKEY_ESC Escape key (Esc)
CSKEY_ENTER Return key
CSKEY_TAB Tab key
CSKEY_BACKSPACE Backspace key
CSKEY_SPACE Spacebar
CSKEY_UP Up Arrow key
CSKEY_DOWN Down Arrow key
CSKEY_LEFT Left Arrow key
CSKEY_RIGHT Right Arrow key
CSKEY_PGUP Page Up key
CSKEY_PGDN Page Down key
CSKEY_HOME Home key
CSKEY_END End key
CSKEY_INS Insert key
CSKEY_DEL Delete key
CSKEY_CTRL Control key
CSKEY_ALT Alt key
CSKEY_SHIFT Shift key
CSKEY_F1 Function key F1
CSKEY_F2 Function key F2
CSKEY_F3 Function key F3
CSKEY_F4 Function key F4
CSKEY_F5 Function key F5
CSKEY_F6 Function key F6
CSKEY_F7 Function key F7
CSKEY_F8 Function key F8
CSKEY_F9 Function key F9
CSKEY_F10 Function key F10
CSKEY_F11 Function key F11
CSKEY_F12 Function key F12
CSKEY_CENTER Center key (“5” on numeric keypad)
CSKEY_PADPLUS numeric keypad + key
CSKEY_PADMINUS Numeric keypad – key
CSKEY_PADMULT Numeric keypad * key
CSKEY_PADDIV Numeric keypad / key
CSKEY_FIRST Numeric value of the first control key code
CSKEY_LAST Numeric value of the last control key code
Getting Started with Crystal Space | 63
Creating Your Own Project
Ü related
NOTE Although the CSKEY_FIRST and CSKEY_LAST macros are not actually
to keys as such, they provide a useful means of cycling through all the
possible control keys as they will always be set to the first and last codes for the
control keys.
In addition to the list in Table 3-2, we can check for modifiers, such as the Ctrl
key being pressed at the same time as the “p” key. This is really useful and also
really simple to do. The modifiers are actually stored as a bitfield, so for exam-
ple, if we wanted to see if the user pressed Ctrl+n, we would use the following if
statement:
if (ev.Type == csevKeyDown && ev.Key.Code == 'n' && ev.Key.Modifiers &
CSMASK_CTRL)
Or, if we wanted to see if Ctrl+Shift+E was pressed, we could use the following
if statement:
if (ev.Type == csevKeyDown && ev.Key.Code == 'p' && ev.Key.Modifiers &
CSMASK_CTRL && ev.Key.Modifiers & CSMASK_SHIFT)
Table 3-3 shows all the possible modifier key masks that can be used in combi-
nation with the key codes.
Table 3-3: Modifier key masks
Modifier Key Mask Actual Key
CSMASK_SHIFT Shift key
CSMASK_CTRL Control key (Ctrl)
CSMASK_ALT Alt key
CSMASK_ALLSHIFTS Shift, Ctrl, or Alt key
CSMASK_FIRST This is a special case that only returns true if this is the first time a
key has been pressed (i.e., it is not an event caused by key repeat
from it being held down).
Mouse Events
The next event type is mouse events, which are csevMouseMove, csevMouse-
Down, csevMouseUp, csevMouseClick, and csevMouseDoubleClick.
When any of these events are received, it is possible to retrieve additional
information by accessing the Mouse structure, which is within the Union in the
iEvent interface. The definition of the Mouse structure can be seen here:
struct
{
int x,y; // Mouse coords
int Button; // Button number: 1-left, 2-right, 3-middle
int Modifiers; // Control key state
} Mouse;
As you can see, within this structure we can grab useful information such as the
x, y position of the mouse, which button was pressed (if a csevMouseDown
event occurred), and if any control keys were held down at the same time (see
Table 3-3 for a list of key modifiers).
64 | Chapter 3
Creating Your Own Project
Joystick Events
Next in the list of event types are the joystick events: csevJoystickMove,
csevJoystickDown, and csevJoystickUp. If any of these events occur, we can
access the Joystick structure, which again is contained within the Union in the
iEvent interface. The definition for this structure can be seen here:
struct
{
int number; // Joystick number (1, 2, ...)
int x, y; // Joystick x, y
int Button; // Joystick button number
int Modifiers; // Control key state
} Joystick;
As you can see, this is very similar to the mouse events, with the addition of the
number variable, which determines which joystick the event actually came from
(if there is more than one joystick connected).
Command Events
When a command event is received, such as csevCommand or csevBroadcast,
we need to access the Command structure from within the Union. The Com-
mand structure looks as follows:
struct
{
Code; // Command code
void *Info; // Command info
} Command;
Unlike the previous structures for events we have seen, like the key and mouse
events, the Command structure actually has its own sub-list of possible com-
mands, which are stored in the Code unsigned integer within the Command
structure when a csevCommand or csevBroadcast event is received. Table 3-4
has a complete list of possible values for Code.
Table 3-4: Command code macros
Command Code Macro Purpose
cscmdNothing There was no command.
cscmdQuit Used to terminate the application.
cscmdFocusChanged The application has either lost or gained focus. If this command
is received, the Info pointer in the structure will point to a Boolean
value, where true means the window has gained focus and false
means it has lost focus.
cscmdSystemOpen This event is broadcast to all plug-ins inside
csSystemDriver::Open right after all drivers were initialized and
opened.
cscmdSystemClose This event is broadcast to all plug-ins inside
csSystemDriver::Close right before starting to close all drivers.
cscmdContextResize This is sent if the user resizes the application window. If this
command is received, the Info pointer will point to a iGraphics2D
interface, which will reference the context that has been changed.
Getting Started with Crystal Space | 65
Summary
Y
frame of rendering.
cscmdFinalProcess This command is broadcast right after cscmdPostProcess on each
cscmdCanvasHidden
FL
frame of rendering.
This command lets the application know that the application
(canvas) is not currently visible to the users (for example, it may
AM
have been iconified).
cscmdCanvasExposed This command lets the application know the application is now
visible (when it was previously hidden).
TE
Network Events
Finally, we have the network event called csevNetwork. When this event occurs,
it fills the Network structure within the Union contained within the iEvent inter-
face. The Network structure can be seen here:
struct
{
iNetworkSocket2 *From; // Socket data received on
iNetworkPacket *Data; // Packet of data received
} Network;
We’re not going to look into the network events, as we will not be using them at
all.
Summary
This concludes the section on getting started with Crystal Space. You should
now have a good understanding of the foundations of a simple Crystal Space
application that is going to help a lot over the next few chapters.
In the next chapter, we will be moving into 3D by looking at how to create
and handle 3D worlds within the Crystal Space engine.
Team-Fly®
This page intentionally left blank.
Chapter 4
Moving into 3D
Introduction
In this chapter, we will build on the knowledge we gained in the previous chap-
ter by looking into how to use the actual 3D engine in Crystal Space. We will
start by using the example from Chapter 3 as a foundation for this chapter. The
first thing we will look at is how to create a simple room and add the functional-
ity to move the camera about the world. Once we have covered this, we will
move on to loading in animated Quake 2 models (and move them about). Then
finally we will look at a map modeling tool called Valve Hammer Editor (for-
merly known as Worldcraft) that we can use to create our own 3D game map
and then we will load it into Crystal Space, along with an animated model we
can move around the world (with collision detection). As you can see, we’re
going to cover a lot here, so let’s waste no time in getting started.
67
68 | Chapter 4
Basics of 3D in Crystal Space
#include "iengine/camera.h"
#include "iengine/light.h"
#include "iengine/statlght.h"
#include "iengine/texture.h"
#include "iengine/mesh.h"
#include "iengine/movable.h"
#include "iengine/material.h"
#include "imesh/thing/polygon.h"
#include "imesh/thing/thing.h"
#include "imesh/object.h"
#include "ivideo/graph3d.h"
#include "ivideo/graph2d.h"
#include "ivideo/txtmgr.h"
#include "ivideo/texture.h"
#include "ivideo/material.h"
#include "ivideo/fontserv.h"
#include "igraphic/image.h"
#include "igraphic/imageio.h"
#include "imap/parser.h"
#include "ivaria/reporter.h"
#include "ivaria/stdrep.h"
#include "csutil/cmdhelp.h"
CS_IMPLEMENT_APPLICATION
Butterfly::~Butterfly ()
{
}
bool Butterfly::LoadPixMaps ()
{
return true;
}
void Butterfly::SetupFrame ()
Moving into 3D | 69
Basics of 3D in Crystal Space
{
// Check input...
// NEW ->
iCamera* c = view->GetCamera();
if (kbd->GetKeyState (CSKEY_RIGHT))
c->GetTransform ().RotateThis (CS_VEC_ROT_RIGHT, speed);
if (kbd->GetKeyState (CSKEY_LEFT))
c->GetTransform ().RotateThis (CS_VEC_ROT_LEFT, speed);
if (kbd->GetKeyState (CSKEY_PGUP))
c->GetTransform ().RotateThis (CS_VEC_TILT_UP, speed);
if (kbd->GetKeyState (CSKEY_PGDN))
c->GetTransform ().RotateThis (CS_VEC_TILT_DOWN, speed);
if (kbd->GetKeyState (CSKEY_UP))
c->Move (CS_VEC_FORWARD * 4 * speed);
if (kbd->GetKeyState (CSKEY_DOWN))
c->Move (CS_VEC_BACKWARD * 4 * speed);
g3d->FinishDraw();
// <- NEW
// Begin 2D rendering...
if (!g2d->BeginDraw ())
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Failed to begin 2D frame");
return;
}
//g2d->Clear(0); REMOVED
if(logoImg)
{
logoImg->DrawScaled (g3d, g2d->GetWidth()-logoImg->Width()-10,
g2d->GetHeight()-logoImg->Height()-10, logoImg->Width(),
logoImg->Height());
70 | Chapter 4
Basics of 3D in Crystal Space
// draw text...
int fntcol = g2d->FindRGB (255, 255, 0);
char buf[256];
sprintf(buf, "Butterfly Grid");
g2d->Write(font, 10,10, fntcol, -1, buf);
}
void Butterfly::FinishFrame ()
{
g2d->FinishDraw ();
g2d->Print (NULL);
}
return false;
}
bool Butterfly::Initialize ()
{
if (!csInitializer::RequestPlugins (object_reg,
CS_REQUEST_VFS,
CS_REQUEST_SOFTWARE3D,
CS_REQUEST_ENGINE,
CS_REQUEST_FONTSERVER,
CS_REQUEST_IMAGELOADER,
Moving into 3D | 71
Basics of 3D in Crystal Space
CS_REQUEST_LEVELLOADER,
CS_REQUEST_REPORTER,
CS_REQUEST_REPORTERLISTENER,
CS_REQUEST_END))
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Can't initialize plugins!");
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.butterfly",
"Error opening system!");
return false;
}
// NEW ->
txtmgr->SetVerbose (true);
iPolygon3D* p;
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-5, 0, 5));
p->CreateVertex (csVector3 (5, 0, 5));
p->CreateVertex (csVector3 (5, 0, -5));
p->CreateVertex (csVector3 (-5, 0, -5));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 2.5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-5, 10, -5));
p->CreateVertex (csVector3 (5, 10, -5));
p->CreateVertex (csVector3 (5, 10, 5));
p->CreateVertex (csVector3 (-5, 10, 5));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 2.5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-5, 10, 5));
p->CreateVertex (csVector3 (5, 10, 5));
p->CreateVertex (csVector3 (5, 0, 5));
p->CreateVertex (csVector3 (-5, 0, 5));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 2.5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (5, 10, 5));
p->CreateVertex (csVector3 (5, 10, -5));
p->CreateVertex (csVector3 (5, 0, -5));
p->CreateVertex (csVector3 (5, 0, 5));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 2.5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-5, 10, -5));
p->CreateVertex (csVector3 (-5, 10, 5));
p->CreateVertex (csVector3 (-5, 0, 5));
p->CreateVertex (csVector3 (-5, 0, -5));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 2.5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (5, 10, -5));
p->CreateVertex (csVector3 (-5, 10, -5));
p->CreateVertex (csVector3 (-5, 0, -5));
p->CreateVertex (csVector3 (5, 0, -5));
74 | Chapter 4
Basics of 3D in Crystal Space
csRef<iStatLight> light;
iLightList* ll = room->GetLights ();
engine->Prepare ();
// <- NEW
font = g2d->GetFontServer()->LoadFont(CSFONT_LARGE);
return true;
}
void Butterfly::Start ()
{
csDefaultRunLoop (object_reg);
}
csInitializer::DestroyApplication (object_reg);
return 0;
}
Moving into 3D | 75
Basics of 3D in Crystal Space
#include <stdarg.h>
#include "csutil/ref.h"
struct iObjectRegistry;
struct iEngine;
struct iLoader;
struct iGraphics2D;
struct iGraphics3D;
struct iKeyboardDriver;
struct iVirtualClock;
Y
struct iEvent;
struct iView;
struct
struct
struct
iFont;
FL
iTextureManager;
iSector; // NEW
AM
class Butterfly
{
TE
private:
iObjectRegistry* object_reg;
csRef<iEngine> engine;
csRef<iLoader> loader;
csRef<iGraphics2D> g2d;
csRef<iGraphics3D> g3d;
csRef<iKeyboardDriver> kbd;
csRef<iVirtualClock> vc;
csRef<iView> view;
csRef<iTextureManager> txtmgr;
csRef<iSector> room; // NEW
csPixmap* logoImg;
csRef<iFont> font;
bool Initialize();
void Start();
};
#endif // __BUTTERFLY_H__
Team-Fly®
76 | Chapter 4
Basics of 3D in Crystal Space
If you compile this code and then run it, you should see something similar to the
following:
As you can see from this screen shot, my choice of texture for the wall could not
be any worse — but hey, I’m not an artist J. Try moving the camera about with
the cursor keys. In addition, you can use Page Up and Page Down to tilt the
camera up and down, and then you will be able to move in that direction.
Finally, note how we are still able to draw our logo and text onto the screen,
above the 3D rendered scene.
Ü rendering.
Note You may not have noticed but up until now we have been using software
If you want to run your applications using OpenGL instead of software
then you need to add the following command line parameter:
-video=opengl
So if the executable for this application is called appbutterfly3d1.exe and it is in
the root Crystal Space folder, we would execute it with the following command:
c:\crystal\CS\appbutterfly3d1.exe -video=opengl
Let’s now look into the code and see what we have changed and added. First, we
have added a smart pointer to an iSector interface called room as a member of
our Butterfly class. A sector is an empty area of space in Crystal Space, but it
does not represent any actual geometry. A sector can contain geometry and mesh
objects (as we will see later) and lights.
In this example, we will be using this sector, named room, to contain the
geometry and lighting for our actual room. So, the only line of code we have
added to the Butterfly class in the header is the following:
Moving into 3D | 77
Basics of 3D in Crystal Space
Since this is the only change to the header file, let’s now look at the changes in
the source file.
The first thing we are going to look at here is what we have added to the Ini-
tialize method. The first new line we have added is the following:
// NEW ->
txtmgr->SetVerbose (true);
What this does is set the texture manager to verbose, which simply means it will
give up more information in the console window.
We then disable lighting caching as it is not required for such a simple 3D
level. This is done by calling the SetLightingCacheMode method of the iEngine
interface, passing 0 as a parameter:
engine->SetLightingCacheMode(0);
Next, we load in the Butterfly Grid logo as a texture, using the same logo.jpg
contained in the butterfly.zip file in the data folder. To load it in, we use the
LoadTexture method of the iLoader interface, which we have a reference to
called loader. The first parameter of this method is a name for the texture, so we
can reference it later in the application, and the second parameter is the filename
on the VFS (Virtual File System). The complete code segment that loads in the
texture can be seen here:
if (!loader->LoadTexture ("butterflytexture", "/lib/butterfly/logo.jpg"))
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Error loading 'butterflytexture' texture!");
return false;
}
We now have our texture (of the Butterfly Grid logo) referenced by an
iMaterialWrapper interface pointer called tm.
Next, we create a sector in the engine called room and then store the pointer
to the iSector interface the CreateSector method returns in our room instance
variable. This can be seen here:
room = engine->CreateSector("room");
The next line of code creates a convex outline for our room sector. This is done
by creating an iMeshWrapper and assigning it the name “walls.” This can be
seen in the next line of code:
csRef<iMeshWrapper> walls (engine->CreateSectorWallsMesh (room, "walls"));
78 | Chapter 4
Basics of 3D in Crystal Space
Now that Crystal Space knows which texture will be used to map the polygon,
we define four vertices that will represent the corners of the quad (the wall). We
specify these by calling the CreateVertex method of the iPolygon3D interface,
which takes a csVector3D as a parameter. The four vertices for our first wall are:
p->CreateVertex (csVector3 (-5, 0, 5));
p->CreateVertex (csVector3 (5, 0, 5));
p->CreateVertex (csVector3 (5, 0, -5));
p->CreateVertex (csVector3 (-5, 0, -5));
The final part of specifying the polygon is to state the texture coordinates on it
(so it knows how the material should be mapped onto it). The way this works is
very clever and easy to use. Let’s first look at the line of code we use to assign
the texture coordinates:
p->SetTextureSpace(p->GetVertex (0), p->GetVertex (1), 2.5);
In the preceding diagram, the four vertices are represented by v(0), v(1), v(2),
and v(3). So in our SetTextureSpace method call, we are saying that we want
v(0) to v(1) to represent the u-axis for our texture, as shown in the following dia-
gram. Note that the GetVertex method simply returns the csVector3 structure for
that vertex.
So once the u-axis is known, the method then works out the v-axis internally, as
it will be orthogonal to the u-axis. Thus, our texture coordinate axes will look as
follows:
The final parameter of this method is to determine how the texture should be
applied, so you can, for example, tile the texture across the polygon. What this
final parameter specifies is the length of the u and v axes, so let’s say the length
of the vector between v(0) and v(1) is 10 units (as our example is); if we specify
this final parameter as 2.5, our texture will be tiled four times along u and four
times along v, which looks like Figure 4-5.
If we changed this final parameter on the back wall to be 1 instead of 2.5, this
would give us 10/1 = 10 tiles instead of 10/2.5 = 4 tiles, as shown in Figure 4-6.
Moving into 3D | 81
Basics of 3D in Crystal Space
Finally, if we set the final parameter equal to the length of the size of our poly-
gon (which in this example is 10 units), the texture will just be stretched to fit
once onto our polygon. See Figure 4-7.
This process is then repeated for the other three walls, the floor, and the ceiling
to generate our six-polygon room.
After we have created our polygons, the next stage is to add lighting to our
room. To do this we first create a smart pointer to an iStatLight interface called
light, which is used to represent a static light in the world. This can be seen in
the following line of code:
csRef<iStatLight> light;
After we have declared this, we need to obtain a pointer to the list of lights for
our sector (remember a sector contains the lights but not the actual polygon
data). So to do this, we call the GetLights method of our room reference and
store the pointer in a variable called ll, which is of type iLightList:
iLightList* ll = room->GetLights();
We can then create a new light by making a call to the CreateLight method of
our iEngine interface, engine:
light = engine->CreateLight (NULL, csVector3 (-3, 5, 0), 10, csColor (1, 1, 1),
false);
The first parameter of this method is a name for the light, so that it can be refer-
enced later. In this example we will not be using the light after it is created so we
can specify the name as NULL. Next we specify the position of the light within
the sector using a csVector3 structure, placing this light –3, 5, 0 in the x, y, and z
axes respectively. The next parameter specifies the radius of the light, which we
have set to 10 in this example. Then we have the color specified as a csColor
structure, where 1.0 is full brightness and 0.0 is no brightness for each of the
color components (i.e., red, green, and blue respectively). Finally, we have to
specify a Boolean value that specifies whether the light is static (false) or
pseudo-dynamic (true).
Once our light is created, we store it temporarily in our light reference, then
we call the Add method of the iLightList, ll, passing in a call to the QueryLight
method, called on our light reference. This simply adds our light into the light
list for the sector.
ll->Add(light->QueryLight());
This process is then repeated for the other two lights we have added in this
example.
After the lights have been added, we make a call to the Prepare method of our
engine variable:
engine->Prepare();
This method readies your world for rendering by preparing the lightmaps and
also clears up the textures you have loaded as they will be stored internally in
the texture manager.
The final part of the new section in the Initialize method is the creation of our
view into the world. This part deals with setting up our camera and the 2D clip-
ping rectangle.
To create the view, we first create a new csView object by passing a pointer
to our engine (iEngine) and g3d (iGraphics3D) into the constructor of the class.
Moving into 3D | 83
Basics of 3D in Crystal Space
We then cast this to be a smart pointer by using the csPtr template and store it in
our class member called view. This can all be seen in the following line of code:
view = csPtr<iView> (new csView(engine, g3d));
Once we have our view object, we can retrieve a reference to the camera associ-
ated with the view by calling the GetCamera method. After we have the camera,
we proceed by calling the SetSector method of the iCamera interface to set the
sector the camera is within to be our room sector. This can be seen in the follow-
ing line of code:
view->GetCamera()->SetSector(room);
Now that our camera is placed in the sector, we can then specify the starting
position of the camera (i.e., the origin) by calling GetTransform on the camera
and then calling the SetOrigin method, which takes a csVector3 as a parameter.
This can be seen in the following line of code:
view->GetCamera ()->GetTransform ().SetOrigin (csVector3 (-3, 3, -3));
The final part of our initialization is to set the 2D clipping rectangle that deter-
mines the size of the 2D area shown on the screen once the rendering is done. So
if we wanted to leave 120 pixels of the bottom blank, maybe to draw some 2D
player information there, we could instead use the following line of code:
view->SetRectangle (0, 120, g2d->GetWidth (), g2d->GetHeight ());
Now that we have covered the extra initialization, it’s time to take a look at what
we have changed in the SetupFrame method.
84 | Chapter 4
Basics of 3D in Crystal Space
First we obtain the elapsed time since the last frame was rendered, so that we
can perform time-based movement. This is achieved by making a call to the
GetElapsedTicks method of the iVirtualClock interface, which we have a refer-
ence to called vc. We store the return value from this call in a csTicks structure
called elapsed_time. This can be seen here.
csTicks elapsed_time = vc->GetElapsedTicks ();
We can then determine the speed the camera should be moving by using the fol-
lowing line of code:
float speed = (elapsed_time / 1000.0) * (0.03 * 20);
Once we have the speed, we obtain a pointer to the camera using the GetCamera
method of our view object:
iCamera* c = view->GetCamera();
The next four lines of code handle the left and right keypresses, which rotate the
camera left and right respectively. The code for this can be seen here:
if(kbd->GetKeyState (CSKEY_RIGHT))
c->GetTransform().RotateThis (CS_VEC_ROT_RIGHT, speed);
if(kbd->GetKeyState (CSKEY_LEFT))
c->GetTransform().RotateThis (CS_VEC_ROT_LEFT, speed);
As you can see, to rotate the camera, we first use the GetTransform method,
with which we can then call the RotateThis method. To this method we pass in
the vector to rotate around (specified as CS_VEC_ROT_RIGHT, which is a
standard definition in Crystal Space) followed by the angle in radians (which we
have used the speed to represent).
We repeat this for tilting the camera up and down. For moving the camera,
we use the Move method of the iCamera interface, which moves the camera for-
ward relative to its current position and orientation. This can be seen in the
following four lines of code.
if(kbd->GetKeyState (CSKEY_UP))
c->Move (CS_VEC_FORWARD * 4 * speed);
if(kbd->GetKeyState (CSKEY_DOWN))
c->Move (CS_VEC_BACKWARD * 4 * speed);
Next we want to start our 3D rendering, so as we did with 2D, we need to make
a call to the BeginDraw method of our g3d object. Into this method, we pass in a
call to GetBeginDrawFlags on our engine object, which is OR’ed with the flag
CSDRAW_3DGRAPHICS to indicate to the application we are interested in
rendering 3D.
if (!g3d->BeginDraw(engine->GetBeginDrawFlags() | CSDRAW_3DGRAPHICS))
return;
The next part is drawing the world, which is extremely complex. (Well, not
really.) To draw the world (i.e., the 3D geometry), all we need to do is call the
Draw method of our view object and Crystal Space will perform its magic.
view->Draw();
Moving into 3D | 85
Quake 2 Model Example
We then make a call to the FinishDraw method of our g3d object to indicate we
have finished our 3D rendering.
g3d->FinishDraw();
Notice how we can begin our 2D rendering on top of the 3D scene. Be sure to
note that we have removed the g2d->Clear(0); call; if this were left in, we would
not be able to see our 3D scene, as the back buffer would be cleared.
This concludes our first 3D example. In the next section we will look at how
to convert, load, and move an animated Quake 2 (md2) model around in our lit-
tle room.
Y
Now that we know how to make a simple room and render it, in this example we
FL
are going to progress by loading in an animated Quake 2 model. Once the model
is loaded we will allow the player to walk the model around the room in a
third-person style view. Note that we will not be adding collision detection in
AM
this example; we’ll leave that until the next example.
Before we start looking at how to go about this, let’s see a complete working
example. In this example, I use a file called marine.zip; you will need to find a
TE
model of your own to work with. First, copy the file into the c:\crystal\CS\data
folder. Then you will need to add the following additional line to the VFS con-
figuration file (vfs.cfg), located in the c:\crystal\CS folder, using your file name,
of course:
VFS.Mount.lib/skeleton = $@data$/marine.zip
Now either create a new application (following the “Creating and Setting Up the
Project” section in the previous chapter) or simply change the source files for
the previous example. Here is the complete source code listing for this example.
Listing 4-3: butterfly3d2.cpp
#include "cssysdef.h"
#include "cssys/sysfunc.h"
#include "iutil/vfs.h"
#include "csutil/cscolor.h"
#include "cstool/csview.h"
#include "cstool/initapp.h"
#include "cstool/cspixmap.h"
#include "butterfly3d2.h" // MODIFIED
#include "iutil/eventq.h"
#include "iutil/event.h"
#include "iutil/objreg.h"
#include "iutil/csinput.h"
#include "iutil/virtclk.h"
#include "iengine/sector.h"
#include "iengine/engine.h"
#include "iengine/camera.h"
#include "iengine/light.h"
#include "iengine/statlght.h"
#include "iengine/texture.h"
#include "iengine/mesh.h"
Team-Fly®
86 | Chapter 4
Quake 2 Model Example
#include "iengine/movable.h"
#include "iengine/material.h"
#include "imesh/thing/polygon.h"
#include "imesh/thing/thing.h"
#include "imesh/object.h"
#include "imesh/sprite3d.h" // NEW
#include "ivideo/graph3d.h"
#include "ivideo/graph2d.h"
#include "ivideo/txtmgr.h"
#include "ivideo/texture.h"
#include "ivideo/material.h"
#include "ivideo/fontserv.h"
#include "igraphic/image.h"
#include "igraphic/imageio.h"
#include "imap/parser.h"
#include "ivaria/reporter.h"
#include "ivaria/stdrep.h"
#include "csutil/cmdhelp.h"
CS_IMPLEMENT_APPLICATION
isWalking = false;
}
Butterfly::~Butterfly ()
{
}
bool Butterfly::LoadPixMaps ()
{
return true;
}
void Butterfly::SetupFrame ()
{
// Check input...
Moving into 3D | 87
Quake 2 Model Example
iCamera* c = view->GetCamera();
if (kbd->GetKeyState (CSKEY_PGUP))
c->Move(csVector3(0, 1, 0) * 4 * speed);
if (kbd->GetKeyState (CSKEY_PGDN))
c->Move(csVector3(0, 1, 0) * 4 * speed * -1);
if (kbd->GetKeyState (CSKEY_UP))
c->Move (CS_VEC_FORWARD * 4 * speed);
if (kbd->GetKeyState (CSKEY_DOWN))
c->Move (CS_VEC_BACKWARD * 4 * speed);
// NEW ->
// Player movement...
if(kbd->GetKeyState ('w'))
{
if(isWalking == false)
playerstate->SetAction("run");
isWalking = true;
player->GetMovable()->SetPosition(player->GetMovable()->
GetTransform().This2Other(csVector3(1,0,0) * speed * 5));
player->GetMovable()->UpdateMove();
}
else
{
if(isWalking == true)
playerstate->SetAction("stand");
isWalking = false;
}
if(kbd->GetKeyState ('a'))
{
player->GetMovable ()->GetTransform ().RotateThis
(csVector3(0, -1, 0), speed * 4);
player->GetMovable()->UpdateMove();
}
if(kbd->GetKeyState ('d'))
{
player->GetMovable ()->GetTransform ().RotateThis
(csVector3(0, 1, 0), speed * 4);
player->GetMovable()->UpdateMove();
}
// <- NEW
g3d->FinishDraw();
// Begin 2D rendering...
if (!g2d->BeginDraw ())
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Failed to begin 2D frame");
return;
}
if(logoImg)
{
logoImg->DrawScaled (g3d, g2d->GetWidth()-logoImg->Width()-10,
g2d->GetHeight()-logoImg->Height()-10, logoImg->Width(),
logoImg->Height());
}
// draw text...
int fntcol = g2d->FindRGB (255, 255, 0);
char buf[256];
sprintf(buf, "Butterfly Grid");
g2d->Write(font, 10,10, fntcol, -1, buf);
}
void Butterfly::FinishFrame ()
{
g2d->FinishDraw ();
g2d->Print (NULL);
}
butterfly->FinishFrame ();
return true;
}
else if (ev.Type == csevKeyDown && ev.Key.Code == CSKEY_ESC)
{
csRef<iEventQueue> q (CS_QUERY_REGISTRY (object_reg, iEventQueue));
if (q) q->GetEventOutlet()->Broadcast(cscmdQuit);
return true;
}
return false;
}
bool Butterfly::Initialize ()
{
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.butterfly",
"Can't initialize plugins!");
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.butterfly",
"Error opening system!");
return false;
}
txtmgr = g3d->GetTextureManager();
txtmgr->SetVerbose (true);
// MODIFIED ->
iPolygon3D* p;
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-20, 0, 20));
p->CreateVertex (csVector3 (20, 0, 20));
p->CreateVertex (csVector3 (20, 0, -20));
p->CreateVertex (csVector3 (-20, 0, -20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-20, 20, -20));
p->CreateVertex (csVector3 (20, 20, -20));
p->CreateVertex (csVector3 (20, 20, 20));
p->CreateVertex (csVector3 (-20, 20, 20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
p = walls_state->CreatePolygon ();
92 | Chapter 4
Quake 2 Model Example
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-20, 20, 20));
p->CreateVertex (csVector3 (20, 20, 20));
p->CreateVertex (csVector3 (20, 0, 20));
p->CreateVertex (csVector3 (-20, 0, 20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (20, 20, 20));
p->CreateVertex (csVector3 (20, 20, -20));
p->CreateVertex (csVector3 (20, 0, -20));
p->CreateVertex (csVector3 (20, 0, 20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-20, 20, -20));
p->CreateVertex (csVector3 (-20, 20, 20));
p->CreateVertex (csVector3 (-20, 0, 20));
p->CreateVertex (csVector3 (-20, 0, -20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (20, 20, -20));
p->CreateVertex (csVector3 (-20, 20, -20));
p->CreateVertex (csVector3 (-20, 0, -20));
p->CreateVertex (csVector3 (20, 0, -20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
csRef<iStatLight> light;
iLightList* ll = room->GetLights ();
// <- MODIFIED
// NEW ->
csMatrix3 m;
m.Identity ();
m *= 5.0;
// <- NEW
engine->Prepare ();
font = g2d->GetFontServer()->LoadFont(CSFONT_LARGE);
return true;
}
94 | Chapter 4
Quake 2 Model Example
void Butterfly::Start ()
{
csDefaultRunLoop (object_reg);
}
csInitializer::DestroyApplication (object_reg);
return 0;
}
#include <stdarg.h>
#include "csutil/ref.h"
struct iObjectRegistry;
struct iEngine;
struct iLoader;
struct iGraphics2D;
struct iGraphics3D;
struct iKeyboardDriver;
struct iVirtualClock;
struct iEvent;
struct iView;
struct iTextureManager;
struct iFont;
struct iSector;
struct iMeshWrapper; // NEW
struct iSprite3DState; // NEW
class Butterfly
{
private:
iObjectRegistry* object_reg;
csRef<iEngine> engine;
csRef<iLoader> loader;
csRef<iGraphics2D> g2d;
csRef<iGraphics3D> g3d;
csRef<iKeyboardDriver> kbd;
csRef<iVirtualClock> vc;
csRef<iView> view;
csRef<iTextureManager> txtmgr;
Moving into 3D | 95
Quake 2 Model Example
csRef<iSector> room;
csPixmap* logoImg;
csRef<iFont> font;
csRef<iMeshWrapper> player; // NEW
csRef<iSprite3DState> playerstate; // NEW
bool isWalking; // NEW
Y
void DrawFrame2D ();
public:
FL
Butterfly(iObjectRegistry* object_reg);
~Butterfly();
AM
bool Initialize();
void Start();
};
TE
#endif // __BUTTERFLY_H__
When we run this example, we are able to move around the model with the “w,”
“a,” and “d” keys, tilt the camera with the Page Up and Page Down keys, and
also zoom in and out with the up and down arrows.
The following images show how this looks when we run it.
Team-Fly®
96 | Chapter 4
Quake 2 Model Example
Now that we have seen it in action, let’s discuss the process involved in creating
this example.
Before we even consider looking at the code, the first place to start is the 3D
model. As I mentioned before, we are using a Quake 2 model in this example,
which uses the md2 model format. We’ll do this step by step, so even if you’re
not familiar with this format, you’ll be able to create the example.
First we need to get a model (or make one if you are artistically inclined).
The best place that I have found for models has to be www.polycount.com,
which has a huge database of various model formats, including the Quake 2 md2
models we require.
' http://www.polycount.com
You can either browse the Quake 2 model section and find one you like or use
the following link to download the one that I used for the example. Note that this
tutorial will give exact instructions for the one I selected, so it may be best to
use this for now.
' http://www.planetquake.com/polycount/downloads/index.asp?model=242
Once downloaded, you will have a zip file named q2mdl-pmarine.zip. Extract
the zip and then move through the subfolders contained within it until you find a
list of files.
Moving into 3D | 97
Quake 2 Model Example
Figure 4-11: The models file list (as extracted from the zip file)
We are only actually interested in two files here. The first is the main model file,
tris.MD2, and the second is the skin for the model, which we can pick from
Reese.pcx, Centurion.pcx, brownie.pcx, and USMC.pcx.
Crystal Space does not support the PCX image format, so we first need to
pick which skin we are going to use and then convert it into PNG format. To do
this, simply use your favorite graphics application (such as Paint Shop Pro; a
trial version is available on the CD).
I’m going to pick the brownie.pcx skin, shown in Figure 4-12.
98 | Chapter 4
Quake 2 Model Example
Note that the model file contains the texture coordinates to map this skin cor-
rectly, so we don’t need to worry about that aspect. Save this texture back into
the same directory with the filename brownie.png.
After we have the texture in the correct image format for Crystal Space, the
next thing we need to do is convert the model to the correct format. To do this,
first copy the tris.MD2 model file into the main Crystal Space folder (i.e.,
c:\crystal\CS). Next, go to the command prompt and ensure that you are in the
c:\crystal\CS directory, then type the following:
mdl2spr tris.MD2 tris
Moving into 3D | 99
Quake 2 Model Example
What we just did was use the mdl2spr Crystal Space tool, which converts a
Quake 2 (md2) model into a Crystal Space 3D sprite. If you look in the c:\crys-
tal\CS folder, you will find a new file called tris.spr. Move this file back to the
original folder that contains the files extracted from the model zip.
Next add the tris.spr and brownie.PNG files into a new zip archive called
marine.zip and then move this zip file into the Crystal Space data directory, so it
has this complete path:
c:\crystal\CS\data\marine.zip
If you didn’t do this previously, you now need to add the mount point for this
new marine.zip into the Virtual File System. This is done by adding the follow-
ing line in the vfs.cfg, located in the c:\crystal\CS folder:
VFS.Mount.lib/marine = $@data$/marine.zip
Our model is still not ready to use though. Due to recent advancements in Crys-
tal Space, the model now needs to be in an XML format. Fortunately, there is a
tool called cs2xml located in the root CS folder. So go back to the command
prompt now and ensure you are in the c:\crystal\CS folder, then type the
following:
cs2xml lib/marine/tris.spr
Note how we need to use the VFS path here. When this tool is executed, you
should see something similar to the following.
This converts and replaces our tris.spr within the marine.zip file. Next we need
to make a minor modification to the XML, so extract the tris.spr to the data
directory and then open it up using WordPad (it will probably not look correct if
you use Notepad to open it). The file will look as follows.
As we want to be able to specify the skin of the model from within our applica-
tion, we need to change the fourth line down to read as follows:
<material>skin</material>
Once you have made this change, save the file and then replace the one in
marine.zip with the one you just modified.
Now the model is ready to be loaded in, so let’s proceed by looking at what
we have changed, added, and removed from the code for this example.
The first addition to the source is the inclusion of the sprite3d.h header file
(located in the imesh folder), which contains everything required to load and
manipulate 3D sprites. This can be seen here:
#include "imesh/sprite3d.h"
The first is an iMeshWrapper, which we will use to store the model transforma-
tion information in. Then we have an iSprite3DState, which we will be using to
control the animation of the model. Finally, we have a Boolean to store whether
the model is currently walking or not.
Let’s now jump to the Initialize method. The first thing we have done here is
modify the room coordinates so that each of the walls are now 40 units long and
20 units high. Also note that we have changed the length of the u,v coordinate
now to 5 so that the texture fits exactly eight times along each wall. Here is the
Moving into 3D | 101
Quake 2 Model Example
code for one of the polygons that represents the room so you can see the
changes:
p->CreateVertex (csVector3 (-20, 0, 20));
p->CreateVertex (csVector3 (20, 0, 20));
p->CreateVertex (csVector3 (20, 0, -20));
p->CreateVertex (csVector3 (-20, 0, -20));
p->SetTextureSpace(p->GetVertex (0), p->GetVertex (1), 5);
The next change is that we have added an extra light and changed the positions
of the lights so that one is placed in the center of each corner of the room at a
height of seven units. In addition, we have changed the radius that the lights
span from 10 units to 20 to take into account the larger area the room now cov-
ers. The modified light definitions can be seen here:
light = engine->CreateLight (NULL, csVector3 (-10, 7, 0), 20, csColor
(1, 1, 1), false);
ll->Add (light->QueryLight ());
Next comes a more interesting part where we actually load in the model. The
first thing we do is load in the skin for the model. Recall that we made that
minor change to the xml as follows:
<material>skin</material>
What we are saying here is that the material for the model will be found by the
name “skin” in our application. Therefore, when we load in our skin texture
from marine.zip on the VFS, we simply need to give it the name skin to associ-
ate it with the model. Of course we can later change the texture we use and still
call it skin, or alternatively we can modify the material the model uses at run
time, if for example, we wanted to give the impression of the player changing
clothes.
Anyway, back to the code. So to load in the skin for our model, we use the
LoadTexture method of our iLoader interface, which we previously acquired as
the pointer loader:
iTextureWrapper* txt = loader->LoadTexture ("skin", "/lib/marine/brownie.png",
CS_TEXTURE_3D, txtmgr, true);
The first parameter of this method is the name for the texture. We have called it
skin in this example so that it will be associated correctly with our model
(remember, we name the material for the model “skin” in the 3D sprite file).
The next parameter is the name of our texture, which we specify as
102 | Chapter 4
Quake 2 Model Example
Now that we have loaded in the model’s skin, we then load in a mesh factory
from the 3D sprite file, which we can then use to make the model from. This is
achieved by calling the LoadMeshObjectFactory method of the loader object,
passing in the VFS path to our tris.spr file:
csRef<iMeshFactoryWrapper> imeshfact (loader->LoadMeshObjectFactory
("/lib/marine/tris.spr"));
Do not be confused by the term “factory”; all it means is that it is used in the
same way as a real factory — to make things J. So we now have a reference to
an iMeshFactoryWrapper, called imeshfact. Before we continue, we ensure that
we have a valid pointer by comparing it to NULL:
if (imeshfact == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Error loading mesh object factory!");
return false;
}
We can then create our actual mesh from the factory by calling the
CreateMeshWrapper method on our engine object:
player = (engine->CreateMeshWrapper(imeshfact, "PlayerSprite", room, csVector3
(-3, 3, 3)));
As you can see, we first pass in the factory that we wish to create the mesh from,
which in this example is imeshfact, and then we pass in a name for our mesh so
it can be referenced later (in case we need to reference it). Next, we pass in the
sector that the mesh will appear in, which we specify as our room sector, and set
the initial position for the mesh as csVector3.
Then we adjust the scaling of the model to make it the correct size for our
level. To do this, we obtain the identity matrix, multiply it by five, then apply
Moving into 3D | 103
Quake 2 Model Example
this as a hard transformation to the mesh factory, as shown in the following seg-
ment of code.
csMatrix3 m;
m.Identity ();
m *= 5.0;
Once we have this, we can set the starting animation for our model (player) by
calling the SetAction method, passing in the name of the action we wish it to be
doing. At the moment we want it just to stand, so we pass in the string "stand":
playerstate->SetAction ("stand");
To find all the possible actions a model has, open up the sprite file (tris.spr) after
it has been converted into XML and do a search for the word “action.” The first
one you will find is the stand action, which will look as follows:
<action name="stand">
If you keep searching you will find run and many other actions that the model
can perform. Be careful, though: The actions may not always be named the same
in each different model file.
The final addition to our Initialize method is the following line of code:
player->DeferUpdateLighting (CS_NLIGHT_STATIC|CS_NLIGHT_DYNAMIC, 10);
Basically what this does is tell the engine to only calculate the lighting for the
model once it is visible on the screen, where the first parameter specifies what
light types this refers to (in this case static and dynamic lights) and the second
specifies the maximum number of lights to use to calculate the object’s lighting.
Now that we have seen how to load and initialize the object, let’s see what is
involved in the movement and rendering process. Let’s jump to the SetupFrame
method.
The first thing we have modified here is the camera movement code. All we
have done is remove the handling code for rotating the camera left and right, as
this is now redundant, leaving the camera movement code looking as follows:
iCamera* c = view->GetCamera();
if (kbd->GetKeyState (CSKEY_PGUP))
c->Move(csVector3(0, 1, 0) * 4 * speed);
if (kbd->GetKeyState (CSKEY_PGDN))
c->Move(csVector3(0, 1, 0) * 4 * speed * -1);
if (kbd->GetKeyState (CSKEY_UP))
c->Move (CS_VEC_FORWARD * 4 * speed);
if (kbd->GetKeyState (CSKEY_DOWN))
c->Move (CS_VEC_BACKWARD * 4 * speed);
104 | Chapter 4
Quake 2 Model Example
We then want to check the w to see if the player wishes to move the character
forward.
if(kbd->GetKeyState ('w'))
{
If so, we check to see if the player is already walking. If the model is not, then
we call the SetAction method on the playerstate object, setting the action to the
run action. After this we will be assured the model will be in the walking state,
and we set the isWalking flag to true. This can all be seen here.
if(isWalking == false)
playerstate->SetAction("run");
isWalking = true;
To move the model forward in the direction it is facing, we use the following
line of code:
player->GetMovable()->SetPosition(player->GetMovable()->
GetTransform().This2Other(csVector3(1,0,0) * speed * 5));
If you said “argh” after seeing that, I would not blame you. J But it’s not that
bad actually. First we get a reference to the iMovable interface by calling the
GetMovable method on the player object. Then we call the SetPosition method,
which allows us to adjust the position of the object. In this method, we then
make a call to the GetTransform method on the player’s iMovable reference,
retrieving the current transformation of the model. On this we call This2Other,
passing in our untransformed forward vector, scaled by the scalar speed at which
we wish to move. The This2Other method gives us the correct new position of
the player, taking into account the player’s current transformation.
After we have moved the player forward, we need to make the call to
UpdateMove so the transformations are applied. This can be seen here:
player->GetMovable()->UpdateMove();
If “w” was not pressed, the player will no longer be moving, so we need to
ensure the animation is returned to the stand animation. We do this with the fol-
lowing segment of code:
else
{
if(isWalking == true)
playerstate->SetAction("stand");
isWalking = false;
}
Note that if we did not use the isWalking variable and just set the action every
frame, the animation would never actually happen, as when the SetAction
method is called, the animation is restarted from the beginning again.
The next thing we handle is if the player wishes to turn to the left by pressing
the “a” key. This is handled with the following segment of code:
Moving into 3D | 105
Quake 2 Model Example
if(kbd->GetKeyState ('a'))
{
player->GetMovable()->GetTransform().RotateThis(csVector3(0, -1, 0),
speed * 4);
player->GetMovable()->UpdateMove();
}
As you can see, all we do is obtain the iMovable interface, obtain the current
transformation of the model, then call the method RotateThis. Into the
RotateThis method we pass in the axis we wish to rotate the model around, fol-
lowed by the angle in radians, which we base on the speed variable. Again note
that we call UpdateMove to perform the new transformation.
The same technique is then used to rotate the model to the right, as shown
Y
below.
if(kbd->GetKeyState ('d'))
{
FL
player->GetMovable()->GetTransform().RotateThis(csVector3(0, 1, 0),
speed * 4);
AM
player->GetMovable()->UpdateMove();
}
Next we add the following line of code to make the camera always face the
model.
TE
view->GetCamera()->GetTransform().LookAt(player->GetMovable()->GetPosition()+
csVector3(0, 1, 0)-view->GetCamera()->GetTransform().GetOrigin(),
csVector3(0, 1, 0));
Then we add the vector (0, 1, 0) to this, so the camera is not pointing at the
ground below the player. Finally, we simply deduct the current position of the
camera, which is obtained as follows:
view->GetCamera()->GetTransform().GetOrigin()
For rendering, we need to make no changes as Crystal Space does the magic for
us.
As you saw in that example, being able to walk through walls is not ideal, so
let’s now move on to the next example, where we look at how to add basic colli-
sion detection between the player and the walls.
Team-Fly®
106 | Chapter 4
Adding Simple Collision Detection
#include "imap/parser.h"
#include "ivaria/reporter.h"
#include "ivaria/stdrep.h"
#include "ivaria/collider.h" // NEW
#include "igeom/polymesh.h" // NEW
#include "csutil/cmdhelp.h"
CS_IMPLEMENT_APPLICATION
isWalking = false;
Butterfly::~Butterfly ()
{
}
bool Butterfly::LoadPixMaps ()
{
return true;
}
void Butterfly::SetupFrame ()
{
// NEW ->
// <- NEW
108 | Chapter 4
Adding Simple Collision Detection
// Check input...
// Get elapsed time from the virtual clock.
csTicks elapsed_time = vc->GetElapsedTicks ();
iCamera* c = view->GetCamera();
if (kbd->GetKeyState (CSKEY_PGUP))
c->Move(csVector3(0, 1, 0) * 4 * speed);
if (kbd->GetKeyState (CSKEY_PGDN))
c->Move(csVector3(0, 1, 0) * 4 * speed * -1);
if (kbd->GetKeyState (CSKEY_UP))
c->Move(CS_VEC_FORWARD * 4 * speed);
if (kbd->GetKeyState (CSKEY_DOWN))
c->Move(CS_VEC_BACKWARD * 4 * speed);
// Player movement...
if(kbd->GetKeyState ('w'))
{
if(isWalking == false)
playerstate->SetAction("run");
isWalking = true;
player->GetMovable()->SetPosition(player->GetMovable()->
GetTransform().This2Other(csVector3(1, 0, 0) *
speed * 25));
player->GetMovable()->UpdateMove();
// NEW ->
// <- NEW
}
else
{
if(isWalking == true)
playerstate->SetAction("stand");
isWalking = false;
}
Moving into 3D | 109
Adding Simple Collision Detection
if(kbd->GetKeyState ('a'))
{
player->GetMovable()->GetTransform().RotateThis
(csVector3(0, -1, 0), speed * 4);
player->GetMovable()->UpdateMove();
}
if(kbd->GetKeyState ('d'))
{
player->GetMovable()->GetTransform().RotateThis
(csVector3(0, 1, 0), speed * 4);
player->GetMovable()->UpdateMove();
}
g3d->FinishDraw();
// Begin 2D rendering...
if (!g2d->BeginDraw ())
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Failed to begin 2D frame");
return;
}
if(logoImg)
{
logoImg->DrawScaled(g3d, g2d->GetWidth()-logoImg->Width()-10,
g2d->GetHeight()-logoImg->Height()-10, logoImg->Width(),
logoImg->Height());
}
// draw text...
int fntcol = g2d->FindRGB (255, 255, 0);
char buf[256];
sprintf(buf, "Butterfly Grid");
g2d->Write(font, 10,10, fntcol, -1, buf);
}
110 | Chapter 4
Adding Simple Collision Detection
void Butterfly::FinishFrame ()
{
g2d->FinishDraw ();
g2d->Print (NULL);
}
return false;
}
bool Butterfly::Initialize ()
{
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("crystalspace.collisiondetection.rapid",
iCollideSystem), // NEW
CS_REQUEST_END))
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Can't initialize plugins!");
return false;
Moving into 3D | 111
Adding Simple Collision Detection
// NEW ->
// <- NEW
// 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.butterfly",
"Error opening system!");
return false;
}
txtmgr = g3d->GetTextureManager();
txtmgr->SetVerbose (true);
iMaterialWrapper* tm =
engine->GetMaterialList ()->FindByName ("butterflytexture");
iPolygon3D* p;
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-20, 0, 20));
p->CreateVertex (csVector3 (20, 0, 20));
p->CreateVertex (csVector3 (20, 0, -20));
p->CreateVertex (csVector3 (-20, 0, -20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-20, 20, -20));
p->CreateVertex (csVector3 (20, 20, -20));
p->CreateVertex (csVector3 (20, 20, 20));
p->CreateVertex (csVector3 (-20, 20, 20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-20, 20, 20));
p->CreateVertex (csVector3 (20, 20, 20));
p->CreateVertex (csVector3 (20, 0, 20));
p->CreateVertex (csVector3 (-20, 0, 20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (20, 20, 20));
p->CreateVertex (csVector3 (20, 20, -20));
p->CreateVertex (csVector3 (20, 0, -20));
p->CreateVertex (csVector3 (20, 0, 20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-20, 20, -20));
p->CreateVertex (csVector3 (-20, 20, 20));
p->CreateVertex (csVector3 (-20, 0, 20));
p->CreateVertex (csVector3 (-20, 0, -20));
114 | Chapter 4
Adding Simple Collision Detection
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (20, 20, -20));
p->CreateVertex (csVector3 (-20, 20, -20));
p->CreateVertex (csVector3 (-20, 0, -20));
p->CreateVertex (csVector3 (20, 0, -20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
csRef<iStatLight> light;
iLightList* ll = room->GetLights ();
csMatrix3 m;
m.Identity ();
Moving into 3D | 115
Adding Simple Collision Detection
m *= 5.0;
// NEW ->
Y
playerCollider = CreateCollider(player);
if (playerCollider == NULL)
{
FL
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
AM
"Error creating playerCollider!");
return false;
}
TE
mapCollider = CreateCollider(walls);
if (mapCollider == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Error creating mapCollider!");
return false;
}
// <- NEW
engine->Prepare ();
font = g2d->GetFontServer()->LoadFont(CSFONT_LARGE);
return true;
}
void Butterfly::Start ()
{
Team-Fly®
116 | Chapter 4
Adding Simple Collision Detection
csDefaultRunLoop (object_reg);
}
// NEW ->
iCollider* Butterfly::CreateCollider(iMeshWrapper* mesh)
{
csRef<iPolygonMesh> polmesh (SCF_QUERY_INTERFACE (mesh->GetMeshObject (),
iPolygonMesh));
if (polmesh)
{
csColliderWrapper* wrap = new csColliderWrapper
(mesh->QueryObject (), cdsys, polmesh);
wrap->DecRef ();
return wrap->GetCollider ();
}
else
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.simpcd",
"Object doesn't support collision detection!");
return NULL;
}
}
// <- NEW
csInitializer::DestroyApplication (object_reg);
return 0;
}
#include <stdarg.h>
#include "csutil/ref.h"
struct iObjectRegistry;
struct iEngine;
struct iLoader;
struct iGraphics2D;
struct iGraphics3D;
struct iKeyboardDriver;
struct iVirtualClock;
struct iEvent;
Moving into 3D | 117
Adding Simple Collision Detection
struct iView;
struct iTextureManager;
struct iFont;
struct iSector;
struct iSprite3DState;
struct iMeshWrapper;
class Butterfly
{
private:
iObjectRegistry* object_reg;
csRef<iEngine> engine;
csRef<iLoader> loader;
csRef<iGraphics2D> g2d;
csRef<iGraphics3D> g3d;
csRef<iKeyboardDriver> kbd;
csRef<iVirtualClock> vc;
csRef<iView> view;
csRef<iTextureManager> txtmgr;
csRef<iSector> room;
csRef<iCollideSystem> cdsys; // NEW
csPixmap* logoImg;
csRef<iFont> font;
csRef<iMeshWrapper> player;
csRef<iSprite3DState> playerstate;
bool isWalking;
bool Initialize();
void Start();
};
#endif // __BUTTERFLY_H__
118 | Chapter 4
Adding Simple Collision Detection
If you run this example, you find that you now cannot go through any of the
walls. Let’s see how this was implemented, first taking a look at the changes we
have made to the header file.
First we have added a smart pointer to an iCollideSystem interface, which
contains the important method Collide, discussed shortly. The definition for this
is:
csRef<iCollideSystem> cdsys;
Then we have added two pointers to iCollider interfaces, one for the player and
one for the walls:
iCollider* playerCollider;
iCollider* mapCollider;
The final addition here is a prototype for a helper method called CreateCollider,
which we will use to obtain an iCollider interface pointer from an
iMeshWrapper object. Here is the prototype for this new method:
iCollider* CreateCollider (iMeshWrapper* mesh);
Let’s now delve into the main source file and see what we have changed and
added there.
First, in the Butterfly class constructor, we have set the initial values of our
playerCollider and mapCollider pointers to NULL:
playerCollider = NULL;
mapCollider = NULL;
Next, if you now look at the Initialize method, you’ll notice on the first line we
have a call to RequestPlugins. Since the RAPID collision detection system is
also a plug-in, we need to specify here that we also wish to load it. So at the end
of the list, we have added the following:
CS_REQUEST_PLUGIN("crystalspace.collisiondetection.rapid", iCollideSystem)
The next part we have added is to actually obtain a reference to the collision
detection system. This is done in a similar way to all the other elements, such as
graphics and input, and can be seen in the following block of code:
cdsys = CS_QUERY_REGISTRY (object_reg, iCollideSystem);
if (!cdsys)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.simpcd",
"Can't find the collision detection system!");
return false;
}
We then create iColliders for the player and walls meshes. This is done via our
helper function CreateCollider:
playerCollider = CreateCollider(player);
if (playerCollider == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Error creating playerCollider!");
return false;
}
mapCollider = CreateCollider(walls);
if (mapCollider == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Error creating mapCollider!");
return false;
}
Let’s now look at the CreateCollider method so we can see how iCollider is
obtained from iMeshWrapper. As you know from the prototype and the code
above, the CreateCollider method takes a pointer to an iMeshWrapper object and
then returns a pointer to an iCollider object. This can be seen here:
iCollider* Butterfly::CreateCollider(iMeshWrapper* mesh)
{
The first thing we do in this method is obtain an iPolygonMesh pointer from the
mesh object contained within the iMeshWrapper:
csRef<iPolygonMesh> polmesh (SCF_QUERY_INTERFACE (mesh->GetMeshObject (),
iPolygonMesh));
We then decrement the reference count and return a pointer to the actual
iCollider object by making a call to the GetCollider method of the
csColliderWrapper class.
wrap->DecRef ();
return wrap->GetCollider();
That is all the changes and additions to the Initialize method now, so let’s take a
look at what we have added to the SetupFrame method in order to test for the
collisions.
First, we store the initial transformation of the player, before we apply any
movement to it. This is achieved with the following line of code.
csReversibleTransform oldPlayerTrans = player->GetMovable()->GetTransform();
Note the reason we are storing this is because if we do detect that a collision has
occurred after we move the player, we can simply restore this transformation
and the player will return to the previous position.
We are only actually interested in collisions when the player is moving for-
ward, as we always wish the player to be able to rotate freely (in this example
anyway). So we have placed the collision detection code within the “w”
keypress if statement. Let’s look at how it works.
First we make a call to ResetCollisionPairs, which basically resets the colli-
sion detection system.
cdsys->ResetCollisionPairs();
Next, we need to get the full transformation of the player and the walls (i.e., the
level geometry). We do this by first getting the iMovable interface, and then
from that we call GetFullTransform. Then finally we store the result in a
csReversibleTransform.
csReversibleTransform ft1 = player->GetMovable()->GetFullTransform();
csReversibleTransform ft2 = walls->GetMovable()->GetFullTransform();
Once we have this information we can plug it into the Collide method available
from the collision detection system (cdsys). This Collide method simply returns
true or false, letting you know if the objects collided or not. Here is the line of
code where we call the method:
bool collision = cdsys->Collide(playerCollider, &ft1, mapCollider, &ft2);
Moving into 3D | 121
Creating and Loading a Map
And that is all there is to it. Note, however, that this is very basic collision detec-
tion, but I’ll leave you to expand upon it.
Ü method
Note A more advanced method of collision detection is to use the CollidePath
of the collision detection system. This can also be used for features such
as gravity.
' http://www.valve-erc.com/content/?page=utilities
Figure 4-17: The 2D views tab Figure 4-18: The 3D views tab
After these three tabs have been configured, do not close the window; instead
download the following two texture packs. Once they are downloaded, place the
cstex_1.wad and cstex_2.wad files into the following folder.
c:\crystal\wads
' ftp://sunsite.dk/projects/crystal/support/map2cs/cstex_1.zip
' ftp://sunsite.dk/projects/crystal/support/map2cs/cstex_2.zip
Move to the next tab in the window called Textures. Click the Add WAD button
and add both the .wad files that you just extracted to the c:\crystal\wads folder.
Once this is done the window should look like the following.
Moving into 3D | 123
Creating and Loading a Map
Next, select the Game Configurations tab and click the Edit button to the right of
the Configurations pull-down. You should now see the following dialog.
On this dialog, click Add and enter Crystal Space for the name of the game.
Then click the OK button and the Close button.
You should now be back in the Configure Windows Game Configurations
tab. Click the Add button to the right of the Game Data files text area. When you
click it, you will be presented with a browse dialog. Now browse to the follow-
ing folder:
c:\crystal\hammer\fgd\half-life
124 | Chapter 4
Creating and Loading a Map
Select the halflife.fgd file and click Open. The window should now look as
follows.
Now let’s get started on our map. First, select File followed by New… from the
main menu. Your screen should now look as follows.
Moving into 3D | 125
Creating and Loading a Map
Y
FL
AM
TE
Like me, you’re probably not an artist, but we can create a simple map to dem-
onstrate the process. So from the toolbar at the left, select the following icon:
This tool allows us to draw primitive objects, with the default being a solid rect-
angular block.
Now, in the top-right view, drag out a box so that the center is roughly where
the green lines intersect (the origin). Once you do this is should look similar to
the following:
Team-Fly®
126 | Chapter 4
Creating and Loading a Map
We have now defined how large our room will be if we were looking down on it.
However, if you look at the front and side views, you can see that our room has
a very low ceiling. So what we want to do now is drag the top-middle white
square (handle) up a bit, making it look somewhat like the following.
Now that we are happy with the dimensions of our room, press the Return key.
The white rectangle you drew should turn a light blue color and the handles will
disappear, as shown in the following figure.
Next, click on the Selection tool: . Then click on the border of the box we
drew (in the top, front, or side view). When it is selected, it will turn red and the
handles will appear again. Select Tools from the main menu, then click the Make
Hollow option. When you do this, you will be asked how thick you would like
the walls. Just leave this at the default value of 32 and click OK. Your screen
should now look as follows.
128 | Chapter 4
Creating and Loading a Map
If it is not already, ensure the top of the floor of the room is running along the
green shaded line. If you need to move the room, simply use the Selection tool
and drag it as required.
Now that we have our room, it would be good to add something
semi-interesting into it, so we are now going to add a box inside our scene (I did
say semi-interesting J). To do this, use the Primitives tool as before when creat-
ing the room and basically create a mini-room instead this one, leaving out the
hollowing part as this is not required. After you have repeated the previous
steps, your room should now look similar to the following:
Moving into 3D | 129
Creating and Loading a Map
The final thing we are going to add to this masterpiece of level design is a light.
Recall that we hard coded the position of the lights in our previous examples;
however, it is much easier to simply place them within the map.
So, first select the Entities tool: . Once it’s selected, you’ll notice that the
Categories pull-down menu displays “(entities)” and the Objects pull-down dis-
plays “aiscripted_sequence.”
What we need to do is change the Object pull-down to the “light” option.
Now click anywhere inside our room.When you do this, you should see a green
square with horizontal and vertical lines coming out of each side. Drag this
green box to about the center of the room, just below the ceiling. It should now
look as follows.
130 | Chapter 4
Creating and Loading a Map
After you’re happy with its location, simply press the Return key. Note that it
turns pink.
Now go to the Selection tool (the top one that looks like an arrow) and select
the light by left-clicking on it, then right-click on it to bring up the menu for it.
On this menu click the Properties option and you will then be presented with the
following.
Select the Brightness attribute and you will then see the following:
The four triplets you can now see to the right represent the red, green, and blue
color of the light (in the range of 0 to 255) and the radius for the light. All we
want to do here is change the radius to the value of 75, so it reads: 255 255 128
75.
Close this window using the x. Finally, we are going to apply texturing, first
to the whole room, then the floor, and finally the “crate” we added. To do this,
first use the Selection tool to select the room and then click the Browse button
on the top at the right-hand side of the main display. You will get a visual dis-
play of all the available textures. Pick a texture that you think suits the walls and
room and then double-click on the one you like. When you do this, you should
see the texture appear in the preview box to the left of the browse button.
Next, ensuring the room is selected (and highlighted in red), click on the
Apply Texture button: . Once this is done, again using the Selection tool,
right-click on the highlighted room and pick the Ungroup option from the
pop-up menu. Then, deselect the room by clicking to the side of it, and then
select the floor of the room. The screen should then look as follows:
132 | Chapter 4
Creating and Loading a Map
Once the floor is selected, pick a different texture using the browse button, then
simply click the Apply Texture button.
Finally, use the Selection tool to select the crate and apply a suitable texture
to it using the same technique we used for the entire room.
When you do this, the screen should look similar to the following:
If you now check in the c:\crystal\cs\data folder, you will notice a new zip file
called testmap.zip. The next step in the conversion process is to convert the
world file contained within the zip into XML format. So to do this, first change
the directory at the command prompt to c:\crystal\cs, then execute the following
command:
cs2xml data\testmap.zip
Once this is done, open up the zip file, extract the world file from the zip, and
open it up using WordPad. Once it’s open, go to Edit in the main menu, pick
Find, and enter the following search string:
moveable
There will only be one occurrence of moveable. When you find it, delete the
entire line that it is on. The line you must delete is shown in the following screen
shot (note that your world file may look different).
After this is done, save the file, then replace the one currently in the zip file with
this one.
Next, go back to the command prompt, and enter the following command in
the c:\crystal\cs folder:
levtool –dynavis data/testmap.zip
This performs the final preparation that is required before loading the map. Then
finally, before we create the code to view it, we need to add the following line
into the vfs.cfg file, located in the c:\crystal\CS folder:
VFS.Mount.lib/testmap = $@data$/testmap.zip
#include "ivideo/material.h"
#include "ivideo/fontserv.h"
#include "igraphic/image.h"
#include "igraphic/imageio.h"
#include "imap/parser.h"
#include "ivaria/reporter.h"
#include "ivaria/stdrep.h"
#include "ivaria/collider.h"
#include "igeom/polymesh.h"
#include "csutil/cmdhelp.h"
Y
CS_IMPLEMENT_APPLICATION
FL
// The global pointer to our application...
Butterfly *butterfly;
AM
Butterfly::object_reg = object_reg;
isWalking = false;
playerCollider = NULL;
Butterfly::~Butterfly ()
{
// NEW ->
bool Butterfly::LoadMap()
{
// Set VFS current directory to the level we want to load.
csRef<iVFS> VFS (CS_QUERY_REGISTRY (object_reg, iVFS));
VFS->ChDir ("/lib/testmap");
Team-Fly®
136 | Chapter 4
Creating and Loading a Map
return true;
}
// <- NEW
bool Butterfly::LoadPixMaps ()
{
return true;
}
void Butterfly::SetupFrame ()
{
// Check input...
// Get elapsed time from the virtual clock.
csTicks elapsed_time = vc->GetElapsedTicks ();
iCamera* c = view->GetCamera();
if (kbd->GetKeyState (CSKEY_PGUP))
c->Move(csVector3(0, 1, 0) * 4 * speed);
if (kbd->GetKeyState (CSKEY_PGDN))
c->Move(csVector3(0, 1, 0) * 4 * speed * -1);
if (kbd->GetKeyState (CSKEY_UP))
c->Move (CS_VEC_FORWARD * 4 * speed);
if (kbd->GetKeyState (CSKEY_DOWN))
c->Move (CS_VEC_BACKWARD * 4 * speed);
// Player movement...
if(kbd->GetKeyState ('w'))
{
if(isWalking == false)
playerstate->SetAction("run");
isWalking = true;
player->GetMovable()->SetPosition(player->GetMovable()->
GetTransform().This2Other(csVector3(1,0,0) * speed * 25));
Moving into 3D | 137
Creating and Loading a Map
player->GetMovable()->UpdateMove();
csRef<iMeshList> ml (engine->GetMeshes());
}
}
}
else
{
if(isWalking == true)
playerstate->SetAction("stand");
isWalking = false;
}
if(kbd->GetKeyState ('a'))
{
player->GetMovable ()->GetTransform ().RotateThis
(csVector3(0, -1, 0), speed * 4);
player->GetMovable()->UpdateMove();
}
if(kbd->GetKeyState ('d'))
{
player->GetMovable ()->GetTransform ().RotateThis
(csVector3(0, 1, 0), speed * 4);
player->GetMovable()->UpdateMove();
}
// NEW ->
iLight *lights;
int lightcount = engine->GetNearbyLights (room, player->GetMovable()->
GetPosition(), CS_NLIGHT_STATIC, &lights, 20);
player->UpdateLighting(&lights, lightcount);
// <- NEW
g3d->FinishDraw();
// Begin 2D rendering...
if (!g2d->BeginDraw ())
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Failed to begin 2D frame");
return;
}
if(logoImg)
{
logoImg->DrawScaled (g3d, g2d->GetWidth()-logoImg->Width()-10,
g2d->GetHeight()-logoImg->Height()-10, logoImg->Width(),
logoImg->Height());
}
// draw text...
int fntcol = g2d->FindRGB (255, 255, 0);
char buf[256];
sprintf(buf, "Butterfly Grid");
g2d->Write(font, 10,10, fntcol, -1, buf);
}
void Butterfly::FinishFrame ()
{
g2d->FinishDraw ();
g2d->Print (NULL);
}
return true;
}
else if (ev.Type == csevBroadcast && ev.Command.Code == cscmdFinalProcess)
{
butterfly->FinishFrame ();
return true;
}
else if (ev.Type == csevKeyDown && ev.Key.Code == CSKEY_ESC)
{
csRef<iEventQueue> q (CS_QUERY_REGISTRY (object_reg, iEventQueue));
if (q) q->GetEventOutlet()->Broadcast(cscmdQuit);
return true;
}
return false;
}
bool Butterfly::Initialize ()
{
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("crystalspace.collisiondetection.rapid",
iCollideSystem), // NEW
CS_REQUEST_END))
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Can't initialize plugins!");
return false;
}
{
csCommandLineHelper::Help (object_reg);
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.butterfly",
"Error opening system!");
return false;
}
txtmgr = g3d->GetTextureManager();
txtmgr->SetVerbose (true);
iMaterialWrapper* tm =
engine->GetMaterialList ()->FindByName ("butterflytexture");
// NEW ->
LoadMap();
// <- NEW
csRef<iMeshFactoryWrapper> imeshfact (
loader->LoadMeshObjectFactory ("/lib/marine/tris.spr"));
if (imeshfact == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Error loading mesh object factory!");
return false;
}
csMatrix3 m;
m.Identity ();
m *= 5.0;
csReversibleTransform l_rT=csReversibleTransform();
l_rT.SetT2O (m);
l_rT.SetOrigin (csVector3(0, 0, 0));
imeshfact->HardTransform (l_rT);
playerCollider = CreateCollider(player);
if (playerCollider == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Error creating playerCollider!");
return false;
}
font = g2d->GetFontServer()->LoadFont(CSFONT_LARGE);
return true;
}
void Butterfly::Start ()
{
csDefaultRunLoop (object_reg);
}
csInitializer::DestroyApplication (object_reg);
return 0;
}
#include <stdarg.h>
#include "csutil/ref.h"
struct iObjectRegistry;
struct iEngine;
struct iLoader;
struct iGraphics2D;
struct iGraphics3D;
struct iKeyboardDriver;
struct iVirtualClock;
struct iEvent;
struct iView;
struct iTextureManager;
struct iFont;
struct iSector;
struct iSprite3DState;
struct iMeshWrapper;
class Butterfly
{
private:
iObjectRegistry* object_reg;
csRef<iEngine> engine;
csRef<iLoader> loader;
csRef<iGraphics2D> g2d;
csRef<iGraphics3D> g3d;
csRef<iKeyboardDriver> kbd;
csRef<iVirtualClock> vc;
csRef<iView> view;
csRef<iTextureManager> txtmgr;
csRef<iSector> room;
csRef<iCollideSystem> cdsys;
csPixmap* logoImg;
csRef<iFont> font;
csRef<iMeshWrapper> player;
csRef<iSprite3DState> playerstate;
bool isWalking;
iCollider* playerCollider;
Moving into 3D | 145
Creating and Loading a Map
Y
public:
Butterfly(iObjectRegistry* object_reg);
~Butterfly();
FL
bool Initialize();
AM
void Start();
};
#endif // __BUTTERFLY_H__
TE
If we run this in a project that has been set up correctly, we should see some-
thing similar to the following.
Team-Fly®
146 | Chapter 4
Creating and Loading a Map
As you can see from the screen shot, Crystal Space also handles shadows, giving
our map that extra level of detail, which is really great. How did we load it then?
Let’s take a look.
The first thing we have added is the following line to the header file:
csPArray<iCollider> mapColliderArray;
This creates an expandable pointer array for holding iCollider objects. The
csPArray is somewhat similar to the C++ STL (standard template libraries) vec-
tor and also Java’s ArrayList and Vector implementations. What we’re going to
use this for is to hold a list of colliders for all the meshes that are contained
within our map.
Now in the source file, let’s look at what we have added to the Initialize
method.
The first new part we come to is the call to our new method LoadMap, so
let’s look to see what this method does.
In this method, we first obtain a pointer to the virtual file system and then
make the current directory that of our map (remember we added the extra line to
the vfs.cfg file earlier). This can be seen here:
csRef<iVFS> VFS (CS_QUERY_REGISTRY (object_reg, iVFS));
VFS->ChDir ("/lib/testmap");
Then, since we are now in the testmap directory (which is in reality the zip file),
we load in the world file using the method LoadMapFile, which is available
from the iLoader interface.
if (!loader->LoadMapFile ("world"))
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Couldn't load level!");
return false;
}
After the map is loaded, we call the Prepare method on our engine object as
follows:
engine->Prepare ();
We then place the camera in the map by finding the room sector, which is the
default for all maps, then we set that to be the sector the camera is located in and
assign the position of it as we did in previous examples. This is done as follows:
room = engine->GetSectors ()->FindByName ("room");
view->GetCamera ()->SetSector (room);
view->GetCamera ()->GetTransform ().SetOrigin (csVector3 (-3, 5, -3));
Next, since our map may be made up of multiple meshes, we need to generate a
list of colliders, one for each of the meshes, so that we can test our player object
against each of them as it’s moving about. To do this, we first obtain a list of
meshes from the map by calling the GetMeshes method of our engine object.
This is stored in a pointer called meshlist, which is of type iMeshList.
csRef<iMeshList> meshlist (engine->GetMeshes());
Moving into 3D | 147
Creating and Loading a Map
Then after this, we can find out how many meshes there were by calling the
GetCount method of the meshlist object. We create a collider for each of the
meshes in the mesh list by calling the CreateCollider method we made in the last
example. Then we push it onto our mapColliderArray, as shown below:
for(int i=0; i < meshlist->GetCount(); i++)
{
csRef<iMeshWrapper> mw (meshlist->Get(i));
mapColliderArray.Push(CreateCollider(mw));
}
The next change we have made is where the collision detection occurs — when
the player moves forward. Instead of simply checking the player against a mesh
now, we need to cycle through the array we generated before to check against all
the meshes. So we first get a list of meshes from the engine, as we did
previously.
csRef<iMeshList> ml (engine->GetMeshes());
Then, because we don’t want to take into account any additional meshes (such
as the player) that were added after we generated our mapColliderArray, we run
a for loop for the number of entries in mapColliderArray:
for(int i=0; i<mapColliderArray.Length(); i++)
For each of these, we first reset the collision detection system using the follow-
ing line of code:
cdsys->ResetCollisionPairs();
Then we get the full transform of the current mesh, calling the Get method to
retrieve the mesh at the current index i of our for loop:
csReversibleTransform ft2 = ml->Get(i)->GetMovable ()->GetFullTransform ();
We then check the collision between playerCollider and the collider at the cur-
rent index of the for loop — retrieved by calling Get on our mapColliderArray,
passing in i.
bool collision = cdsys->Collide(playerCollider, &ft1, mapColliderArray.Get(i),
&ft2);
The rest of the collision detection is the same as in the previous example, but
can be seen here again for reference.
if (collision)
{
// Restore old transforms.
player->GetMovable ()->SetTransform(oldPlayerTrans);
player->GetMovable ()->UpdateMove ();
}
}
After our collision detection, the final part we have added is a few lines to
update the model’s lighting every frame to give it a more realistic effect.
148 | Chapter 4
Summary
To do this, we first get a list of lights that are near the player using the fol-
lowing code:
iLight *lights;
int lightcount = engine->GetNearbyLights (room, player->GetMovable()
->GetPosition(), CS_NLIGHT_STATIC, &lights, 20);
As you can see, all we do is create a pointer to an iLight interface, then make a
call to the GetNearbyLights method on our engine object. Into this method we
pass in the current sector, followed by the player’s current position, the type of
lighting we want to get information on (static lighting), a pointer to the iLight
pointer, and the maximum number of lights that we want to retrieve. Note that
the return value of this method tells us the actual number of lights retrieved.
After we have the pointer to the list of lights and the number of lights, we
simply pass this into the method UpdateLighting, which is a member of the
iMeshWrapper interface. This can be seen in this final line of code:
player->UpdateLighting(&lights, lightcount);
Summary
Well, there you have it. You have now seen all the basics of 3D programming in
Crystal Space. From this point, you should be able to take this knowledge and
expand upon it by looking though the manual and API guide and just experi-
menting with code and different ideas.
In the next chapter, we are going to get down and dirty with the Object Man-
agement System (OMS) that integrates with our applications to allow access to
the powerful functionality of the Butterfly Grid.
Chapter 5
Introduction
Now that we have an idea of how Crystal Space works, we are going to look at
how to integrate the Butterfly Grid with an existing Crystal Space application.
For this, we are going to use one of the demonstration applications that is dis-
tributed with the Crystal Space SDK.
This chapter is divided into two sections. In the first, we will be using the
OMS (Object Management System) directly without any help from the wrapper.
Then in the second, we will be utilizing the wrapper to see how easy it can be to
perform integration.
Using either method, the outcome will be the same. By the end of this chap-
ter, you will have a simple application in which you can see another logged-in
player and move about using the cursor keys in the respective applications.
Look for the butterfly-client project in the central list on the Projects page, and
proceed by clicking on it. You will then be on the main project page. Click File
149
150 | Chapter 5
Obtaining the Latest OMS Client Libraries
Sharing from the Project tools list, and you will be taken to a screen that looks
as follows.
As you can see, it is possible to download the latest build of the client libraries
here, so continue by clicking on the SDK-current file. You will then be asked to
download a file named sdk-oms.zip, so save this now to a location that you will
remember.
Just to keep everything in the same place, we are going to extract the contents
of the zip file we just downloaded to the c:\crystal directory. When you do this,
this folder should look similar to the following screen shot.
The actual client libraries are located within the butterfly-grid folder, as well as
three sample applications to have a look at.
Integrating the OMS with an Existing Application | 151
Integrating the OMS without the Wrapper
Now that we have the SDK, let’s move on to actually integrating it.
Note that the only minor change we need to make before it will compile is the
location specified for the header file. We need to change this in the demosky.cpp
file from:
#include "apps/demosky/demosky.h"
to:
#include "demosky.h”
Then, in the project settings, we want to add the following additional libraries
into the Link tab:
oms.lib ws2_32.lib
These two libraries are the OMS for the Butterfly Grid and the WinSock 2
library, respectively.
152 | Chapter 5
Integrating the OMS without the Wrapper
Once this is done, you should have no problems compiling, so try this now.
Once compiled, if you run the application, it should look similar to the following
screen shot.
What is happening in this demo is you’re moving around with a sky box follow-
ing you, making it appear that the sky is always around you.
What we are going to do to this application is allow two players to connect to
the server and let them move around the world. As this is a simple(ish) example,
we will be using black cubes to represent the players in the world.
The code for the first example is split into several different files and is based
upon the DemoSky example application supplied with the Crystal Space API.
First, here is the complete code listing for the non-wrapper demo. Examine
the code and try it out; then we will look at how it all works in detail.
Listing 5-1: DemoSky OMS Example 1
DemoSky.cpp
/*
Copyright (C) 1998-2000 by Jorrit Tyberghein
Copyright (C) 2001 by W.C.A. Wijngaards
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#include "cssysdef.h"
#include "cssys/sysfunc.h"
#include "demosky.h"
#include "cstool/proctex.h"
#include "cstool/prsky.h"
#include "cstool/csview.h"
#include "cstool/initapp.h"
#include "csutil/cmdhelp.h"
#include "ivideo/graph3d.h"
#include "ivideo/graph2d.h"
#include "ivideo/natwin.h"
#include "ivideo/txtmgr.h"
#include "ivideo/fontserv.h"
#include "ivaria/conout.h"
#include "imesh/sprite2d.h"
#include "imesh/object.h"
#include "imap/parser.h"
#include "iengine/mesh.h"
#include "iengine/engine.h"
#include "iengine/sector.h"
#include "iengine/camera.h"
#include "iengine/movable.h"
#include "iengine/material.h"
#include "imesh/thing/polygon.h"
#include "imesh/thing/thing.h"
#include "ivaria/reporter.h"
#include "igraphic/imageio.h"
#include "iutil/comp.h"
#include "iutil/eventh.h"
#include "iutil/eventq.h"
#include "iutil/event.h"
#include "iutil/objreg.h"
#include "iutil/csinput.h"
#include "iutil/virtclk.h"
#include "iutil/vfs.h"
// Added
#include "imesh/sprite3d.h"
#include "grid-oms/oms.h"
#include "ClientCreates.h"
154 | Chapter 5
Integrating the OMS without the Wrapper
// End
CS_IMPLEMENT_APPLICATION
DemoSky::DemoSky ()
{
sky = NULL;
sky_f = NULL;
sky_b = NULL;
sky_l = NULL;
sky_r = NULL;
sky_u = NULL;
sky_d = NULL;
flock = NULL;
// Added
//** OMS Variables
m_pOMS = NULL;
m_pcServerIP = NULL;
m_pcServerPort = NULL;
m_pcUsername = NULL;
m_pcPassword = NULL;
m_iGame = 0;
m_iVersion = 0;
m_guidAvatar = 0;
m_vThingList.clear();
// End
}
Integrating the OMS with an Existing Application | 155
Integrating the OMS without the Wrapper
DemoSky::~DemoSky ()
{
// Added
CleanUp();
// End
// Modified
//delete flock;
// End
delete sky;
delete sky_f;
delete sky_b;
delete sky_l;
delete sky_r;
Y
delete sky_u;
delete sky_d;
}
void Cleanup ()
FL
AM
{
csPrintf ("Cleaning up...\n");
iObjectRegistry* object_reg = System->object_reg;
delete System; System = NULL;
TE
csInitializer::DestroyApplication (object_reg);
}
Team-Fly®
156 | Chapter 5
Integrating the OMS without the Wrapper
}
else if (ev.Type == csevBroadcast && ev.Command.Code == cscmdFinalProcess)
{
System->FinishFrame ();
return true;
}
else
{
return System ? System->HandleEvent (ev) : 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_CONSOLEOUT,
CS_REQUEST_END))
{
Report (CS_REPORTER_SEVERITY_ERROR, "Couldn't init app!");
return false;
}
// Open the main system. This will open all the previously loaded plug-ins.
iNativeWindow* nw = myG2D->GetNativeWindow ();
if (nw) nw->SetTitle ("Crystal Space Procedural Sky Demo");
if (!csInitializer::OpenApplication (object_reg))
{
Report (CS_REPORTER_SEVERITY_ERROR, "Error opening system!");
Cleanup ();
exit (1);
}
font = myG2D->GetFontServer()->LoadFont(CSFONT_LARGE);
// Some commercials...
Report (CS_REPORTER_SEVERITY_NOTIFY, "Crystal Space Procedural Sky Demo.");
158 | Chapter 5
Integrating the OMS without the Wrapper
// First disable the lighting cache. Our app is simple enough not to need this.
engine->SetLightingCacheMode (0);
p = walls_state->CreatePolygon ();
p->SetMaterial (imatu);
p->CreateVertex (csVector3 (-size, simi, -size));
p->CreateVertex (csVector3 (size, simi, -size));
p->CreateVertex (csVector3 (size, simi, size));
p->CreateVertex (csVector3 (-size, simi, size));
p = walls_state->CreatePolygon ();
p->SetMaterial (imatf);
p->CreateVertex (csVector3 (-size, size, simi));
p->CreateVertex (csVector3 (size, size, simi));
p->CreateVertex (csVector3 (size, -size, simi));
p->CreateVertex (csVector3 (-size, -size, simi));
p = walls_state->CreatePolygon ();
p->SetMaterial (imatr);
p->CreateVertex (csVector3 (simi, size, size));
p->CreateVertex (csVector3 (simi, size, -size));
p->CreateVertex (csVector3 (simi, -size, -size));
p->CreateVertex (csVector3 (simi, -size, size));
p = walls_state->CreatePolygon ();
p->SetMaterial (imatl);
p->CreateVertex (csVector3 (-simi, size, -size));
p->CreateVertex (csVector3 (-simi, size, size));
p->CreateVertex (csVector3 (-simi, -size, size));
p->CreateVertex (csVector3 (-simi, -size, -size));
p = walls_state->CreatePolygon ();
p->SetMaterial (imatb);
p->CreateVertex (csVector3 (size, size, -simi));
p->CreateVertex (csVector3 (-size, size, -simi));
p->CreateVertex (csVector3 (-size, -size, -simi));
p->CreateVertex (csVector3 (size, -size, -simi));
// Modified
// flock = new Flock(engine, 10, sg, room);
// End
// Added
160 | Chapter 5
Integrating the OMS without the Wrapper
char pcBuffer[1024];
char *pcTempBuffer;
char *pcTemp;
pcTemp = pcBuffer;
pcTempBuffer = strchr(pcTemp, ':');
if ( !pcTempBuffer ) return TRUE;
pcTempBuffer[0] = 0;
pcServerIP = pcBuffer;
pcTemp = pcTempBuffer + 1;
pcTempBuffer = strchr(pcTemp, ':');
if ( !pcTempBuffer ) return TRUE;
pcTempBuffer[0] = 0;
pcServerPort = pcTemp;
pcTemp = pcTempBuffer + 1;
pcTempBuffer = strchr(pcTemp, ':');
Integrating the OMS with an Existing Application | 161
Integrating the OMS without the Wrapper
pcTemp = pcTempBuffer + 1;
pcTempBuffer = strchr(pcTemp, ':');
if ( !pcTempBuffer ) return TRUE;
pcTempBuffer[0] = 0;
pcPassword = pcTemp;
pcTemp = pcTempBuffer + 1;
pcTempBuffer = strchr(pcTemp, ':');
if ( !pcTempBuffer ) return TRUE;
pcTempBuffer[0] = 0;
m_iGame = atoi(pcTemp);
pcTemp = pcTempBuffer + 1;
pcTempBuffer = strchr(pcTemp, ':');
if ( !pcTempBuffer ) return TRUE;
pcTempBuffer[0] = 0;
m_iVersion = atoi(pcTemp);
pcTemp = pcTempBuffer + 1;
pcTempBuffer = strchr(pcTemp, ':');
if ( pcTempBuffer )
{
pcTempBuffer[0] = 0;
pcNPSInPort = pcTemp;
pcTemp = pcTempBuffer + 1;
pcTempBuffer = strchr(pcTemp, ':');
if ( pcTempBuffer )
{
pcTempBuffer[0] = 0;
pcNPSOutPort = pcTemp;
}
}
}
//** OMS
SetUsername(pcUsername);
SetPassword(pcPassword);
CreateOMS(pcServerIP, pcServerPort, pcNPSInPort, pcNPSOutPort);
// End
engine->Prepare ();
return true;
}
void DemoSky::SetupFrame ()
{
// Added
Update();
// End
// Modified
//flock->Update(elapsed_time);
// End
if (kbd->GetKeyState (CSKEY_RIGHT))
view->GetCamera ()->GetTransform ().RotateThis (CS_VEC_ROT_RIGHT, speed);
if (kbd->GetKeyState (CSKEY_LEFT))
view->GetCamera ()->GetTransform ().RotateThis (CS_VEC_ROT_LEFT, speed);
if (kbd->GetKeyState (CSKEY_PGUP))
view->GetCamera ()->GetTransform ().RotateThis (CS_VEC_TILT_UP, speed);
if (kbd->GetKeyState (CSKEY_PGDN))
view->GetCamera ()->GetTransform ().RotateThis (CS_VEC_TILT_DOWN, speed);
if (kbd->GetKeyState (CSKEY_UP))
view->GetCamera ()->Move (CS_VEC_FORWARD * 2.0f * speed);
if (kbd->GetKeyState (CSKEY_DOWN))
view->GetCamera ()->Move (CS_VEC_BACKWARD * 2.0f * speed);
view->Draw ();
// Modified
const char *text = "Escape quits."
" Arrow keys/pgup/pgdown to move.";
// End
int txtx = 10;
int txty = myG2D->GetHeight() - 20;
myG2D->Write(font, txtx+1, txty+1, myG2D->FindRGB(80, 80, 80), -1, text);
myG2D->Write(font, txtx, txty, myG2D->FindRGB(255, 255, 255), -1, text);
}
void DemoSky::FinishFrame ()
{
myG3D->FinishDraw ();
myG3D->Print (NULL);
// Added
FPOINT3 vPosition;
vPosition.x = (view->GetCamera()->GetTransform().GetOrigin().x * X_SCALE) +
X_OFFSET;
vPosition.y = (view->GetCamera()->GetTransform().GetOrigin().z * Y_SCALE) +
Y_OFFSET;
vPosition.z = (view->GetCamera()->GetTransform().GetOrigin().y * X_SCALE) +
Z_OFFSET;
FPOINT3 vOrientation = {0.0f, 0.0f, 0.0f};
m_pOMS->SetMotionByGUID(m_guidAvatar, vPosition, vOrientation);
// End
}
return false;
}
/*---------------------------------------------------------------------*
* Main function
*---------------------------------------------------------------------*/
int main (int argc, char* argv[])
{
srand (time (NULL));
// Initialize the main system. This will load all needed plug-ins
// (3D, 2D, network, sound, ...) and initialize them.
if (!System->Initialize (argc, argv, NULL))
{
System->Report (CS_REPORTER_SEVERITY_ERROR, "Error initializing system!");
Cleanup ();
exit (1);
}
// Main loop.
csDefaultRunLoop(System->object_reg);
Cleanup ();
return 0;
}
// Added
void DemoSky::CleanUp()
{
if ( m_pOMS )
{
// Put the client back to a known spot
FPOINT3 vPosition;
vPosition.x = (0.0f * X_SCALE) + X_OFFSET;
vPosition.y = (0.0f * Y_SCALE) + Y_OFFSET;
vPosition.z = (0.0f * Z_SCALE) + Z_OFFSET;
FPOINT3 vOrientation = {0.0f, 0.0f, 0.0f};
m_pOMS->SetMotionByGUID(m_guidAvatar, vPosition, vOrientation);
delete m_pOMS;
m_pOMS = NULL;
}
if ( m_pcServerIP )
delete [] m_pcServerIP;
m_pcServerIP = NULL;
if ( m_pcServerPort )
delete [] m_pcServerPort;
Integrating the OMS with an Existing Application | 165
Integrating the OMS without the Wrapper
m_pcServerPort = NULL;
if ( m_pcUsername )
delete [] m_pcUsername;
m_pcUsername = NULL;
if ( m_pcPassword )
delete [] m_pcPassword;
m_pcPassword = NULL;
}
Y
{
m_pcUsername = new char[strlen(pcUsername) + 1];
}
FL
if ( m_pcUsername )
strcpy(m_pcUsername, pcUsername);
AM
}
if ( pcPassword )
{
m_pcPassword = new char[strlen(pcPassword) + 1];
if ( m_pcPassword )
strcpy(m_pcPassword, pcPassword);
}
}
Team-Fly®
166 | Chapter 5
Integrating the OMS without the Wrapper
return false;
}
if (m_pOMS->GetAbortFlag())
{
BNRESULT Result = m_pOMS->ServerConnect(m_iGame, m_iVersion,
m_pcServerIP, m_pcServerPort, pcNPSInPort, pcNPSOutPort);
switch ( Result )
{
case BNRESULT_INVALID_PARAMETER:
MessageBox(NULL, "Invalid server name. Check the "\
"server name and try again.\n\n(For this "\
"sample to work properly you must obtain "\
"valid server name,\nusername, password, "\
"game and version numbers)\n\n"\
"Modify the ServerInfo.cfg file so it "\
"contains the correct information, or \n"\
"search for TODO: in the sample code for "\
"the lines that should be changed.",
"OMS Failure", MB_OK);
return false;
break;
case BNRESULT_ERROR:
MessageBox(NULL, "Could not create the OMS.\n"\
"Probable cause network port is in use.\n\n"\
"Please make sure "no other instances of "\
"the program are running and try again.\n"\
"Or change the local port numbers used by "\
"one instance of the program.",
"OMS Failure", MB_OK);
return false;
break;
}
}
return true;
}
return false;
}
void DemoSky::Update()
{
EventInfo *pEventInfo = NULL;
long lNumEventsLeft = 1;
bool bStillMore = true;
int iIndex = 0;
Integrating the OMS with an Existing Application | 167
Integrating the OMS without the Wrapper
if ( !m_pOMS )
return;
switch ( pEventInfo->eEventType )
{
// Logon and avatar selection events
// Not processed here, the window messaging scheme is used
// to handle these events.
case OMS_EVENT_LOGON_PASS:
{
char pcText[1024];
printf("Logged In Successfully as %s\n",
GetUsername());
if (uCount > 0)
{
printf("Selecting first
ident\n");
GetOMS()->SelectIdentity(
vIdentities[0]);
}
}
}
break;
case OMS_EVENT_EMBODY_DONE:
printf("Embodied Avatar Successfully\n");
UpdateThing(pEventInfo->guidThing, true);
break;
case OMS_EVENT_EMBODY_FAIL:
printf("\n\n\nEmbodied Avatar Failed!!!\n\n\n");
break;
case OMS_EVENT_THING_NEW:
CreateThing(pEventInfo->guidThing);
break;
case OMS_EVENT_THING_HERE:
case OMS_EVENT_THING_SET:
UpdateThing(pEventInfo->guidThing,
(pEventInfo->guidThing == m_guidAvatar));
break;
case OMS_EVENT_THING_DROP:
case OMS_EVENT_THING_GONE:
RemoveThing(pEventInfo->guidThing);
break;
case OMS_EVENT_MESSAGE_USER_OFFLINE:
ReceiveOffline(pEventInfo->pcMessage,
pEventInfo->guidThing);
break;
case OMS_EVENT_MESSAGE_USER_PING:
ReceivePing(pEventInfo->pcMessage,
pEventInfo->guidThing);
break;
case OMS_EVENT_MESSAGE_RECEIVED:
switch ((BN_MESSAGE_TYPE)pEventInfo->typeThing)
// Flags cast to BN_MESSAGE_TYPE
{
case BN_MESSAGE_TYPE_TEXT_CHAT:
ReceiveMessage(pEventInfo->pcUsername,
pEventInfo->pcMessage,
pEventInfo->guidThing);
break;
Integrating the OMS with an Existing Application | 169
Integrating the OMS without the Wrapper
case BN_MESSAGE_TYPE_BINARY_PROJECTILE:
ReceiveProjectile(pEventInfo->pcMessage,
pEventInfo->usMessageLength);
break;
default:
ASSERT_ERROR(false, "OMS: Received message
of unknown flag type\n");
break;
}
break;
case OMS_EVENT_MESSAGE_RECEIVED_SECURE:
if (pEventInfo->pcMessage &&
pEventInfo->pcUsername)
{
// Find the user since the response can send
// a message
if (m_pOMS)
m_pOMS->MessageFind(m_guidAvatar,
pEventInfo->pcUsername, true);
char pcMessage[256];
char pcAccept[256];
char pcReject[256];
pEventInfo = NULL;
}
}
if ( !m_pOMS )
return false;
UpdateThingView(guidThing, bClientControlled);
PROPERTY_LIST_STRING ))
{
if ( vAttributes[uAttrib].m_Attribute.Value.String.pcData
!= NULL )
delete [] vAttributes[
uAttrib].m_Attribute.Value.String.pcData;
vAttributes[uAttrib].m_Attribute.Value.String.pcData =
NULL;
vAttributes[uAttrib].m_Attribute.Value.String.iLength = 0;
vAttributes[uAttrib].m_Attribute.Type = PROPERTY_NULL;
}
return bRetVal;
}
if (GetOMS())
{
if (BN_SUCCESS(GetOMS()->GetTypeByGUID(guidThing, typeObject)))
{
sprintf(pcTemp, "Created Thing %ld of type %d", guidThing,
typeObject);
case 0:
case 1:
m *= 2.0;
break;
case 2:
case 3:
m *= 1.0;
break;
default:
m *= 0.25;
break;
}
sprite->GetMovable ()->SetTransform (m);
sprite->GetMovable ()->SetPosition (pos);
sprite->GetMovable ()->UpdateMove ();
csRef<iSprite3DState> spstate (SCF_QUERY_INTERFACE
(sprite->GetMeshObject (), iSprite3DState));
spstate->SetAction ("default");
sprite->SetZBufMode (CS_ZBUF_USE);
sprite->SetRenderPriority
(engine->GetObjectRenderPriority ());
sprite->DeferUpdateLighting
(CS_NLIGHT_STATIC|CS_NLIGHT_DYNAMIC, 10);
bRetVal = true;
}
else
printf(pcTemp, "Created Thing FAILED could not get thing
type\n");
}
else
printf(pcTemp, "Created Thing FAILED could not get oms pointer\n");
fflush(stdout);
return bRetVal;
}
fflush(stdout);
bRetVal = true;
return bRetVal;
}
bRetVal = true;
return bRetVal;
}
bRetVal = true;
return bRetVal;
}
bRetVal = true;
return bRetVal;
}
char pcTemp[1024];
if ( pcMessage )
{
printf("User %s (0x%04x) said: %s\n", pcUsername ? pcUsername :
"UNKNOWN", ulKey, pcMessage);
fflush(stdout);
bRetVal = true;
}
return bRetVal;
}
Y
{
bool bRetVal = false;
FL
char pcTemp[1024];
char *pcStartPos = pcData;
ULONG ulLong;
AM
if ((usDataLength > sizeof(BNGUID) + (sizeof(float) * 4) + 2))
{
// Retrieve the data from the data string
TE
BNGUID guidOrigin;
memcpy(&ulLong, pcData, sizeof(ULONG));
guidOrigin = ntohl(ulLong);
pcData += sizeof(BNGUID);
float fVelocity;
memcpy(&ulLong, pcData, sizeof(ULONG));
ulLong = ntohl(ulLong);
fVelocity = *((float *)&ulLong);
pcData += sizeof(ULONG);
Team-Fly®
176 | Chapter 5
Integrating the OMS without the Wrapper
if ( pcExplosion )
{
printf(" ending in %s\n", pcExplosion);
}
else
{
printf("\n");
}
fflush(stdout);
return bRetVal;
}
bRetVal = true;
}
return bRetVal;
}
char pcTemp[256];
sprintf(pcTemp, " -> Thing %ld moved to position %4.2f, %4.2f, %4.2f",
guidThing, fX, fY, fZ);
spThing->GetMovable()->SetPosition (pos);
spThing->GetMovable()->UpdateMove ();
bRetVal = true;
return bRetVal;
}
return bRetVal;
}
char pcTemp[256];
if ( pcAnimation )
{
printf(" -> Thing %ld animation set to %s\n", guidThing, pcAnimation);
fflush(stdout);
return bRetVal;
}
return bRetVal;
}
Temp.guidThing = guidThing;
Temp.spThing = pThing;
m_vThingList.push_back(Temp);
}
return true;
}
return false;
}
return NULL;
}
return spTemp;
}
// End
DemoSky.h
/*
Copyright (C) 1998-2000 by Jorrit Tyberghein
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#ifndef DEMOSKY_H
#define DEMOSKY_H
#include <stdarg.h>
#include "csgeom/math2d.h"
#include "csgeom/math3d.h"
// Added
#include <list>
//** OMS Includes
#include "../butterfly-grid/grid-common/thing/thing_types.h"
class COMS;
struct EventInfo;
struct iMeshFactoryWrapper;
// End
class csProcSky;
class csProcSkyTexture;
class Flock;
struct iSector;
struct iView;
struct iEngine;
struct iDynLight;
struct iMaterialWrapper;
struct iPolygon3D;
struct iFont;
struct iMeshWrapper;
struct iMaterialWrapper;
struct iLoader;
struct iKeyboardDriver;
struct iGraphics3D;
struct iGraphics2D;
struct iVirtualClock;
struct iObjectRegistry;
struct iEvent;
// Added
typedef struct ThingItem
{
BNGUID guidThing;
csRef<iMeshWrapper> spThing;
class DemoSky
{
public:
iObjectRegistry* object_reg;
private:
Integrating the OMS with an Existing Application | 181
Integrating the OMS without the Wrapper
iSector* room;
csRef<iView> view;
csRef<iEngine> engine;
iMaterialWrapper* matPlasma;
csRef<iFont> font;
csRef<iLoader> LevelLoader;
csRef<iGraphics2D> myG2D;
csRef<iGraphics3D> myG3D;
csRef<iKeyboardDriver> kbd;
csRef<iVirtualClock> vc;
// the sky
csProcSky *sky;
// the six sides (front, back, left, right, up, down)
csProcSkyTexture *sky_f, *sky_b, *sky_l, *sky_r, *sky_u, *sky_d;
// Added
csRef<iMeshFactoryWrapper> imeshfact;
// Added
void CleanUp();
COMS *GetOMS() { return m_pOMS; }
void SetAvatar(BNGUID guidThing) {m_guidAvatar = guidThing;}
BNGUID GetAvatar() {return m_guidAvatar;}
bool UpdateThing(BNGUID guidThing, bool bClientControlled);
const char *GetUsername() {return m_pcUsername;}
void SetUsername(const char *pcUsername);
const char *GetPassword() {return m_pcPassword;}
void SetPassword(const char *pcPassword);
bool CreateOMS(char *pcIP, char *pcPort, char *pcNPSInPort, char
*pcNPSOutPort);
void Update();
bool CreateThing(BNGUID guidThing);
182 | Chapter 5
Integrating the OMS without the Wrapper
public:
DemoSky ();
virtual ~DemoSky ();
bool Initialize (int argc, const char* const argv[], const char *iConfigName);
void SetupFrame ();
void FinishFrame ();
bool HandleEvent (iEvent &Event);
#endif // DEMOSKY_H
ClientCreates.cpp
#include "ClientCreates.h"
#include "ClientObject.h"
ClientCreates.h
#include "../butterfly-grid/grid-common/thing/oms_cthing.h"
#include "ClientObject.h"
#define NUM_CLIENT_OBJECTS 2 + 1
ClientObject.cpp
#include "ClientObject.h"
184 | Chapter 5
Integrating the OMS without the Wrapper
//#define USE_SAMPLE_DEAD_RECKONING_MODELS
#ifdef USE_SAMPLE_DEAD_RECKONING_MODELS
#include "../../butterfly-grid/grid-oms/DRNoncontrolledObject.h"
#include "../../butterfly-grid/grid-oms/DRControlledObject.h"
#endif
INT BNAttribIdentityVALUES[] =
{
BNATTRIB_IDENTITY_UNKNOWN,
BNATTRIB_IDENTITY_MAN,
BNATTRIB_IDENTITY_WOMAN,
BNATTRIB_IDENTITY_GHOST,
};
// MAKE SURE CHANGES TO THE ABOVE ENUM ARE REFLECTED IN:
// g_pcBNAttribSpecTypes in ClientObject.cpp;
#ifdef USE_SAMPLE_DEAD_RECKONING_MODELS
if ( !bIsClientControlled )
this->SetModel(new CDRNoncontrolledObject());
else
this->SetModel(new CDRControlledObject());
#endif
}
CPhysical::~CPhysical()
{
DeleteType( BN_ATTRIB_ANIMATION, 0, pAnimation );
}
CLiving::~CLiving()
{
Integrating the OMS with an Existing Application | 185
Integrating the OMS without the Wrapper
Y
CAvatar::~CAvatar()
{
} FL
DeleteType( BN_ATTRIB_IDENTITY, BN_ATTRIB_IDENTITY, pIdentity );
AM
CAnimal::CAnimal(BNGUID guidObject, bool bIsClientControlled, BNGUID
guidParentObject /*= GUID_INVALID*/, bool bPreventClobber /*= false*/)
: CLiving(BN_THING_ANIMAL, guidObject, bIsClientControlled,
guidParentObject, bPreventClobber)
TE
{
}
CAnimal::~CAnimal()
{
}
ClientObject.h
#ifndef CLIENTOBJECT_H_INCLUDED
#define CLIENTOBJECT_H_INCLUDED
#include "../butterfly-grid/grid-oms/oms.h"
#include "../butterfly-grid/grid-common/thing/oms_cthing.h"
#include "ClientObjectDefines.h"
Team-Fly®
186 | Chapter 5
Integrating the OMS without the Wrapper
};
#endif /*!CLIENTOBJECT_H_INCLUDED*/
ClientObjectDefines.h
#ifndef CLIENTOBJECTDEFINES_H_INCLUDED
#define CLIENTOBJECTDEFINES_H_INCLUDED
#include "../butterfly-grid/grid-common/butterfly_types.h"
enum BN_ATTRIB_ENUM
{
BN_ATTRIB_ANIMATION = BUTTERFLY_SUBTYPES_MAX + 1, BN_ATTRIB_IDENTITY,
};
#endif /*!CLIENTOBJECTDEFINES_H_INCLUDED*/
Before we even attempt to run this, there are a couple of prerequisites. First, in
the Visual Studio include directories list (located in the Tools 4 Options menu),
we need to add the following to the Include Files list:
c:\crystal\butterfly-grid
Then we need to create a server info file, which will be read in by our sample
application. Note that to run two copies of the client we need to have two server
info files with different login names (and associated passwords) and also differ-
ent local ports (if the two applications are running on the same computer).
Listing 5-2 shows a sample configuration file.
188 | Chapter 5
Integrating the OMS without the Wrapper
// Other accounts use the same information but the number increments!
Note that only the first line of the file is ever read in, so the rest of the file con-
tains comments to explain each of the parameters in the colon-delimited string.
In the other configuration file, we simply change the first line to read as follows:
wordware.butterfly.net:9907:wordware2:BocFemp2:7:1:8002:8002:
Before running the application, ensure that the ServerInfo.cfg file is in the same
directory as the executable. When you run two copies of it, ensuring you have
modified the configuration file before running the second one, you should be
able to move your player around with the arrow keys and also see the other
player moving around. Figures 5-6 through 5-8 show how this should look.
(Note that you will also see three other black rectangles that represent static
objects within the server’s database.)
Figure 5-6 shows two players connected on the same machine. Player one is
in the bottom left screen and is looking at player two, denoted by the black
cuboid.
Integrating the OMS with an Existing Application | 189
Integrating the OMS without the Wrapper
Notice also the debug information we are outputting to the console. This informs
you of messages the client is receiving from the game server. Figure 5-8 shows a
sample screen shot of some information that the console displayed while we
moved the second player.
Now that we have seen a working demo, let’s take an in-depth look at the code
that makes it work.
We will try to look at this in the most logical way possible, which would be
to start at the application’s entry point and follow through how it all works.
Starting in the main method, first the pseudorandom number generator is
seeded with the time.
int main (int argc, char* argv[])
{
srand (time (NULL));
After this, a new instance of the application class DemoSky is created and stored
within the global variable System:
System = new DemoSky ();
Execution then moves to the DemoSky constructor during the object creation.
Here, the first thing that is done is the objects used to create the procedural sky
and skybox are initialized to null. This can be seen here:
DemoSky::DemoSky ()
{
sky = NULL;
sky_f = NULL;
sky_b = NULL;
sky_l = NULL;
sky_r = NULL;
sky_u = NULL;
sky_d = NULL;
Note that the “_f,” “_b,” etc., stands for front, back, left, right, up, and down —
the sides of the skybox. The sky object is of type csProcSky and the other six
Integrating the OMS with an Existing Application | 191
Integrating the OMS without the Wrapper
The first of these, m_pOMS, is of type COMS, which provides the primary
client interface to the Butterfly Grid. Additionally, it provides a cached represen-
tation of all the “Things” within the range of the player’s avatar. Also, the OMS
provides the functionality to use the CThing class to represent objects within the
world that can have client-defined attributes to hold custom game states, which
can then automatically propagate through the Grid whenever they change. The
OMS also provides a way to define and use custom dead reckoning models as
well as allowing the invocation of server-side scripts.
Ü going
Note Dead reckoning is a term used to denote prediction of what the player is
to do. This can be done by several means, for example statistical, linear
equations, etc.
A good example of this would be a bullet fired from a gun. When the bullet is
fired, other players would know it had been fired, along with the initial velocity.
From this information, it would be possible for all the clients to estimate the tra-
jectory of the bullet and “guess” where it should be. So it would be said that the
clients were using dead reckoning to predict the bullet’s position.
Note that this guesswork is only used until the actual packet containing
updated information for the bullet is received; then the position, etc., is updated
and the guesswork begins again.
To connect to the server, we need to know its IP address and also the port it is
running on. If you remember from before, we created a file called ServerInfo.cfg
to contain this information. However, we want to store this information within
member variables of our application class. This is where the next four variables
come in. m_pcServerIP, m_pcServerPort, m_pcUsername, and m_pcPassword
are defined within the header file as char pointers and are used to hold the server
IP, server port, and the username and password for the user to connect to the
server with, respectively. After these, we then have m_iGame and m_iVersion,
defined as integers, which will be used to store the ID number of the game and
the ID number of the game version, again which will be read in from the
ServerInfo.cfg file. Finally, we have m_guidAvatar, which is defined as type
BNGUID (a typedef located within butterfly_types.h in the client libraries for an
unsigned int). This is used to store the unique ID of the avatar that the player has
embodied, after he or she has successfully connected to the game server.
192 | Chapter 5
Integrating the OMS without the Wrapper
After the call to the constructor, execution returns to the main method, where
the next method to be called is the Initialize method of the DemoSky object.
Let’s now look there.
First, we have the standard code to load in the plug-ins, which we have seen
several times before. After this, the first application-specific code is where the
procedural textures are created for the sky:
sky = new csProcSky();
sky->SetAnimated(object_reg, false);
sky_f = new csProcSkyTexture(sky);
iMaterialWrapper* imatf = sky_f->Initialize(object_reg, engine, txtmgr,
"sky_f");
sky_b = new csProcSkyTexture(sky);
iMaterialWrapper* imatb = sky_b->Initialize(object_reg, engine, txtmgr,
"sky_b");
sky_l = new csProcSkyTexture(sky);
iMaterialWrapper* imatl = sky_l->Initialize(object_reg, engine, txtmgr,
"sky_l");
sky_r = new csProcSkyTexture(sky);
iMaterialWrapper* imatr = sky_r->Initialize(object_reg, engine, txtmgr,
"sky_r");
sky_u = new csProcSkyTexture(sky);
iMaterialWrapper* imatu = sky_u->Initialize(object_reg, engine, txtmgr,
"sky_u");
sky_d = new csProcSkyTexture(sky);
iMaterialWrapper* imatd = sky_d->Initialize(object_reg, engine, txtmgr,
"sky_d");
Note that you can change the Boolean in the SetAnimated method to true to
make the sky animate, which looks pretty nice but does slow the application
down a lot.
Then, the six walls, which are textured with the procedural textures, are cre-
ated in a way that is similar to what we saw in the first example in Chapter 4,
butterfly3d1.
The next important part in this method is where we load in a texture for our
player 3D sprite (a.k.a. the cube). For this, we load in the spark texture, which is
located in the standard library that comes with Crystal Space. The code to do
this is the following:
iTextureWrapper* txt = LevelLoader->LoadTexture ("spark",
"/lib/std/spark.png", CS_TEXTURE_3D, txtmgr, true);
if (txt == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.simple",
"Error loading texture!");
return false;
}
We then load in a simple mesh factory, which can be used to create rectangular
meshes and is also located within the standard library. The code to load this can
be seen here:
Integrating the OMS with an Existing Application | 193
Integrating the OMS without the Wrapper
imeshfact = LevelLoader->LoadMeshObjectFactory
("/lib/std/sprite1");
if (imeshfact == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.simple1",
"Error loading mesh object factory!");
return false;
}
Once we have our mesh factory to create our player sprites, we then create local
variables to hold the data we are about to retrieve from the ServerInfo.cfg file.
These are the same as we defined in the header and initialized within the con-
structor, but with the addition of two additional variables to hold the local send
and receive ports that will be used for incoming and outgoing data. The declara-
tions are as follows:
char *pcServerIP = "Temporary ServerIP";
char *pcServerPort = "Temporary ServerPort";
char *pcUsername = "Temporary Username";
char *pcPassword = "Temporary Password";
char *pcNPSInPort = "8002";
char *pcNPSOutPort = "8002";
Next, we create three temporary variables that we will use to assist with the
reading of the configuration file:
char pcBuffer[1024];
char *pcTempBuffer;
char *pcTemp;
The next step is to obtain a handle to the file by calling the fopen method. We
first attempt to open it from the current folder. If this does not succeed, we then
try up a level. We store the result in a FILE pointer called pConfig. This can be
seen in the following code segment:
FILE *pConfig = fopen("ServerInfo.cfg", "r");
if ( !pConfig )
pConfig = fopen("..\\ServerInfo.cfg", "r");
Once we have read this in, we need to tokenize the data within the line to extract
all the information we require. To do this, we first store a pointer to the start of
the array in our pcTemp variable, as can be seen here:
pcTemp = pcBuffer;
194 | Chapter 5
Integrating the OMS without the Wrapper
Then, we call the strchr method, passing in our pcTemp pointer along with the
character we want to find (i.e., the delimiter). This method then returns a pointer
to the location where the “:” character was found, inclusive of the character’s
position, or NULL if no character was found.
pcTempBuffer = strchr(pcTemp, ':');
If pcTempBuffer was not a valid pointer, we simply return from the method.
if (!pcTempBuffer) return TRUE;
Next, we set the position within the array where the “:” character was found to
be NULL. This is done by setting the first position of the pcTempBuffer array to
0, as this pointer points to the position in the string where the character was
found. Because the string is now null terminated at the end of the first token, we
can then assign our first variable to be located at the start of the string:
pcServerIP = pcBuffer;
Remember that pcBuffer currently points to the start of the string read in.
Next, we want to extract the server port, so we assign to the pcTemp pointer
the position stored within the pcTempBuffer string, plus one, which will there-
fore make pcTemp point at the first character after the first “:” delimiter (which
has now been set to NULL). This can be seen here:
pcTemp = pcTempBuffer + 1;
Again, we find the position of the next delimiter by calling the strchr method,
storing its location again within the pcTempBuffer string. We then again check
its validity and return from the method if it turns out to be a null pointer.
pcTempBuffer = strchr(pcTemp, ':');
if (!pcTempBuffer) return TRUE;
Once again, we set the delimiter character to 0 (NULL), then assign the string to
our pcServerPort pointer:
pcServerPort = pcTemp;
We then repeat this process for the username and password. When we get to the
game and version numbers, we simply use the atoi method to convert the string
read in to an integer, as this method takes a null-terminated string. The conver-
sion for the game ID can be seen here:
m_iGame = atoi(pcTemp);
Once we have the game and version IDs, we then look for the optional incoming
and outgoing local port numbers. Note that the only time we actually need to
specify these is if we plan to run more than one client on the same machine. We
store these port numbers as strings and use the following code to read them in
from the config file.
pcTemp = pcTempBuffer + 1;
pcTempBuffer = strchr(pcTemp, ':');
if ( pcTempBuffer )
Integrating the OMS with an Existing Application | 195
Integrating the OMS without the Wrapper
{
pcTempBuffer[0] = 0;
pcNPSInPort = pcTemp;
pcTemp = pcTempBuffer + 1;
pcTempBuffer = strchr(pcTemp, ':');
if ( pcTempBuffer )
{
pcTempBuffer[0] = 0;
pcNPSOutPort = pcTemp;
}
}
If there is no delimiter found, we assume that the local ports have not been spec-
ified and we use the default ones that were assigned just before attempting to
Y
read in the config file.
Next, we call two methods called SetUsername and SetPassword, passing in
FL
the username and password information we just retrieved from the configuration
file. These can be seen here:
AM
SetUsername(pcUsername);
SetPassword(pcPassword);
All these methods really do is check that the string passed in is valid (i.e., the
username or password). If it is, they then create a new char array of the appro-
TE
priate size and store a pointer to it in the respective member variable (i.e.,
m_pcUsername or m_pcPassword). These two methods can be seen in full here:
void DemoSky::SetUsername(const char *pcUsername)
{
if ( pcUsername )
{
m_pcUsername = new char[strlen(pcUsername) + 1];
if ( m_pcUsername )
strcpy(m_pcUsername, pcUsername);
}
}
After we set the member instances of the username and password, we then make
a call to our user-defined CreateOMS method, passing in the local variables we
just retrieved from the config file.
CreateOMS(pcServerIP, pcServerPort, pcNPSInPort, pcNPSOutPort);
Team-Fly®
196 | Chapter 5
Integrating the OMS without the Wrapper
The first thing we do within this method is store the server IP and port in the
member variables as we just did with the username and password. This is done
with the following segment of code:
m_pcServerPort = new char[strlen(pcPort) + 1];
if( m_pcServerPort )
strcpy(m_pcServerPort, pcPort);
m_pcServerIP = new char[strlen(pcIP) + 1];
if( m_pcServerIP )
strcpy(m_pcServerIP, pcIP);
If the server IP and port strings are not null pointers, we continue by creating an
instance of the COMS class, passing in all the information we have collected
and storing the resulting object in our member variable m_pOMS.
if ( m_pcServerPort && m_pcServerIP )
{
// create an Object Management System
// TODO: OPTIONALLY - Specify different port numbers for the last two
// optional parameters to create a version of the program that can be
// run simultaneously with another version on the same machine. i.e.,
// Two instances of the program cannot be run on the same machine and
// use the same local ports
m_pOMS = new COMS(m_iGame, m_iVersion, m_pcServerIP, m_pcServerPort,
pcNPSInPort, pcNPSOutPort);
Note that this method does not actually connect us to the server; it only initial-
izes the Object Management System.
Once we have created it, we check that it has been allocated successfully; if
not, we show a message box and exit the method.
if ( !m_pOMS )
{
MessageBox(NULL, "Out of memory", "Out of memory", MB_OK);
return false;
}
Next, we make a call to the GetAbortFlag method of the COMS class. This
method simply returns true if the OMS encountered an error during construction.
The test for this is shown below.
if(m_pOMS->GetAbortFlag())
{
If an error did occur, we want to know what happened so we can inform the user.
To do this, we must attempt a connection to the server to retrieve an enumerated
list of errors. We connect by means of the ServerConnect method, which is also
a member of the COMS class. Into this method, we pass in the game ID, game
version, server IP and port, and the local incoming and outgoing ports. The
return value from this method returns a BNRESULT, which dictates the outcome
of the connection to the server.
BNRESULT Result = m_pOMS->ServerConnect(m_iGame, m_iVersion, m_pcServerIP,
m_pcServerPort, pcNPSInPort, pcNPSOutPort);
Integrating the OMS with an Existing Application | 197
Integrating the OMS without the Wrapper
BNRESULT_OK Success.
As you can see, not all these values relate to errors with the construction of the
OMS. The only two errors we need to look for here are BNRESULT_INVAL-
ID_PARAMETER and BNRESULT_ERROR. To do this, we switch the return
value of the ServerConnect method and display the error appropriately in the
form of a window message box to the user. The switch statement for this is
shown here.
switch ( Result )
{
case BNRESULT_INVALID_PARAMETER:
MessageBox(NULL, "Invalid server name. Check the server "\
"name and try again.\n\n(For this sample to work "\
"properly you must obtain valid server name, \n"\
198 | Chapter 5
Integrating the OMS without the Wrapper
After we know there were no errors in the construction of our COMS object, we
proceed by making a call to a method called SetupCreateThingTable. The pur-
pose of this method is to let you, the developer, create derived versions of the
CThing objects to better suit the needs of your specific game. Into this method
we pass a “table” of information. By table we mean two arrays that directly
relate to each other. The first array contains function pointers of how to create
the object type specified at the same position in the second array.
Into this method we pass the total amount of different object types for the
game, followed by the array of function pointers, which we have called
CreateArray, and the list of numerical object types, which we have called
ObjectArray. This can be seen here:
m_pOMS->SetupCreateThingTable(NUM_CLIENT_OBJECTS, CreateArray, ObjectArray);
For the first object type, i.e., the test object, we use the CreateAvatar method;
however, this is redundant as this object will never be used. For our
BN_THING_AVATAR object, we store a pointer to the CreateAvatar method,
and for the BN_THING_ANIMAL object, we store a pointer to the
CreateAnimal method.
As we mentioned briefly before, all our objects must be derived from the
CThing class. Because all our objects are derived from this, they can be safely
cast up to be of type CThing again for use within the OMS. So what we need to
do first is define two classes, one for our players (avatars), which will be called
CAvatar, and one for our NPCs, called CAnimal.
These two classes are defined as follows:
class CAvatar : public CLiving
{
public:
CAvatar(BNGUID guidObject, bool bIsClientControlled, BNGUID
guidParentObject = GUID_INVALID, bool bPreventClobber = false);
virtual ~CAvatar();
{
public:
CAnimal(BNGUID guidObject, bool bIsClientControlled, BNGUID
guidParentObject = GUID_INVALID, bool bPreventClobber = false);
virtual ~CAnimal();
Notice how the CAvatar class contains an additional private member called
pIdentity of type CEnumAttrib. This additional variable is a special class type
that relates directly to the attributes that can be stored on the server. All attrib-
utes are derived from the CAttrib class, which is a friend of the CThing class.
Table 5-2 is a complete list of possible attributes that can be used within the
CThing derived classes.
Table 5-2: Possible attributes for CThing derived classes
Attribute Class Definition
CBlobAttrib Used to hold a generic blob of data. The maximum size for this is
defined as OMS_MAX_BLOB_LEN within the OMS.
CStringAttrib Used to hold a string, i.e., an array of char data, up to the maximum
size of OMS_MAX_STRING_LEN as defined in the OMS.
CTokenAttrib Used to hold a token object, for which the structure is defined as
follows.
typedef struct token
{
BNGUID guid; // 4-byte game unique identifier
BNSPEC spec; // 2-byte game subtype specifier
BNTYPE type; // 2-byte game objtype specifier
// Required to use the std::list::sort function
bool operator != (const token &token1) const
{
if ((guid == token1.guid) && (spec == token1.spec) &&
(type == token1.type))
return false;
else
return true;
}
} TOKEN;
CTokenListAttrib Uses an STL vector to hold a list of token structures. (See CTokenAttrib.)
If you look in the ClientObjectDefines.h header file, you will find the following
list of enumerated values:
typedef enum BNAttribIdentity
{
BNATTRIB_IDENTITY_UNKNOWN,
BNATTRIB_IDENTITY_MAN,
BNATTRIB_IDENTITY_WOMAN,
BNATTRIB_IDENTITY_GHOST,
BNATTRIB_IDENTITY_MAX,
} BNATTRIB_IDENTITY;
The CAvatar and CAnimal classes are derived from the CLiving class, which is
defined as follows:
class CLiving : public CPhysical
{
public:
CLiving(BNOBJECTTYPE objecttype, BNGUID guidObject, bool bIsClientControlled,
BNGUID guidParentObject = GUID_INVALID, bool bPreventClobber = false);
virtual ~CLiving();
Then, as you can see, this class extends the CPhysical class, which is defined as
follows:
class CPhysical : public CThing
{
public:
CPhysical(BNOBJECTTYPE objecttype, BNGUID guidObject, bool
bIsClientControlled, BNGUID guidParentObject = GUID_INVALID,
bool bPreventClobber = false);
virtual ~CPhysical();
202 | Chapter 5
Integrating the OMS without the Wrapper
The CPhysical class is a direct subclass of the CThing class and has a private
attribute class pAnimation, which is of type CStringAttrib. So, the two values
defined within our BN_ATTRIB_ENUM refer to the attributes within the
CPhysical class and CAvatar class, i.e., BN_ATTRIB_ANIMATION refers to
the CStringAttrib pAnimation pointer in the CPhysical class and
BN_ATTRIB_IDENTITY refers to the CEnumAttrib pIdentity pointer in the
CAvatar class.
Then, in our ClientObject.cpp, we also define an array of integers called
BNAttribIdentityVALUES into which we store the enumerated values for
BN_ATTRIB_IDENTITY. We will see this being used in the constructor for the
CAvatar class.
Now back to the main code. In the CreateOMS method, after the call to
SetupCreateThingTable, we make a call to another method of the COMS class
called HandleSets.
The HandleSets method is used to tell the OMS how the client would like to
receive messages relating to things in the world. This is set to OMS_EVENT_
TRANSFER_TYPE_EVENT, which adds the events to an internal message list
within the OMS. We’ll see more of how this works later in the explanation.
There are two other event transfer types. Table 5-3 lists all three possible types.
Table 5-3: Event transfer types
Transfer Type Description
After this method call, we make a call to the COMS method called
HandleLogin. The HandleLogin method is used to specify how the client should
receive login messages, which again we specify as OMS_EVENT_TRANS-
FER_TYPE_EVENT.
Integrating the OMS with an Existing Application | 203
Integrating the OMS without the Wrapper
Ü sageHandle,
Note Within the COMS class, there is also a method called SetEventMes-
which can be used to give the OMS the handle to the window that
will receive the event messages through window notification messages. The mes-
sage ID is WM_OMS_MESSAGE. The WPARAM contains a pointer to the structure
describing the OMS event (it must be deleted by the receiver). The LPARAM con-
tains the OMS_EVENT enumerated value identifying the event. This currently only
functions under MS Windows.
However, this is not required within our application as we poll the message list
each frame, as we will see shortly.
After this, the method returns and we continue execution back in the Initialize
method. The next call we make in the Initialize method, after the CreateOMS
method, is to ServerLogin method, which again is a member of the COMS class.
The call to this method can be seen here:
GetOMS()->ServerLogin(GetUsername(), GetPassword(), 0);
As you may have guessed, the first two parameters for this method are the
username and password of the player, which are retrieved from our member
variables by the GetUsername and GetPassword methods shown below.
const char *GetUsername() {return m_pcUsername;}
Note that these two methods are defined within the header file due to their sim-
plicity. The final parameter of the ServerLogin method allows you to specify a
timeout in milliseconds that the method should wait for a successful login. If 0
(zero) is specified, the client can simply wait until the login message is received
by means of the way specified by the HandleLogin method we saw earlier. Set-
ting this final parameter to zero is the most common way of dealing with this.
After this, we then make a call to the Prepare method of the Crystal Space
engine, then we set up the camera, positioning it at the origin of the world. This
can be seen in the following code segment.
engine->Prepare ();
Now, as the player is represented by the camera (i.e., first-person view), we need
to inform the OMS of the position of the player. Note that we could use the posi-
tion the player disconnected at; however, for now we will be resetting the
position of the player to the origin each time he or she logs in.
204 | Chapter 5
Integrating the OMS without the Wrapper
The first is a pointer to an EventInfo structure, which is the generic format used
to send messages through the Butterfly Grid. The EventInfo structure is defined
within the OMS, but can be seen here for reference:
typedef struct EventInfo
{
BNGUID guidThing; // This is the GUID of the Thing that
// the event is about.
BNOBJECTTYPE typeThing; // This is the type of Thing that the
// event is about.
BNGUID guidTo; // This is the GUID of the Thing that
// the event was sent to. It is for
// Daemon clients only.
Integrating the OMS with an Existing Application | 205
Integrating the OMS without the Wrapper
Y
// only for instant message events
// and it is self deleting.
char *pcAccept;
FL // This is the prompt to display to
// the user for accepting a secure
// message. It is used only for secure
AM
// instant message events and it is
// self deleting.
char *pcReject; // This is the prompt to display to
// the user for rejecting a secure
TE
/*
* This constructor initializes the structure to ensure that all of the members are
empty.
*/
()
{memset(this, 0, sizeof(EventInfo));}
/*
* This destructor cleans up all of the self-deleting members of the structure.
*/
Team-Fly®
206 | Chapter 5
Integrating the OMS without the Wrapper
~EventInfo()
{ if ( pcMessage ) delete [] pcMessage;
if ( pcUsername ) delete [] pcUsername;
if ( pcAccept ) delete [] pcAccept;
if ( pcReject ) delete [] pcReject;
if ( pMsg ) delete pMsg;
if ( pParameterList ) delete pParameterList;
}
} EVENT_INFO_STRUCT;
The next variable, lNumEventsLeft is used to record how many events still need
to be processed. We initially set this to 1, and then retrieve this value from the
OMS a little bit further on in the method.
The bStillMore Boolean is used to denote if there are still more events to pro-
cess, whereas iIndex is used as a simple counter to retrieve all the events and
also limit the maximum number of events processed each frame, as we will see
soon.
After this, we check that our pointer to the COMS object m_pOMS is still
valid using the following two lines of code:
if ( !m_pOMS )
return;
Next, we call the GetFirstEvent method of the COMS class, passing in a pointer
to our lNumEventsLeft variable, which will be filled in with the current number
of events that require processing.
The GetFirstEvent method notifies the OMS that the client is about to update
the positions of the objects within the world and that no OMS events should be
added during this process. Hence, the OMS uses this method to prevent the cli-
ent from processing the movement of a Thing twice in one frame. This method
returns a pointer to an EventInfo structure, which we store in the pEventInfo
pointer we just declared. If no event occurred, the GetFirstEvent method returns
NULL.
After this, we create the following for loop:
for (iIndex = 0; bStillMore && ((iIndex < 10) || (lNumEventsLeft > 50));
iIndex++ )
{
The purpose of this loop is to make the application handle up to ten events at the
start of each frame (i.e., iIndex < 10) or as many events as required to reduce the
number of events pending down to 50 (i.e., lNumEventsLeft > 0).
For each iteration of this loop, we must retrieve the next event, then deal with
it appropriately. Note, however, that we may already have the first event (pro-
vided there was a first event), so we deal with this using the following two lines
of code:
if (pEventInfo == NULL)
pEventInfo = m_pOMS->GetNextEvent(&lNumEventsLeft);
After the message has been dealt with, we set the pEventInfo pointer to NULL,
so the next iteration of the for loop finds the pointer to be NULL and makes a
call to the GetNextEvent method. Note again how a pointer to the
Integrating the OMS with an Existing Application | 207
Integrating the OMS without the Wrapper
If the pEventInfo pointer was valid, we next need to determine what the message
is. We can find this out by switching the eEventType member of the EventInfo
structure.
switch ( pEventInfo->eEventType )
{
All the events that are handled in this application are discussed at the end of this
section. Note that not all are fully implemented.
After the call to Update back in the SetupFrame method, the next thing we do
is store the current time and the elapsed time. This can be seen in the following
few lines of code:
csTicks elapsed_time, current_time;
elapsed_time = vc->GetElapsedTicks ();
current_time = vc->GetCurrentTicks ();
We then base the rotational and movement speed of the camera on the time that
has elasped to give the impression of smooth movement. This can be seen here:
float speed = (elapsed_time / 1000.0f) * (0.03f * 20.0f);
Then we check the keyboard input as we have seen before to let the user move
the camera about with the arrow keys (remembering the camera is actually
where the player is within the world). The code used to check the keyboard
input and adjust the position of the camera is shown below.
if (kbd->GetKeyState (CSKEY_RIGHT))
view->GetCamera ()->GetTransform ().RotateThis (CS_VEC_ROT_RIGHT, speed);
if (kbd->GetKeyState (CSKEY_LEFT))
view->GetCamera ()->GetTransform ().RotateThis (CS_VEC_ROT_LEFT, speed);
if (kbd->GetKeyState (CSKEY_PGUP))
view->GetCamera ()->GetTransform ().RotateThis (CS_VEC_TILT_UP, speed);
if (kbd->GetKeyState (CSKEY_PGDN))
view->GetCamera ()->GetTransform ().RotateThis (CS_VEC_TILT_DOWN, speed);
if (kbd->GetKeyState (CSKEY_UP))
view->GetCamera ()->Move (CS_VEC_FORWARD * 2.0f * speed);
if (kbd->GetKeyState (CSKEY_DOWN))
view->GetCamera ()->Move (CS_VEC_BACKWARD * 2.0f * speed);
208 | Chapter 5
Integrating the OMS without the Wrapper
After this, we call the BeginDraw method of the iGraphics3D interface to initial-
ize the renderer:
if(!myG3D->BeginDraw (engine->GetBeginDrawFlags() | CSDRAW_3DGRAPHICS))
return;
We then draw the world by making a call to the Draw method of the view object
(which is of type iView). This line can be seen here:
view->Draw();
Once in 2D mode, we output the instructions for quitting the application, for
which the code can be seen here:
const char *text = "Escape quits."
" Arrow keys/pgup/pgdown to move.";
// End
int txtx = 10;
int txty = myG2D->GetHeight() - 20;
myG2D->Write(font, txtx+1, txty+1, myG2D->FindRGB(80, 80, 80), -1, text);
myG2D->Write(font, txtx, txty, myG2D->FindRGB(255, 255, 255), -1, text);
That concludes the SetupFrame method. The next logical method to look at is
the FinishFrame method. In the FinishFrame method, we first complete the ren-
dering process with the following two lines of code:
myG3D->FinishDraw ();
myG3D->Print (NULL);
Then we retrieve the current position of the camera (i.e., of the player), and also
intialize a vector to represent the orientation of the player to zero, as we are not
implementing it in this sample. The code that does this can be seen here:
FPOINT3 vPosition;
vPosition.x = (view->GetCamera()->GetTransform().GetOrigin().x * X_SCALE) +
X_OFFSET;
vPosition.y = (view->GetCamera()->GetTransform().GetOrigin().z * Y_SCALE) +
Y_OFFSET;
vPosition.z = (view->GetCamera()->GetTransform().GetOrigin().y * X_SCALE) +
Z_OFFSET;
FPOINT3 vOrientation = {0.0f, 0.0f, 0.0f};
We then inform the OMS of our new position by calling the SetMotionByGUID
method of the COMS class and passing in our unique ID (m_guidAvatar), the
position, and the orientation, as shown here:
m_pOMS->SetMotionByGUID(m_guidAvatar, vPosition, vOrientation);
That concludes the FinishFrame method. The final method we now need to look
at is the CleanUp method. This method first checks whether our OMS pointer is
Integrating the OMS with an Existing Application | 209
Integrating the OMS without the Wrapper
valid; if so, it creates a new position and orientation vector, both initialized to 0,
then makes a call to the SetMotionByGUID method. This can be seen here:
if ( m_pOMS )
{
FPOINT3 vPosition;
vPosition.x = (0.0f * X_SCALE) + X_OFFSET;
vPosition.y = (0.0f * Y_SCALE) + Y_OFFSET;
vPosition.z = (0.0f * Z_SCALE) + Z_OFFSET;
FPOINT3 vOrientation = {0.0f, 0.0f, 0.0f};
m_pOMS->SetMotionByGUID(m_guidAvatar, vPosition, vOrientation);
Note that the reason we are doing this is to reset the player’s position back to the
origin. We could omit this and leave the player where he or she was before dis-
connecting or, for example, in an RPG (role-playing game), we could move the
player to the nearest safe location (such as a town).
After this, we make a call to the ServerLogout method of the COMS class,
passing in how long we are prepared to wait for a successful logout in millisec-
onds (which in this example we specify as two seconds). The call to this method
is shown here:
m_pOMS->ServerLogout(2000);
After we are logged out, we can delete the COMS object and set the pointer to it
to NULL:
delete m_pOMS;
m_pOMS = NULL;
}
The final task is to delete the memory we allocated for the server IP, port, player
username, and player password. This can be seen in this final code segment:
if ( m_pcServerIP )
delete [] m_pcServerIP;
m_pcServerIP = NULL;
if ( m_pcServerPort )
delete [] m_pcServerPort;
m_pcServerPort = NULL;
if ( m_pcUsername )
delete [] m_pcUsername;
m_pcUsername = NULL;
if ( m_pcPassword )
delete [] m_pcPassword;
m_pcPassword = NULL;
Events
The events that were handled in DemoSky OMS Example 1 are explained in the
following sections.
210 | Chapter 5
Integrating the OMS without the Wrapper
OMS_EVENT_LOGON_PASS
The first event we handle is the successful login of the client. When this occurs,
we simply output this to the console. The case for this event is as follows:
case OMS_EVENT_LOGON_PASS:
{
char pcText[1024];
printf("Logged In Successfully as %s\n", GetUsername());
printf("Grid Sample (logged in as %s)\n", GetUsername());
}
break;
OMS_EVENT_LOGON_FAIL
This message is received if the player login was unsuccessful. You can see this
event occurring if you enter an incorrect username or password within your
ServerInfo.cfg file. In this sample, we simply output the fact that the login was
unsuccessful; however, this could be handled within the application and ask the
user to re-enter the information — we will see this more fully in the tutorials at
the end of the book.
Here is the case we have used for handling this method in this application:
case OMS_EVENT_LOGON_FAIL:
printf("\n\n\nLogon Failed!!!\n\n\n");
break;
OMS_EVENT_IDENT_LIST_CHANGE
This message is received when the server sends a new list of entities that can be
embodied by the player. This message is generally received after a successful
login attempt.
In the case for this message, we first create a temporary unsigned integer
variable called uCount and also a STL vector of type NBIDENTITY called
vIdentities.
UINT uCount = 0;
std::vector<BNIDENTITY> vIdentities;
We then check to see if our COMS pointer is valid and make a call to the
GetIdentities method of the COMS class, passing in our STL vector to be filled
up with possible identities that can be embodied. Note that the GetOMS method
is defined within DemoSky.h and simply returns our member pointer to the
COMS object. This can be seen here:
if (GetOMS() && BN_SUCCESS(GetOMS()->GetIdentities(vIdentities)))
{
Note also that the BN_SUCCESS is a macro for testing success of all the OMS
methods.
If the call to GetIdentities was successful, we can then test how many possi-
ble identities can be embodied by assessing the size of the STL vector with the
following line of code:
uCount = vIdentities.size();
Integrating the OMS with an Existing Application | 211
Integrating the OMS without the Wrapper
After we find the size, if there were any identities available to embody (i.e.,
uCount > 0), we select the first available one by passing position 0 of the
vIdentities vector into the OMS method SelectIdentity, shown below.
if (uCount > 0)
{
printf("Selecting first ident\n");
GetOMS()->SelectIdentity(vIdentities[0]);
}
The SelectIdentity method simply imforms the OMS of which identity the client
should embody. The complete case for this event can be seen here for reference:
case OMS_EVENT_IDENT_LIST_CHANGE:
{
UINT uCount = 0;
std::vector<BNIDENTITY> vIdentities;
OMS_EVENT_EMBODY_DONE
After the identity for the player has been set, the server will embody the player
and then inform the client it is ready by means of the OMS_EVENT_
EMBODY_DONE message.
From here, we call the SetAvatar method, passing in its unique ID, which is
obtained from the guidThing member of the EventInfo structure. This call can
be seen here:
SetAvatar(pEventInfo->guidThing);
The SetAvatar method is a simple method, defined within the DemoSky.h header
file, which records the GUID of the player into the member variable
m_guidAvatar. The code for this method can be seen here:
void SetAvatar(BNGUID guidThing) {m_guidAvatar = guidThing;}
We then make a call to the UpdateThing method, passing in the unique ID along
with a Boolean true to signify that this Thing is client controlled. The call to this
method can be seen here:
UpdateThing(pEventInfo->guidThing, true);
212 | Chapter 5
Integrating the OMS without the Wrapper
The UpdateThing method is used to acquire and store the attributes associated
with Things in the world (i.e., the CAttrib members we have defined within the
CThing derived classes). So within the UpdateThing method, we first define the
following temporary variables:
bool bRetVal = false;
static std::vector<CThingAttributeValue> vAttributes;
UINT uAttrib;
char *pcTemp = NULL;
bRetVal = true;
return bRetVal;
}
After this call, we then test whether the object is client or server controlled by
examining the Boolean value bClientControlled that was passed into the
method.
If the Thing was not client controlled, we know it will be a CAnimal thing, so
we want to get its attributes — its position, orientation, and also our user-
defined CStringAttrib attribute pAnimation.
To get the list of attributes from the OMS, we make a call to the
GetStatesByGUID method, which is a member of the COMS class, passing in
the unqiue ID of the Thing and the STL vector vAttributes for the OMS to fill up
with CAttrib objects (remembering that all attributes, such as CStringAttrib, are
derived from the CAttrib class). The call to the GetStatesByGUID method can
be seen here:
m_pOMS->GetStatesByGUID(guidThing, vAttributes);
Integrating the OMS with an Existing Application | 213
Integrating the OMS without the Wrapper
Once we have our attributes, we can then loop through them by setting our
uAttrib variable to 0, then looping until it is greater than or equal to the size of
our vector size, as shown here:
for (uAttrib = 0; uAttrib < vAttributes.size(); uAttrib++)
{
Then, for each of the attributes, we can switch the m_idState variable, which
determines what the attribute relates to.
switch ( vAttributes[uAttrib].m_idState )
{
The first attribute we handle within this switch statement is the built-in
BUTTERFLY_POSITION attribute. This is used to denote the position of the
Thing within the world. The class used to define this attribute is the
CVectorAttrib class, hence it has a 3D world position contained within it. To
actually retrieve these three values, we access the m_Attribute variable, which is
a member of the CThingAttributeValue class and is the type we defined the STL
vector to be at the start of the method. This definition for this class can be seen
here:
class CThingAttributeValue
{
public:
STATEID m_idState; // This is the state ID.
BNOBJECTTYPE m_typeObject; // This is the state subtype. It is 0
// if it can be set by the client. It
// is a non-zero value for a state
// that can only be set by the server,
// daemon, or wizard.
bool m_bDirty; // This is true if the state is dirty.
CTHINGATTRIBUTE m_Attribute; // This is the value for the state.
public:
//! This is the constructor.
CThingAttributeValue() {}
From this class, we then access the m_Attribute member, which is of type
CTHINGATTRIBUTE. The CTHINGATTRIBUTE structure is shown below.
typedef struct cThingAttribute
{
int iListIndex; // This is the list index that this
// value represents.
214 | Chapter 5
Integrating the OMS without the Wrapper
As you can see, this CTHINGATTRIBUTEUNION union contains all the possi-
ble variable types that can be used with the CAttrib derived classes. As we know
the position is a three-dimensional vector, we access the vVector member of this
union, from which we can then access the x, y, and z components of the vector.
This can be seen in the following code segment:
case BUTTERFLY_POSITION:
bRetVal = SetThingPosition(guidThing,
vAttributes[uAttrib].m_Attribute.Value.vVector.x,
vAttributes[uAttrib].m_Attribute.Value.vVector.y,
vAttributes[uAttrib].m_Attribute.Value.vVector.z);
break;
As you can see from the preceding code segment, the three positional values (x,
y, and z) are passed into our user-defined SetThingPosition method, which we
will look at now.
The SetThingPosition method updates our world, as in the 3D geometry, with
the correct postion of the Thing that has moved.
Within this method, the first thing we must do is find the iMeshWrapper
within the world that relates to the object that is moved. This is done via our
user-defined FindThingItem method, to which we pass in the unique ID of our
Thing and it returns the mesh associated with it. The call to this method can be
seen here:
csRef<iMeshWrapper> spThing = FindThingItem(guidThing);
Integrating the OMS with an Existing Application | 215
Integrating the OMS without the Wrapper
Within our DemoSky.h header file, we have defined the following structure:
typedef struct ThingItem
{
BNGUID guidThing;
csRef<iMeshWrapper> spThing;
As you can see, this structure basically contains the unique ID of the Thing and
Y
also a smart pointer to the mesh that is used to represent the Thing within the
FL
world. We also have an STL vector called m_vThingList in the header, which is
used to record all the objects within our world (which are registered with the
server). The definition of this can be seen here:
AM
std::list< THINGITEM > m_vThingList;
Therefore, what our FindThingItem method does is search through this list,
comparing the guidThing passed in with the one contained within each of the
TE
return NULL;
}
Then we can make a call to the SetMotionByGUID method of the COMS class,
passing in the unique ID of the Thing, along with pointers to the two local vari-
ables we created to hold the position and orientation.
Team-Fly®
216 | Chapter 5
Integrating the OMS without the Wrapper
spThing->GetMovable()->SetPosition (pos);
spThing->GetMovable()->UpdateMove ();
Note also the call to UpdateMove, which executes the position change within
the world.
After this, we output to the console the new position of the object, along with
its unique ID and return from the method.
Back in our UpdateThing method, the next case is for the orientation of the
object changing. Note that we do not implement the orientation change in this
example. We will see this in the tutorials at the end of the book, but we have still
included the case to handle this, as shown below:
case BUTTERFLY_ORIENTATION:
bRetVal = SetThingOrientation(guidThing,
vAttributes[uAttrib].m_Attribute.Value.vVector.x,
vAttributes[uAttrib].m_Attribute.Value.vVector.y,
vAttributes[uAttrib].m_Attribute.Value.vVector.z);
break;
The final case for the non-client control Thing properties is our user-defined
property BN_ATTRIB_ANIMATION. In this, we acquire the string data from
the CTHINGATTRIBUTEUNION by allocating and copying it to our local char
pointer, pcTemp. Once we have the string, we pass it to the SetThingAnimation
method, which again in this example is unimplemented. This case can be seen
here:
case BN_ATTRIB_ANIMATION:
pcTemp = new char[vAttributes[uAttrib].m_Attribute.Value.String.iLength + 1];
strncpy(pcTemp, vAttributes[uAttrib].m_Attribute.Value.String.pcData,
vAttributes[uAttrib].m_Attribute.Value.String.iLength);
pcTemp[vAttributes[uAttrib].m_Attribute.Value.String.iLength] = 0;
bRetVal = SetThingAnimation(guidThing, pcTemp);
delete [] pcTemp;
break;
If the returned data for the attributes was either a string or a blob of data, it is
our responsibility to deallocate the memory. So we first check if the returned
data was of type PROPERTY_STRING or PROPERTY_LIST_STRING. If it
was, we delete the char array contained within the object, then set all the other
data to be NULL. This can be seen in the following block of code:
Integrating the OMS with an Existing Application | 217
Integrating the OMS without the Wrapper
if (( vAttributes[uAttrib].m_Attribute.Type == PROPERTY_STRING )
|| ( vAttributes[uAttrib].m_Attribute.Type == PROPERTY_LIST_STRING ))
{
if ( vAttributes[uAttrib].m_Attribute.Value.String.pcData != NULL )
delete [] vAttributes[uAttrib].m_Attribute.Value.String.pcData;
vAttributes[uAttrib].m_Attribute.Value.String.pcData = NULL;
vAttributes[uAttrib].m_Attribute.Value.String.iLength = 0;
vAttributes[uAttrib].m_Attribute.Type = PROPERTY_NULL;
}
The same also applies for the blob data, which is cleaned up with the following
segment of code:
if (( vAttributes[uAttrib].m_Attribute.Type == PROPERTY_BLOB ))
{
if ( vAttributes[uAttrib].m_Attribute.Value.Blob.pvData != NULL )
delete [] (UBYTE *)vAttributes[uAttrib].m_Attribute.Value.Blob.pvData;
vAttributes[uAttrib].m_Attribute.Value.Blob.pvData = NULL;
vAttributes[uAttrib].m_Attribute.Value.Blob.iLength = 0;
vAttributes[uAttrib].m_Attribute.Type = PROPERTY_NULL;
}
The other part to this method is when the object is client controlled. If this is the
case, we use the CAvatar class, not the CAnimal class. Again, in this section we
call the GetStatesByGUID method of the COMS class and loop through the
attributes with the following code:
m_pOMS->GetStatesByGUID(guidThing, vAttributes);
However, since the Thing is client controlled, there is no need to retrieve the
position or orientation here, so the only attribute we are interested in is our
user-defined BN_ATTRIB_IDENTITY attribute, which determines if the player
is a man, woman, ghost, etc. So as with the non-client controlled section, we
create a switch statement for the m_idState member and test for the
BN_ATTRIB_IDENTITY state. If the state is there, we can then make a call to
our SetThingIdentity method, passing in the unique ID of the Thing as well as
the actual enumerated value stored within the attribute, denoted by the lLong
member of the Value object. This can all be seen here:
switch ( vAttributes[uAttrib].m_idState )
{
case BN_ATTRIB_IDENTITY:
bRetVal = SetThingIdentity(guidThing,
vAttributes[uAttrib].m_Attribute.Value.lLong);
break;
}
After this, we then have the same code to delete the string or blob as applicable
and we return out of the UpdateThing method.
That was a bit of a sidestep; now back to our main event switch cases.
218 | Chapter 5
Integrating the OMS without the Wrapper
OMS_EVENT_EMBODY_FAIL
The OMS_EVENT_EMBODY_FAIL message is sent by the server if the client
could not be embodied into the selected identity. The code in our application for
this case is as follows:
case OMS_EVENT_EMBODY_FAIL:
printf("\n\n\nEmbodied Avatar Failed!!!\n\n\n");
break;
Although we have omitted it in this sample, we could try here to embody an ava-
tar again, but it is likely that a fatal error has occurred.
OMS_EVENT_THING_NEW
The OMS_EVENT_THING_NEW event is handled as follows within our appli-
cation code:
case OMS_EVENT_THING_NEW:
CreateThing(pEventInfo->guidThing);
break;
As you can see, all that we do here is make a call to our user-defined
CreateThing method, passing in the unique ID of the Thing that needs to be
added (created) to our world. Let us look at the CreateThing method now.
In the CreateThing method, we create a Boolean return value, a temporary
char array for manipulating strings, and a variable called typeObject of type
BNOBJECTTYPE, as shown below:
bool DemoSky::CreateThing(BNGUID guidThing)
{
bool bRetVal = false;
char pcTemp[256];
BNOBJECTTYPE typeObject;
Once we have this, we next check that we have a valid pointer to the COMS
class as follows:
if(GetOMS())
{
We then notify the console that we are adding a new Thing to the world using
the following two lines of code:
printf("Created Thing %ld of type %d\n", guidThing, typeObject);
fflush(stdout);
After this, we then want to retrieve the current position and orientation of the
object. We do this by using the GetMotionByGUID method of the COMS class,
passing in the unique ID of the Thing as we have seen before, along with a
Integrating the OMS with an Existing Application | 219
Integrating the OMS without the Wrapper
pointer to a position array and an orientation array. This can all be seen in the
following code segment:
FPOINT3 vPosition = {0.0f, 0.0f, 0.0f};
FPOINT3 vOrientation = {0.0f, 0.0f, 0.0f};
if (BN_SUCCESS(m_pOMS->GetMotionByGUID(guidThing, vPosition, vOrientation)))
{
We then convert the positional values (x, y, and z) into a csVector3 structure.
Again we scale and translate the values into our world coordinates (defined at
the top of the DemoSky.cpp source file).
csVector3 pos;
pos.x = (vPosition.x - X_OFFSET) / X_SCALE;
pos.y = (vPosition.z - Z_OFFSET) / Z_SCALE;
pos.z = (vPosition.y - Y_OFFSET) / Y_SCALE;
After we have the correct position for our new Thing entering the world, we
next obtain the sector that our world is contained within.
iSector* room = engine->GetSectors()->FindByName ("room");
If the sector was valid (which it should be), we continue by creating a new sprite
using the factory we loaded in the Initialize method. This will create another
black cuboid that we can place within our world. The code for this can be seen
here:
if (room)
{
// Add the sprite to the engine.
csRef<iMeshWrapper> sprite (engine->CreateMeshWrapper (imeshfact,
"MySprite", room, csVector3 (-3, 5, 3)));
After this, we switch the type of the object. Note here that we could create dif-
ferent models within the world for different types; however, in this example we
will simply adjust the scaling, as shown in the following switch statement.
switch ( typeObject )
{
case 0:
case 1:
m *= 2.0;
break;
case 2:
case 3:
m *= 1.0;
break;
default:
m *= 0.25;
break;
}
220 | Chapter 5
Integrating the OMS without the Wrapper
So, if the object is of type 0 or type 1, we double the scaling, and if it is type 2 or
3, we keep it the normal size; otherwise we reduce it to a quarter of its original
size.
We then perform the scaling by obtaining the SetTransform method, passing
in our scaling matrix, m. This can be seen here:
sprite->GetMovable()->SetTransform(m);
We set the position of our new Thing by calling the SetPosition method, passing
in the position we obtained and converted from the OMS, pos. This can be seen
in the following line of code:
sprite->GetMovable()->SetPosition (pos);
We then invoke the scaling and position adjusting commands by calling the
UpdateMove method:
sprite->GetMovable()->UpdateMove();
Then we perform the rest of our sprite setup as we have seen in previous exam-
ples. The code for this can be seen here for reference:
csRef<iSprite3DState> spstate (SCF_QUERY_INTERFACE (sprite->GetMeshObject (),
iSprite3DState));
spstate->SetAction ("default");
sprite->SetZBufMode (CS_ZBUF_USE);
sprite->SetRenderPriority (engine->GetObjectRenderPriority ());
sprite->DeferUpdateLighting (CS_NLIGHT_STATIC|CS_NLIGHT_DYNAMIC, 10);
The final part of this CreateThing method is a call to our user-defined method
AddThingItem. Into this method we pass in the unique ID of our new Thing as
well as the smart pointer to the sprite we just created, as shown below.
AddThingItem(guidThing, sprite);
In the AddThingItem method, we first check whether the sprite is valid (pThing)
by ensuring it is not a null pointer. This can be seen here:
bool DemoSky::AddThingItem(BNGUID guidThing, iMeshWrapper *pThing)
{
// Make sure that a valid pointer has been passed in
if ( pThing )
{
If the sprite was valid, we ensure the object is not already in our list by making a
call to another of our user-defined methods called FindThingItem, passing in the
unique ID of the Thing we wish to test for.
if (!FindThingItem(guidThing))
{
So, providing the object was not found already within our THINGITEM vec-
tor, we then create a temporary THINGITEM object as follows:
THINGITEM Temp;
Then we assign the unique ID of the Thing we have just created and also the
pointer to the sprite into the members of the THINGITEM structure:
Temp.guidThing = guidThing;
Temp.spThing = pThing;
Then finally, we add it to the end of our THINGITEM vector, using the follow-
ing line of code:
m_vThingList.push_back(Temp);
OMS_EVENT_THING_HERE
The OMS_EVENT_THING_HERE event is sent if another Thing comes near
the player. We handle this event by making a call to the UpdateThing method as
follows:
UpdateThing(pEventInfo->guidThing, (pEventInfo->guidThing == m_guidAvatar));
Notice how we compare the guidThing member of the EventInfo structure to our
guid (denoted as m_guidAvatar) to determine if we are controlling the Thing or
not.
OMS_EVENT_THING_SET
The OMS_EVENT_THING_SET event is transmitted when at least one state of
another Thing has changed. Again, for this event we call the UpdateThing
method, which we have previously seen. The UpdateThing method updates all
the required states of the objects.
OMS_EVENT_THING_DROP
This event is sent when a Thing moves out of the range of the client and hence
should no longer be visible on the client’s display. To deal with this, we make a
call to our user-defined method RemoveThing, passing in the unique ID of the
Thing we need to remove, which we obtained from the EventInfo structure. The
call to this can be seen here:
RemoveThing(pEventInfo->guidThing);
In the RemoveThing method we first output to the console that we are removing
an object from the world.
printf("Removed Thing %ld", guidThing);
Then we find the sprite relating to the Thing that needs to be removed using our
FindThingItem method as follows:
csRef<iMeshWrapper> sprite = FindThingItem(guidThing);
222 | Chapter 5
Integrating the OMS without the Wrapper
Once we have the reference to the sprite that relates to the Thing, we can remove
it from the world by calling the RemoveObject method of the engine, passing in
the object to be removed.
engine->RemoveObject(sprite);
The RemoveThingItem method simply iterates through the global Thing list,
m_vThingList, until it finds the guidThing ID. When it finds it, it safely removes
it from the list and the method returns. The complete RemoveThingItem method
can be seen here for reference:
iMeshWrapper *DemoSky::RemoveThingItem(BNGUID guidThing)
{
csRef<iMeshWrapper> spTemp = 0;
std::list< THINGITEM >::iterator viterThing;
return spTemp;
}
OMS_EVENT_THING_GONE
The OMS_EVENT_THING_GONE event is received when a Thing has been
removed from the world (i.e., a player disconnects). This is handled in exactly
the same way as the OMS_EVENT_THING_DROP event.
OMS_EVENT_MESSAGE_USER_OFFLINE
This event occurs if the client executes the MessageFind method of the OMS
(which is used to locate other players on the server to initiate chat messages) and
the player searched for is currently offline. We handle this event by calling our
user-defined method ReceiveOffline, which simply outputs to the console in this
example but could be used to inform the user of this failure. The complete
ReceiveOffline method for this example can be seen here for reference:
bool DemoSky::ReceiveOffline(char *pcID, ULONG ulKey)
{
bool bRetVal = false;
char pcTemp[256];
Integrating the OMS with an Existing Application | 223
Integrating the OMS without the Wrapper
bRetVal = true;
return bRetVal;
}
OMS_EVENT_MESSAGE_USER_PING
This event is the opposite of OMS_EVENT_MESSAGE_USER_OFFLINE in
that it is received if the user searched for via the MessageFind call is online. The
dummy method we have created to handle this event (which simply outputs to
the console) can be seen here:
bool DemoSky::ReceivePing(char *pcID, ULONG ulKey)
{
bool bRetVal = false;
char pcTemp[256];
bRetVal = true;
return bRetVal;
}
OMS_EVENT_MESSAGE_RECEIVED
This event signifies that an instant chat message has been received. The
guidThing member of the EventInfo structure contains the Thing’s unique ID.
The thingType member contains the message flags, the pcMessage member con-
tains the message string (not necessarily null terminated), and the
usMessageLength member contains the length of the message. (The message
array is allocated with extra characters to allow a null terminator to be added
without reallocating the string.) Finally, pcUsername contains the username of
the client that sent the message.
When we receive this event, we first cast the typeThing member to
BN_MESSAGE_TYPE, which is an enumeration defined within the
ClientObjectDefines.h header file as follows:
enum BN_MESSAGE_TYPE
{
BN_MESSAGE_TYPE_TEXT_CHAT, // Text-based messages
BN_MESSAGE_TYPE_TEXT_MAX = 0x7F,
// All binary data comes after this
BN_MESSAGE_TYPE_BINARY_PROJECTILE, // Origin (guid), Target Position
// (float) X, Y, Z, Velocity
// (float), Name (string),
// [Explosion (string)]
BN_MESSAGE_TYPE_BINARY_MAX,
};
224 | Chapter 5
Integrating the OMS without the Wrapper
As can be seen, we have two different types of messages that can be received —
chat messages and projectile messages.
If it is a chat message, we handle it with the following case:
switch ((BN_MESSAGE_TYPE)pEventInfo->typeThing) // Flags cast to BN_MESSAGE_TYPE
{
case BN_MESSAGE_TYPE_TEXT_CHAT:
ReceiveMessage(pEventInfo->pcUsername, pEventInfo->pcMessage,
pEventInfo->guidThing);
break;
As you can see, we pass the relevant information into our user-defined
ReceiveMessage method. Let’s look at this method now.
The ReceiveMessage method simply checks that the message is a valid
pointer, then outputs it to the console. The complete method for this can be seen
here:
bool DemoSky::ReceiveMessage(char *pcUsername, char *pcMessage, ULONG ulKey)
{
bool bRetVal = false;
char pcTemp[1024];
if ( pcMessage )
{
printf("User %s (0x%04x) said: %s\n", pcUsername ? pcUsername :
"UNKNOWN", ulKey, pcMessage);
fflush(stdout);
bRetVal = true;
}
return bRetVal;
}
Since the binary data is sent as a string, the ReceiveProjectile method needs to
interpret the message and convert it back to its original binary format (i.e.,
extract the values from it).
Within the method, we first create a char pointer to hold the start of the
pcData char array passed in (which contains the message string data). Then we
also define an unsigned long variable called ulLong:
char *pcStartPos = pcData;
ULONG ulLong;
We then check that the message is at least the minimum expected size, using the
following:
if ((usDataLength > sizeof(BNGUID) + (sizeof(float) * 4) + 2))
{
Integrating the OMS with an Existing Application | 225
Integrating the OMS without the Wrapper
The message should contain the unique ID of the Thing that sent it (i.e.,
BNGUID), the size of four floating-point values, plus an additional two bytes
for the name string.
If the message was the correct size, we can begin extracting the data from it.
We first extract the unique ID from the string using the following code segment:
BNGUID guidOrigin;
memcpy(&ulLong, pcData, sizeof(ULONG));
guidOrigin = ntohl(ulLong);
pcData += sizeof(BNGUID);
After this, we repeat the process to retrieve the four floating-point values, which
are the x, y, z, and velocity of the projectile.
Y
memcpy(&ulLong, pcData, sizeof(ULONG));
ulLong = ntohl(ulLong);
FL
float fX = *((float *)&ulLong);
pcData += sizeof(ULONG);
memcpy(&ulLong, pcData, sizeof(ULONG));
AM
ulLong = ntohl(ulLong);
float fY = *((float *)&ulLong);
pcData += sizeof(ULONG);
memcpy(&ulLong, pcData, sizeof(ULONG));
ulLong = ntohl(ulLong);
TE
float fVelocity;
memcpy(&ulLong, pcData, sizeof(ULONG));
ulLong = ntohl(ulLong);
fVelocity = *((float *)&ulLong);
pcData += sizeof(ULONG);
After that, we can read in the name of the projectile, as it will be null terminated
when it is placed into the string. The code to extract the name can be seen here:
char *pcProjectile = pcData;
pcData += strlen(pcProjectile) + 1;
If there is still data in the string, the remaining data represents an optional addi-
tional string to denote the type of explosion this imaginary projectile will have.
The code to extract the final, optional string can be seen here:
char *pcExplosion = NULL;
if ((int)usDataLength > (pcData - pcStartPos))
{
pcExplosion = pcData;
}
So, in this example, we can simply output this information to the console using
the following segment of code:
printf("Projectile from %ld to %4.2f, %4.2f %4.2f @ %4.2f named %s", guidOrigin,
fX, fY, fZ, fVelocity, pcProjectile);
if ( pcExplosion )
{
printf(" ending in %s\n", pcExplosion);
Team-Fly®
226 | Chapter 5
Integrating the OMS without the Wrapper
}
else
{
printf("\n");
}
fflush(stdout);
OMS_EVENT_MESSAGE_RECEIVED_SECURE
The final event we check for is OMS_EVENT_MESSAGE_RECEIVED_
SECURE. This message is similar to OMS_EVENT_MESSAGE_RECEIVED,
except that it provides a way of receiving a secure message. Note also that this
message can contain a request for a response, which we handle in the case
statement.
First, we check whether the message contains data, and then we call the
MessageFind method of the OMS to assess if the user who sent the message is
online. This can be seen here:
if (pEventInfo->pcMessage && pEventInfo->pcUsername)
{
// Find the user since the response can send a message
if (m_pOMS)
m_pOMS->MessageFind(m_guidAvatar, pEventInfo->pcUsername, true);
Next, we display the question to the console and also check to see if there are
custom accept and reject messages contained within the received message.
sprintf(pcMessage, "%s asks: %s", pEventInfo->pcUsername, pEventInfo->pcMessage);
strcpy(pcAccept, pEventInfo->pcAccept ? pEventInfo->pcAccept : "Yes");
strcpy(pcReject, pEventInfo->pcReject ? pEventInfo->pcReject : "No");
The ReceiveSecure method checks to see if the question passed in is valid and
also that the pointer to our COMS object is valid. This can be seen in the follow-
ing code:
bool DemoSky::ReceiveSecure(EventInfo *pData, char *pcQuestion)
{
bool bRetVal = false;
char pcTemp[256];
If everything is okay, we display a yes/no message box to get the response from
the user. We then send the response using the MessageSecureRespond method of
the OMS. This can all be seen in the following segment of code:
if (MessageBox(NULL, pcQuestion, "Secure Question", MB_YESNO) == IDYES)
{
GetOMS()->MessageSecureRespond(true, pData);
Integrating the OMS with an Existing Application | 227
Integrating the OMS without the Wrapper
The final part of our Update method is to delete the pEventInfo object. Remem-
ber that the OMS allocates it, but we must delete it. This is done with the
following code:
if ( pEventInfo )
delete pEventInfo;
Let’s now see a summary of all the events we have just looked at (and also one
that we have not looked at).
Table 5-4: OMS events
Event Description
Event Description
/*
Copyright (C) 1998-2000 by Jorrit Tyberghein
Copyright (C) 2001 by W.C.A. Wijngaards
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#include "cssysdef.h"
#include "cssys/sysfunc.h"
#include "grid-oms/OMSWrapper.h"
#include "demosky.h"
#include "cstool/proctex.h"
#include "cstool/prsky.h"
#include "cstool/csview.h"
#include "cstool/initapp.h"
#include "csutil/cmdhelp.h"
#include "ivideo/graph3d.h"
#include "ivideo/graph2d.h"
230 | Chapter 5
Integrating the OMS with the Wrapper
#include "ivideo/natwin.h"
#include "ivideo/txtmgr.h"
#include "ivideo/fontserv.h"
#include "ivaria/conout.h"
#include "imesh/sprite2d.h"
#include "imesh/object.h"
#include "imap/parser.h"
#include "iengine/mesh.h"
#include "iengine/engine.h"
#include "iengine/sector.h"
#include "iengine/camera.h"
#include "iengine/movable.h"
#include "iengine/material.h"
#include "imesh/thing/polygon.h"
#include "imesh/thing/thing.h"
#include "ivaria/reporter.h"
#include "igraphic/imageio.h"
#include "iutil/comp.h"
#include "iutil/eventh.h"
#include "iutil/eventq.h"
#include "iutil/event.h"
#include "iutil/objreg.h"
#include "iutil/csinput.h"
#include "iutil/virtclk.h"
#include "iutil/vfs.h"
#include "imesh/sprite3d.h"
#include "ClientCreates.h"
CS_IMPLEMENT_APPLICATION
va_end (arg);
}
DemoSky::DemoSky ()
{
sky = NULL;
sky_f = NULL;
sky_b = NULL;
sky_l = NULL;
sky_r = NULL;
sky_u = NULL;
sky_d = NULL;
}
DemoSky::~DemoSky ()
{
CleanUp();
delete sky;
delete sky_f;
delete sky_b;
delete sky_l;
delete sky_r;
delete sky_u;
delete sky_d;
}
void Cleanup ()
{
csPrintf ("Cleaning up...\n");
iObjectRegistry* object_reg = System->object_reg;
delete System; System = NULL;
csInitializer::DestroyApplication (object_reg);
}
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_CONSOLEOUT,
CS_REQUEST_END))
{
Report (CS_REPORTER_SEVERITY_ERROR, "Couldn't init app!");
return false;
}
// Open the main system. This will open all the previously loaded plug-ins.
iNativeWindow* nw = myG2D->GetNativeWindow ();
if (nw) nw->SetTitle ("Crystal Space Procedural Sky Demo");
if (!csInitializer::OpenApplication (object_reg))
{
Report (CS_REPORTER_SEVERITY_ERROR, "Error opening system!");
Cleanup ();
exit (1);
}
234 | Chapter 5
Integrating the OMS with the Wrapper
font = myG2D->GetFontServer()->LoadFont(CSFONT_LARGE);
// Some commercials...
Report (CS_REPORTER_SEVERITY_NOTIFY, "Crystal Space Procedural Sky Demo.");
// First disable the lighting cache. Our app is simple enough not to need this.
engine->SetLightingCacheMode (0);
p = walls_state->CreatePolygon ();
p->SetMaterial (imatu);
p->CreateVertex (csVector3 (-size, simi, -size));
p->CreateVertex (csVector3 (size, simi, -size));
p->CreateVertex (csVector3 (size, simi, size));
p->CreateVertex (csVector3 (-size, simi, size));
p = walls_state->CreatePolygon ();
p->SetMaterial (imatf);
p->CreateVertex (csVector3 (-size, size, simi));
Y
p->CreateVertex (csVector3 (size, size, simi));
p->CreateVertex (csVector3 (size, -size, simi));
FL
p->CreateVertex (csVector3 (-size, -size, simi));
p = walls_state->CreatePolygon ();
TE
p->SetMaterial (imatr);
p->CreateVertex (csVector3 (simi, size, size));
p->CreateVertex (csVector3 (simi, size, -size));
p->CreateVertex (csVector3 (simi, -size, -size));
p->CreateVertex (csVector3 (simi, -size, size));
p = walls_state->CreatePolygon ();
p->SetMaterial (imatl);
p->CreateVertex (csVector3 (-simi, size, -size));
p->CreateVertex (csVector3 (-simi, size, size));
p->CreateVertex (csVector3 (-simi, -size, size));
p->CreateVertex (csVector3 (-simi, -size, -size));
p = walls_state->CreatePolygon ();
p->SetMaterial (imatb);
p->CreateVertex (csVector3 (size, size, -simi));
p->CreateVertex (csVector3 (-size, size, -simi));
p->CreateVertex (csVector3 (-size, -size, -simi));
p->CreateVertex (csVector3 (size, -size, -simi));
Team-Fly®
236 | Chapter 5
Integrating the OMS with the Wrapper
// End
engine->Prepare ();
return true;
}
void DemoSky::SetupFrame ()
{
Integrating the OMS with an Existing Application | 237
Integrating the OMS with the Wrapper
if (kbd->GetKeyState (CSKEY_RIGHT))
view->GetCamera ()->GetTransform ().RotateThis (CS_VEC_ROT_RIGHT, speed);
if (kbd->GetKeyState (CSKEY_LEFT))
view->GetCamera ()->GetTransform ().RotateThis (CS_VEC_ROT_LEFT, speed);
if (kbd->GetKeyState (CSKEY_PGUP))
view->GetCamera ()->GetTransform ().RotateThis (CS_VEC_TILT_UP, speed);
if (kbd->GetKeyState (CSKEY_PGDN))
view->GetCamera ()->GetTransform ().RotateThis (CS_VEC_TILT_DOWN, speed);
if (kbd->GetKeyState (CSKEY_UP))
view->GetCamera ()->Move (CS_VEC_FORWARD * 2.0f * speed);
if (kbd->GetKeyState (CSKEY_DOWN))
view->GetCamera ()->Move (CS_VEC_BACKWARD * 2.0f * speed);
view->Draw();
// Modified
const char *text = "Escape quits."
" Arrow keys/pgup/pgdown to move.";
// End
int txtx = 10;
int txty = myG2D->GetHeight() - 20;
myG2D->Write(font, txtx+1, txty+1, myG2D->FindRGB(80,80,80), -1, text);
myG2D->Write(font, txtx, txty, myG2D->FindRGB(255,255,255), -1, text);
}
void DemoSky::FinishFrame ()
{
myG3D->FinishDraw ();
myG3D->Print (NULL);
FPOINT3 vPosition;
vPosition.x = (view->GetCamera()->GetTransform().GetOrigin().x * X_SCALE) +
X_OFFSET;
vPosition.y = (view->GetCamera()->GetTransform().GetOrigin().z * Y_SCALE) +
Y_OFFSET;
238 | Chapter 5
Integrating the OMS with the Wrapper
return false;
}
/*---------------------------------------------------------------------*
* Main function
*---------------------------------------------------------------------*/
int main (int argc, char* argv[])
{
srand (time (NULL));
// Main loop.
csDefaultRunLoop(System->object_reg);
Cleanup ();
return 0;
}
Integrating the OMS with an Existing Application | 239
Integrating the OMS with the Wrapper
// Added
void DemoSky::CleanUp()
{
if (GetOMS())
{
// Put the client back to a known spot
FPOINT3 vPosition;
vPosition.x = (0.0f * X_SCALE) + X_OFFSET;
vPosition.y = (0.0f * Y_SCALE) + Y_OFFSET;
vPosition.z = (0.0f * Z_SCALE) + Z_OFFSET;
FPOINT3 vOrientation = {0.0f, 0.0f, 0.0f};
GetOMS()->SetMotionByGUID(m_guidAvatar, vPosition, vOrientation);
if (!GetOMS())
return false;
UpdateThingView(guidThing, bClientControlled);
vAttributes[uAttrib].m_Attribute.Value.Blob.iLength = 0;
vAttributes[uAttrib].m_Attribute.Type = PROPERTY_NULL;
}
}
}
return bRetVal;
}
if (GetOMS())
{
if (BN_SUCCESS(GetOMS()->GetTypeByGUID(guidThing, typeObject)))
{
printf("Created Thing %ld of type %d\n", guidThing, typeObject);
fflush(stdout);
bRetVal = true;
}
else
printf("Created Thing FAILED could not get thing type\n");
}
else
printf(pcTemp, "Created Thing FAILED could not get oms pointer\n");
fflush(stdout);
return bRetVal;
}
bRetVal = true;
return bRetVal;
}
RemoveThingItem(guidThing);
bRetVal = true;
return bRetVal;
}
printf(" -> Thing %ld moved to position %4.2f, %4.2f, %4.2f", guidThing,
fX, fY, fZ);
spThing->GetMovable()->SetPosition(pos);
spThing->GetMovable()->UpdateMove ();
bRetVal = true;
return bRetVal;
}
{
THINGITEM Temp;
Temp.guidThing = guidThing;
Temp.spThing = pThing;
m_vThingList.push_back(Temp);
}
return true;
}
return false;
}
return NULL;
}
return spTemp;
}
// End
Integrating the OMS with an Existing Application | 245
Integrating the OMS with the Wrapper
DemoSky.h
/*
Copyright (C) 1998-2000 by Jorrit Tyberghein
Y
You should have received a copy of the GNU Library General Public
FL
License along with this library; if not, write to the Free
Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
AM
09/05/2003 - Modified for Butterfly.net Example
*/
#ifndef DEMOSKY_H
TE
#define DEMOSKY_H
#include <stdarg.h>
#include "csgeom/math2d.h"
#include "csgeom/math3d.h"
// Added
#include <list>
//** OMS Includes
#include "../butterfly-grid/grid-common/thing/thing_types.h"
class COMS;
struct EventInfo;
struct iMeshFactoryWrapper;
// End
class csProcSky;
class csProcSkyTexture;
class Flock;
struct iSector;
struct iView;
struct iEngine;
struct iDynLight;
struct iMaterialWrapper;
struct iPolygon3D;
struct iFont;
struct iMeshWrapper;
struct iMaterialWrapper;
struct iLoader;
struct iKeyboardDriver;
Team-Fly®
246 | Chapter 5
Integrating the OMS with the Wrapper
struct iGraphics3D;
struct iGraphics2D;
struct iVirtualClock;
struct iObjectRegistry;
struct iEvent;
// Added
typedef struct ThingItem
{
BNGUID guidThing;
csRef<iMeshWrapper> spThing;
// the sky
csProcSky *sky;
// the six sides (front, back, left, right, up, down)
csProcSkyTexture *sky_f, *sky_b, *sky_l, *sky_r, *sky_u, *sky_d;
// Added
csRef<iMeshFactoryWrapper> imeshfact;
void CleanUp();
/****************************************************************************\
Thing/Avatar event handlers
\****************************************************************************/
void EventThingNew(BNGUID guidThing, BNOBJECTTYPE typeThing);
void EventAvatarNew(BNGUID guidThing, BNOBJECTTYPE typeThing);
void EventThingHere(BNGUID guidThing, BNOBJECTTYPE typeThing, bool
bClientControlled);
void EventThingSet(BNGUID guidThing, BNOBJECTTYPE typeThing, bool
bClientControlled);
void EventThingDrop(BNGUID guidThing, BNOBJECTTYPE typeThing, bool
bClientControlled);
void EventThingGone(BNGUID guidThing, BNOBJECTTYPE typeThing, bool
bClientControlled);
public:
DemoSky ();
virtual ~DemoSky ();
#endif // DEMOSKY_H
248 | Chapter 5
Integrating the OMS with the Wrapper
We then need to add the OMSWrapper.cpp file into our project, which is located
in the following folder:
c:\crystal\butterfly-grid\grid-oms
Once these change are made and the code is recompiled, it should run in exactly
the same manner before we implemented the wrapper.
Let’s now look at the changes we have made to use the wrapper.
First, within the DemoSky.h header file, we now make the DemoSky class
inherit the COMSWrapper class:
class DemoSky : public COMSWrapper
Then within the class definition, we have removed many of our user-defined
method prototypes, replacing them with the following:
/******************************************************************************\
Thing/Avatar event handlers
\******************************************************************************/
void EventThingNew(BNGUID guidThing, BNOBJECTTYPE typeThing);
void EventAvatarNew(BNGUID guidThing, BNOBJECTTYPE typeThing);
void EventThingHere(BNGUID guidThing, BNOBJECTTYPE typeThing, bool
bClientControlled);
void EventThingSet(BNGUID guidThing, BNOBJECTTYPE typeThing, bool
bClientControlled);
void EventThingDrop(BNGUID guidThing, BNOBJECTTYPE typeThing, bool
bClientControlled);
void EventThingGone(BNGUID guidThing, BNOBJECTTYPE typeThing, bool
bClientControlled);
bool EventThingSetPosition(BNGUID guidThing, float fX, float fY, float fZ);
bool EventAvatarSetPosition(BNGUID guidThing, float fX, float fY, float fZ);
These methods are defined within the COMSWrapper class as pure virtual, so
they need to be overridden in the class that inherits them, since we need to create
an instance of the DemoSky class.
Basically, the aim of the wrapper is to handle most of the tasks internally,
allowing you to keep the code as clean and simple as possible. So, instead of
creating your own Update method that switches the events, it is done internally
within the wrapper, then these virtual methods are called, passing in the relevant
data. For example, when a Thing is created on the server, i.e., when an
OMS_EVENT_THING_NEW event occurs, the wrapper will call the
EventThingNew method or the EventAvatarNew method, depending on whether
it is a client- or server-controlled Thing.
Integrating the OMS with an Existing Application | 249
Integrating the OMS with the Wrapper
So how easy does this make it then? Well, in our main code, we first include
the header file for the wrapper with the following line of code:
#include "grid-oms/OMSWrapper.h"
The next change is in the Initialize method, where we connect to the server.
Instead of parsing the ServerInfo.cfg file, creating the OMS, and logging in,
which was quite a lot of code, we can call one simple method that does all of
this for us, as shown here:
ConnectUsingServerInfoFile("ServerInfo.cfg", NUM_CLIENT_OBJECTS, CreateArray,
ObjectArray);
This method is declared within the COMSWrapper class and hence we have
access to it because our DemoSky class inherits the COMSWrapper class. As
you can see, the first parameter is the name of the configuration file, and the sec-
ond is the number of create methods we have, along with the array of function
pointers and enumerations of object types. If you remember from the previous
example, this was located at the end of our own user-defined CreateOMS
method, whereby we called the method SetupCreateThingTable to do this.
After this call, we should be connected to the server, logged in, and embodied
into the correct avatar. We also have a valid pointer to the OMS if we require it,
which we will later in the code. This can be obtained by a call to the GetOMS
method as defined in the COMSWrapper class.
Next, we have changed the start of the SetupFrame method to call the wrap-
per’s Update method, passing in the number of events to processes this frame
and the maximum number of events that can be left remaining. The call to this
can be seen here:
void DemoSky::SetupFrame ()
{
// process OMS events...
Update(100, 10);
In the FinishFrame method we have the same code that simply calls the
GetOMS method to obtain the pointer to the OMS. This can be seen here:
GetOMS()->SetMotionByGUID(GetAvatar(), vPosition, vOrientation);
Methods
Let’s now look at each of the methods we have overridden.
EventThingNew
The EventThingNew method is called upon a new Thing being added into the
game world, i.e., when the OMS receives an OMS_EVENT_THING_NEW
event. When this is called, we simply pass the guidThing ID into the
CreateThing method we created before. This is shown here for reference:
void DemoSky::EventThingNew(BNGUID guidThing, BNOBJECTTYPE typeThing)
{
CreateThing(guidThing);
}
EventAvatarNew
The EventAvatarNew method is also called upon the OMS receiving an
OMS_EVENT_THING_NEW. The difference between this and the
EventThingNew method is that this one is called if the unique ID of the Thing
that needs to be created is the player (the client). The method for
EventAvatarNew is shown here:
void DemoSky::EventAvatarNew(BNGUID guidThing, BNOBJECTTYPE typeThing)
{
CreateThing(guidThing);
}
EventThingHere
The EventThingHere method is called upon receipt of an OMS_EVENT_
THING_HERE event, which signifies that a player has come within range of
another and hence the positional data should be updated. This is done by passing
the unique ID of the object, as well as a Boolean value to state whether it is
client controlled or not, into the UpdateThing method we created in the previous
example.
The EventThingHere method can be seen here:
void DemoSky::EventThingHere(BNGUID guidThing, BNOBJECTTYPE typeThing, bool
bClientControlled)
{
UpdateThing(guidThing, bClientControlled);
}
EventThingSet
The EventThingSet method is called upon an OMS_EVENT_THING_SET
event occurring, which as we discovered before is sent when at least one state of
a Thing has changed. We again handle this event by calling our UpdateThing
method. This is shown in the complete definition of the EventThingSet method:
void DemoSky::EventThingSet(BNGUID guidThing, BNOBJECTTYPE typeThing, bool
bClientControlled)
{
Integrating the OMS with an Existing Application | 251
Integrating the OMS with the Wrapper
UpdateThing(guidThing, bClientControlled);
}
EventThingDrop
The EventThingDrop method is invoked when the the OMS_EVENT_THING_
DROP event happens, i.e., when a player has moved out of the range of the
client. We handle this by passing the unique ID of the Thing into the
RemoveThing method. This can be seen in the definition for the
EventThingDrop method.
void DemoSky::EventThingDrop(BNGUID guidThing, BNOBJECTTYPE typeThing, bool
bClientControlled)
{
RemoveThing(guidThing);
}
EventThingGone
The EventThingGone method is invoked when the OMS_EVENT_THING_
GONE event occurs, i.e., when a player has disconnected from the game. We
handle this by passing the unique ID of the Thing into the RemoveThing
method. This can be seen in the definition for the EventThingGone method:
void DemoSky::EventThingGone(BNGUID guidThing, BNOBJECTTYPE typeThing, bool
bClientControlled)
{
RemoveThing(guidThing);
}
EventThingSetPosition
Finally we have EventThingSetPosition. (Note that we have also implemented
EventAvatarSetPosition; however, we have made it simply call the
EventThingSetPosition method.) Here is the complete EventThingSetPosition
method for reference:
bool DemoSky::EventThingSetPosition(BNGUID guidThing, float fX, float fY,
float fZ)
{
bool bRetVal = false;
char pcTemp[256];
printf(" -> Thing %ld moved to position %4.2f, %4.2f, %4.2f", guidThing,
fX, fY, fZ);
spThing->GetMovable()->SetPosition(pos);
252 | Chapter 5
Summary
spThing->GetMovable()->UpdateMove ();
printf(" -> Thing %ld moved to position %4.2f, %4.2f, %4.2f\n",
guidThing, pos.x, pos.y, pos.z);
fflush(stdout);
}
bRetVal = true;
return bRetVal;
}
Summary
In this chapter, you discovered how to obtain the latest client OMS libraries and
how to integrate the OMS into an existing application by using the COMS class
directly and the COMSWrapper class, also provided with the client libraries.
We will be putting what we have learned in this chapter to a more practical
use in the following tutorial chapters in which we create a complete demo game,
which includes how to sign up new users to your game, log users in, and allow
them to chat with other players.
Chapter 6
Introduction
In the first part of the demo game tutorial, we are going to look at how to build
the front-end GUI for the demo game. This will involve creating login and
signup dialogs, as well as an in-game chat dialog that will allow the players in
the game to communicate.
Installing Qt
To create our in-game GUI, we are going to use a GUI design application called
Qt. With this application, we are able to plan out our graphical user interface in a
visual environment. After it is designed, we can then save it, convert it (using
another tool), and with a minor tweak here and there we can use it directly with
Crystal Space’s AWS (Alternate Windowing System).
First things first: We need to download the evaluation version of the Qt
Designer. To download it, go to the following web site.
' http://www.trolltech.com/download/qt/evaluate.html
253
254 | Chapter 6
Installing Qt
Once in the installation screen, click Next >; you will be asked for your name,
company name, and serial number. Check the e-mail account that you used to
register; you should have received the serial number, which will enable a 30-day
free trial.
Proceed by clicking Next, then agree to the license agreement. After this, you
will be presented with the following options:
Demo Game Part 1 — Building the GUI | 255
Installing Qt
Y
FL
Figure 6-3: Installation options
AM
Since we are simply using the tool to design our GUI and create the output that
we will then convert, we do not really require any of the options, such as Build
TE
Examples and Build Tutorials. However, you may wish to leave them in out of
personal interest if you have plenty of space. The same applies to the integration
options, as they are not really relevant, but leave this set to Microsoft Visual
C++.
After setting these options, continue by clicking the Next button. The next
screen that appears looks as follows:
Team-Fly®
256 | Chapter 6
Designing the GUI
Again, unless you have specific reason to change any of the options, the best
idea is to leave then at their default settings.
The next step is configuring which database drivers should be accessible
from the application.
As with the previous step, this is again irrelevant to what we are going to be
using the application for. Unless you have specific requirements or are planning
to use the application for other tasks, simply leave the database driver settings as
they are.
After clicking Next at this stage, the program will begin installing and then
continue by building the examples.
Select File 4 New… from the main menu to see the following window:
In the New/Open window, select the Dialog option and then click OK. Once this
is done, you should see a window with Form1 in the title bar visible in the center
panel, as shown in Figure 6-8.
We can now begin designing our first dialog, the login dialog.
Pressing Return after entering the new name ensures that it will be updated
within Qt Designer. Notice that in the Object Explorer panel, the form is now
named correctly, as shown in the following image.
However, if you look at the actual form, it is still named “Form1.” The reason
for this is because we have actually changed the name of the form, as in what it
will be referred to within our application, but we have not changed the caption,
i.e., the title of the window.
To do this, we need to change the Caption property in the same panel where
we changed the Name property. Let’s change this now to read Login. Let’s also
resize the Login dialog to be a bit smaller. Once these two things are done, it
should look similar to the following:
260 | Chapter 6
Designing the GUI
Once placed, make sure the PixmapLabel is selected, then change its Name
property to Logo. Click on the … (browse) button to the right of the Pixmap
property and select the logo.jpg Butterfly Grid logo we used in Chapter 3 when
we were exploring the 2D capabilities of Crystal Space. Once you have selected
the Butterfly Grid logo and moved it to the position on the dialog you think suits
it best, it should look as follows.
262 | Chapter 6
Designing the GUI
Next we need to add two text input boxes to allow the player to type in his or her
username and password. We also need two labels to denote what the boxes are
for.
Let’s start by adding the Username text input box. To do this, first click on
the Common Widgets tab on the left. Once there, select the LineEdit option and
click on the Login dialog as you did when placing the PixmapLabel.
Demo Game Part 1 — Building the GUI | 263
Designing the GUI
As it stands, the dialog is pretty non-user friendly. So let’s make it better by add-
ing two labels to show which input box is which.
This is done by selecting the TextLabel option from the Common Widgets tab
and clicking on the Login dialog. Once placed, copy it for the password label,
and then change the Text property to read Username… for the username label
and change it to read Password… for the password label. Once these changes
have been made, it should look similar to Figure 6-17.
Demo Game Part 1 — Building the GUI | 265
Designing the GUI
Y
FL
AM
TE
The final part of our Login dialog is the Create Player and Login buttons. To add
a button, click on the PushButton option on the left-hand menu, and drag the
button to the size you require on the Login dialog. Create two buttons using this
technique, changing the Name property of the first one (on the left) to
CreatePlayer and the second to Login. Then change the Text properties appropri-
ately so the user knows what the buttons actually do, as shown in the following
screen shot.
Team-Fly®
266 | Chapter 6
Designing the GUI
Because we actually want something to happen when the buttons are clicked, we
add a special string to the whatsThis property of each of the buttons that can be
interpreted later by the Crystal Space Alternate Windowing System. What we
are actually going to do here is specify a trigger for each button that will be set
off whenever the button is clicked, which then, in the code, will be associated
with a method call. Take a look at the following line:
c:signalClicked,loginSink::CreatePlayer
This will trigger the CreatePlayer trigger, which is associated with the loginSink
sink, when the button is clicked. So we want to place this in the whatsThis prop-
erty of the Create Player button.
Then for the Login button, we will require the following text to be placed
within its whatsThis property.
c:signalClicked,loginSink::Login
Don’t be too concerned with this for the moment; it will make sense in the sec-
ond part of this tutorial when we convert the files into the correct format for
Crystal Space.
This is all we need for the Login dialog, so the next thing to do is save it as
logindialog.ui in the following location:
c:\crystal\CS\data\temp
Demo Game Part 1 — Building the GUI | 267
Designing the GUI
Next we are going to add five text input boxes with a label above each so the
user knows what they are for. We will name the text input boxes as follows:
SignupFullname
SignupEmail
SignupUsername
SignupPassword
SignupConfirmPassword
268 | Chapter 6
Designing the GUI
Once placed on the dialog, it should look something like the following screen
shot.
Note that for the SignupPassword and SignupConfirmPassword text input boxes,
we have changed echoMode to Password so that the user’s input is displayed as
asterisks.
Once the text input boxes and labels have been placed and named appropri-
ately, all that is required is a Cancel button in case the user pressed the Create
Player button by accident, and a Signup button to confirm he or she wishes to
proceed with the signup process. These two buttons will be named
SignupCancel and DoSignup, and will look as follows once placed on the
Signup dialog.
Demo Game Part 1 — Building the GUI | 269
Designing the GUI
The final part to our Signup dialog is setting the triggers for the buttons, as we
did for the Login dialog.
So for the Cancel button, we require the following string to be placed in the
whatsThis property:
c:signalClicked,signupSink::SignupCancel
Now that this is done, save it as signupdialog.ui in the same folder as the
logindialog.ui:
c:\crystal\CS\data\temp
So, again, start out by closing any other dialogs and create a new dialog.
Once created, resize it to about the average size of a message box and give it the
title Error and set the actual name of the dialog to ErrorDialog. It should then
look similar to the following:
All we actually need on this dialog is a label in which we can change the text to
display the error message to the user and a button so the user can acknowledge
reading the message. Give the label the name ErrorLabel and the button the
name ErrorOk, and place them so they resemble the following screen shot.
Demo Game Part 1 — Building the GUI | 271
Designing the GUI
Notice in the preceding screen shot how the text label has been dragged out to
allow a larger amount of text to fit in. Also note that we have changed the
vAlign property to AlignTop, making the text within the label always start at the
top of the rectangular area defined for it.
Once this is done, we need to set the whatsThis property for the OK button to
the following:
c:signalClicked,errorSink::ErrorOk
This completes the Error dialog, so save it in the same location as the other two
with the filename errordialog.ui.
Let’s first create the new dialog, giving it the name ChatDialog and the title
Chat. Once created, change the dimensions so it looks similar to the following:
Once we have sized and named the dialog, we want to add a text box to display
the chat messages. Select the TextArea option from the left-hand panel and then
drag out an area for it on the dialog. Once this is done, it should look as follows:
Demo Game Part 1 — Building the GUI | 273
Designing the GUI
We want to make it always have the vertical scroll bar visible, so choose
AlwaysOn from the vScrollBarMode property drop-down menu, then rename
the text area to ChatMessageArea.
Next we are going to add a text input box and a button to allow the user to
enter his or her chat messages. These should be named ChatInput and ChatSend,
respectively, and should look something similar to the following:
274 | Chapter 6
Converting the Dialogs
Finally, we need to add the trigger to the Send button, which is done by placing
the following string in the whatsThis property of the Send button:
c:signalClicked,chatSink::Send
Save the Chat dialog with the filename chatdialog.ui in the same directory as the
other three dialogs.
<height>148</height>
</rect>
</property>
<property name="caption">
<string>Error</string>
</property>
<widget class="QPushButton">
<property name="name">
<cstring>ErrorOk</cstring>
</property>
<property name="geometry">
<rect>
<x>120</x>
<y>90</y>
Y
<width>90</width>
<height>40</height>
</rect>
</property> FL
<property name="text">
AM
<string>Ok</string>
</property>
</widget>
<widget class="QLabel">
TE
<property name="name">
<cstring>ErrorLabel</cstring>
</property>
<property name="geometry">
<rect>
<x>10</x>
<y>20</y>
<width>320</width>
<height>50</height>
</rect>
</property>
<property name="text">
<string>ErrorLabel</string>
</property>
<property name="textFormat">
<enum>AutoText</enum>
</property>
<property name="alignment">
<set>AlignTop</set>
</property>
</widget>
</widget>
<layoutdefaults spacing="6" margin="11"/>
</UI>
As you can see, each Thing is defined by a class. For example, the main dialog
is referred to as the QDialog class, and the button is referred to as QPushButton.
So basically, what needs to be done is this needs to be parsed and converted
into an AWS definition file that can then be used within Crystal Space to con-
struct the GUI from. Luckily, Crystal Space comes with an extensible style sheet
that can be used to transform the XML into an AWS definition file.
Team-Fly®
276 | Chapter 6
Converting the Dialogs
To use this, however, you need an XSTL style sheet processor. Xalan-Java, a
free and easy to use processor, is available from the following web link:
' http://xml.apache.org/xalan-j/downloads.html
To use this tool, you will also need the standard edition of the Java Runtime
Environment (preferably version 1.4.1), which is available from the following
link.
' http://java.sun.com
Once you get to the Xalan download site, select xalan-j_2_5_0-bin.zip. After it
has downloaded, extract the zip file to the following directory:
c:\crystal\xalan
Once extracted, you can use the Xalan tool to convert your .ui files. To do this,
first go to the Windows command prompt and change to the c:\crystal\xalan
directory.
Let’s start by converting the error dialog. First we need to enter the following
command (in the Xalan directory):
java org.apache.xalan.xslt.Process -IN c:\crystal\CS\data\temp\
errordialog.ui -XSL c:\crystal\CS\scripts\aws\qt3aws.xsl >
c:\crystal\CS\data\temp\errordialog.txt
If you now open up errordialog.txt, which was saved in the same directory as the
errordialog.ui file, it should look as follows:
Listing 6-2: errordialog.txt
window "ErrorDialog"
{
Style: wfsNormal
Options: wfoGrip+wfoTitle+wfoClose+wfoMin+wfoZoom+wfoControl
Frame: (0,0) - (345,148)
Title: "Error"
component "ErrorOk" is "Command Button"
{
Frame: (120,90) - (210,130)
Caption: "Ok"
}
Demo Game Part 1 — Building the GUI | 277
Converting the Dialogs
As you can see, a lot of unnecessary (and unimplemented) information has been
cut out of the Qt Designer file and it has been put into the correct format for
AWS to understand.
Notice how the ErrorDialog has been changed from being a “QDialog” to
now being a “window” and that the button is now referred to as a “Command
Button,” rather than a “QPushButton.” Table 6-1 shows how the Qt widgets port
to AWS.
Table 6-1: Qt Designer to AWS conversions
Qt Designer AWS Equivalent
QLabel Label
QDialog Window
QWidget Window
QTabWidget Notebook
Next, we need to convert the other three dialogs using the same technique,
which gives us the following three AWS definition files.
278 | Chapter 6
Converting the Dialogs
MaskChar: "*"
}
component "SignupCancel" is "Command Button"
{
Frame: (30,330) - (141,381)
Caption: "Cancel"
connect
{
signalClicked -> signupSink::SignupCancel
}
}
component "DoSignup" is "Command Button"
{
Frame: (220,330) - (331,381)
Caption: "Signup ->"
connect
{
signalClicked -> signupSink::DoSignup
}
}
}
Once we have all four definitions we require, the next step is to merge them with
another file that contains some other AWS definitions for specifying which
images should be used for the windows, etc., also known as the “skin” for the
windows. So, create a new text file in the c:\crystal\CS\data\temp directory
called demogame.def. In this new file, the first thing we need to specify is the
skin for the windows. To do this, place the following at the top of the text file:
Demo Game Part 1 — Building the GUI | 281
Converting the Dialogs
Note here that “/aws/” refers to the AWS Virtual File System (VFS) path, which
is actually the zip file awsdef.zip, as it is defined as following in the VFS con-
figuration (vfs.cfg):
VFS.Mount.aws = $@data$/awsdef.zip
If you look within the awsdef.zip file, you will find the PNG images the previ-
ous code is referring to and you can modify them as required to create the look
and feel you wish to give your GUI system.
282 | Chapter 6
Converting the Dialogs
Next, in each of the four dialog files, we need to make a change to the fol-
lowing line:
wfoGrip+wfoTitle+wfoClose+wfoMin+wfoZoom+wfoControl
This removes the Close, Minimize, and Maximize buttons for all our dialogs, as
we do not really require them.
The other change we need is to make the image work in the Login dialog.
Since we need to reference the VFS rather than a standard file path, we need to
change the following section in logindialog.txt from this:
component "Logo" is "Image View"
{
Frame: (30,30) - (130,130)
}
to this…
component "Logo" is "Image View"
{
Frame: (30,30) - (130,130)
Image: "/lib/butterfly/logo.jpg"
}
Once this is done, we can concatenate all our files onto the end of the skin defi-
nition in our demogame.def file, giving us the following complete Windows
definition file:
Listing 6-7: demogame.def
skin "Normal Windows"
{
Texture: "/aws/texture.png"
HighlightColor: 230,230,230
ShadowColor: 60,60,60
FillColor: 200,200,200
TextDisabledColor: 128,128,0
TextForeColor: 0,0,0
TextBackColor: 255,255,255
ButtonTextColor: 0,0,192
OverlayTextureAlpha: 128
ScrollBarHeight: 16
ScrollBarWidth: 16
WindowMin: "/aws/minimize.png"
WindowZoom: "/aws/zoom.png"
WindowClose: "/aws/close.png"
WindowMinAt: (46, 6) - (46-11, 6+10)
WindowZoomAt: (34, 6) - (34-11, 6+10)
WindowCloseAt: (19, 6) - (19-11, 6+10)
CheckBoxUp: "/aws/chkup.png"
CheckBoxDn: "/aws/chkdn.png"
CheckBoxOn: "/aws/chkon.png"
CheckBoxOff: "/aws/chkoff.png"
RadioButtonUp: "/aws/radup.png"
Demo Game Part 1 — Building the GUI | 283
Converting the Dialogs
RadioButtonDn: "/aws/raddn.png"
RadioButtonOn: "/aws/radon.png"
RadioButtonOff: "/aws/radoff.png"
TreeCollapsed: "/aws/treecol.png"
TreeExpanded: "/aws/treeexp.png"
TreeVertLine: "/aws/treevl.png"
TreeHorzLine: "/aws/treehl.png"
TreeChkUnmarked: "/aws/treechke.png"
TreeChkMarked: "/aws/treechkf.png"
TreeGrpUnmarked: "/aws/treegrpe.png"
TreeGrpMarked: "/aws/treegrpf.png"
ScrollBarUp: "/aws/sbup.png"
ScrollBarDn: "/aws/sbdn.png"
ScrollBarRt: "/aws/sbrt.png"
ScrollBarLt: "/aws/sblt.png"
}
window "ErrorDialog"
{
Style: wfsNormal
Options: wfoGrip+wfoTitle+wfoControl
Frame: (0,0) - (345,148)
Title: "Error"
component "ErrorOk" is "Command Button"
{
Frame: (120,80) - (210,110)
Caption: "Ok"
}
component "ErrorLabel" is "Label"
{
Frame: (10,20) - (330,70)
Caption: "ErrorLabel"
}
}
window "LoginDialog"
{
Style: wfsNormal
Options: wfoGrip+wfoTitle+wfoControl
Frame: (0,0) - (458,229)
Title: "Login"
component "Logo" is "Image View"
{
Frame: (30,30) - (130,130)
Image: "/lib/butterfly/logo.jpg"
}
window "SignupDialog"
{
Style: wfsNormal
Options: wfoGrip+wfoTitle+wfoControl
Frame: (0,0) - (369,405)
Title: "Signup"
component "textLabel1" is "Label"
{
Frame: (30,30) - (150,50)
Caption: "Fullname..."
}
component "textLabel1_2" is "Label"
{
Frame: (30,90) - (150,110)
Caption: "Email Address..."
}
component "textLabel1_2_2" is "Label"
{
Frame: (30,150) - (150,170)
Caption: "Username..."
Demo Game Part 1 — Building the GUI | 285
Converting the Dialogs
}
component "textLabel1_2_2_2" is "Label"
{
Frame: (30,200) - (150,220)
Caption: "Password..."
}
component "textLabel1_2_2_2_2" is "Label"
{
Frame: (30,260) - (180,280)
Caption: "Confirm Password..."
}
component "SignupFullname" is "Text Box"
{
Frame: (40,50) - (330,70)
Y
}
component "SignupEmail" is "Text Box"
{
}
FL
Frame: (40,110) - (330,130)
AM
component "SignupUsername" is "Text Box"
{
Frame: (40,170) - (330,190)
}
TE
Team-Fly®
286 | Chapter 6
Testing the GUI
window "ChatDialog"
{
Style: wfsNormal
Options: wfoGrip+wfoTitle+wfoControl
Frame: (0,0) - (575,160)
Title: "Chat"
component "ChatArea" is "Multiline Edit"
{
Frame: (10,10) - (550,100)
Style: fsSunken
}
component "ChatInput" is "Text Box"
{
Frame: (10,110) - (420,130)
}
component "ChatSend" is "Command Button"
{
Frame: (430,110) - (561,130)
Caption: "Send ->"
connect
{
signalClicked -> chatSink::Send
}
}
}
#include "iengine/sector.h"
#include "iengine/engine.h"
#include "iengine/camera.h"
#include "iengine/light.h"
#include "iengine/statlght.h"
#include "iengine/texture.h"
#include "iengine/mesh.h"
#include "iengine/movable.h"
#include "iengine/material.h"
#include "imesh/thing/polygon.h"
#include "imesh/thing/thing.h"
#include "imesh/object.h"
#include "ivideo/graph3d.h"
#include "ivideo/graph2d.h"
#include "ivideo/txtmgr.h"
#include "ivideo/texture.h"
#include "ivideo/material.h"
#include "ivideo/fontserv.h"
#include "ivideo/natwin.h" // ADDED
#include "igraphic/image.h"
#include "igraphic/imageio.h"
#include "imap/parser.h"
#include "ivaria/reporter.h"
#include "ivaria/stdrep.h"
#include "csutil/cmdhelp.h"
CS_IMPLEMENT_APPLICATION
// Application Specific...
csRef<iFont> font;
// [END] Application Specific
Butterfly::~Butterfly ()
{
}
void Butterfly::SetupFrame ()
{
if(!g3d->BeginDraw (CSDRAW_2DGRAPHICS)) return;
g2d->Clear(0);
// draw text...
288 | Chapter 6
Testing the GUI
char buf[256];
sprintf(buf, "Butterfly Grid Tutorial Game");
g2d->Write(font, 10,10, fntcol, -1, buf);
aws->Redraw ();
aws->Print (g3d, 64);
}
void Butterfly::FinishFrame ()
{
g3d->FinishDraw ();
g3d->Print (NULL);
}
return aws->HandleEvent(ev);
}
bool Butterfly::Initialize ()
{
if (!csInitializer::RequestPlugins (object_reg,
CS_REQUEST_VFS,
CS_REQUEST_SOFTWARE3D,
CS_REQUEST_ENGINE,
CS_REQUEST_FONTSERVER,
Demo Game Part 1 — Building the GUI | 289
Testing the GUI
CS_REQUEST_IMAGELOADER,
CS_REQUEST_LEVELLOADER,
CS_REQUEST_REPORTER,
CS_REQUEST_REPORTERLISTENER,
CS_REQUEST_PLUGIN("crystalspace.window.alternatemanager", iAws),
// ADDED
CS_REQUEST_END))
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Can't initialize plugins!");
return false;
}
return false;
}
// ADDED
// Set the window title...
iNativeWindow* nw = g2d->GetNativeWindow ();
if (nw) nw->SetTitle("Demo Game Tutorial 1");
// 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.butterfly",
"Error opening system!");
return false;
}
// load preferences...
if(!aws->GetPrefMgr()->Load ("/this/data/temp/demogame.def"))
csReport(object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"couldn't load definition file!");
font = g2d->GetFontServer()->LoadFont(CSFONT_LARGE);
return true;
}
void Butterfly::Start ()
{
csDefaultRunLoop (object_reg);
}
font = NULL;
csInitializer::DestroyApplication (object_reg);
return 0;
}
#include <stdarg.h>
#include "csutil/ref.h"
292 | Chapter 6
Testing the GUI
struct iObjectRegistry;
struct iEngine;
struct iLoader;
struct iGraphics2D;
struct iGraphics3D;
struct iKeyboardDriver;
struct iVirtualClock;
struct iEvent;
struct iView;
struct iTextureManager;
class Butterfly
{
private:
iObjectRegistry* object_reg;
csRef<iEngine> engine;
csRef<iLoader> loader;
csRef<iGraphics2D> g2d;
csRef<iGraphics3D> g3d;
csRef<iKeyboardDriver> kbd;
csRef<iVirtualClock> vc;
csRef<iView> view;
csRef<iTextureManager> txtmgr;
// ADDED
csRef<iAws> aws;
iAwsPrefManager* awsprefs;
csRef<iAwsCanvas> awsCanvas;
// [END]
#endif // __BUTTERFLY_H__
When we execute this code, it displays the Login dialog on the screen. However,
note that you will receive some fatal error message boxes first as it requires a
sink. Ignore this for now, as we will be looking at what this is and how we use it
in the next section.
Demo Game Part 1 — Building the GUI | 293
Testing the GUI
Figure 6-28 shows how the Login dialog looks within our Crystal Space
application.
If we then close the application and change the following line of code in the Ini-
tialize method:
iAwsWindow *test = aws->CreateWindowFrom ("LoginDialog");
we can check how the Signup dialog looks. Here is a screen shot of the applica-
tion after this change is made.
294 | Chapter 6
Testing the GUI
We can then check the other two dialogs by replacing the same line of code with
one of the following.
Error dialog:
iAwsWindow *test = aws->CreateWindowFrom ("ErrorDialog");
Chat dialog:
iAwsWindow *test = aws->CreateWindowFrom ("ChatDialog");
Demo Game Part 1 — Building the GUI | 295
Testing the GUI
Here is how the other two dialogs look within our Crystal Space application.
Y
FL
AM
TE
Now that we have seen what the dialogs look like, let’s see the code we have
used to load and display them.
Team-Fly®
296 | Chapter 6
Testing the GUI
The first is an interface to the Alternative Windowing System, and the second is
a pointer to an AwsCanvas interface, which provides the AWS 2D/3D driver.
Next, in our source file, we add includes for the AWS interface and canvas
using the following two lines of code:
#include "iaws/aws.h"
#include "iaws/awscnvs.h"
We also include another header to allow us access to the native window so that
we can change the window title within our code. The line of code for this
include is the following:
#include "ivideo/natwin.h"
Moving next to the Initialize method, the first line added is an extra parameter to
the RequestPlugins method call. This additional parameter, shown below, is used
to initialize the AWS plug-in:
CS_REQUEST_PLUGIN("crystalspace.window.alternatemanager", iAws)
Then the actual AWS interface is acquired using the same method as we used to
acquire the keyboard and graphics, etc., using the CS_QUERY_REGISTRY
macro. This can be seen in the following block of code:
aws = CS_QUERY_REGISTRY (object_reg, iAws);
if (aws == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iAws plugin!");
return false;
}
Next, we use the iNativeWindow interface to set the title of the application to
“Demo Game Tutorial 1.” Note that this is not relevant to AWS, but it is good to
know how to set the title of your application to something other than “Crystal
Space.” Here is the code to perform this:
iNativeWindow* nw = g2d->GetNativeWindow();
if (nw) nw->SetTitle("Demo Game Tutorial 1");
Once the canvas is created, we set an internal flag within AWS to tell it how
redrawing of the windows should be handled. The three options for this are
listed in Table 6-2.
Table 6-2: AWS window flags
Flag Description
AWSF_AlwaysEraseWindows Using this drawing mode makes the AWS erase the windows
before attempting to redraw them.
AWSF_AlwaysRedrawWindows This flag ensures the windows are redrawn every frame,
which is essential if the screen will be cleared every frame
during rendering.
This flag is not required if AWS has complete control over the
rendering process.
AWSF_RaiseOnMouseOver If this flag is set, when the mouse is moved over a window it
will come to the front. If it is not set, the windows will only
come to the foreground if they are clicked on with the mouse.
After this is set, we tell AWS what canvas to use for drawing the windows onto,
which is done using the SetCanvas method:
aws->SetCanvas(awsCanvas);
Recall the demogame.def definition file, the first part of which we copied from
another to define the skin of our window. The first part of our file looked as
follows:
skin "Normal Windows"
{
Texture: "/aws/texture.png"
HighlightColor: 230,230,230
ShadowColor: 60,60,60
FillColor: 200,200,200
TextDisabledColor: 128,128,0
.......[file cut here]
298 | Chapter 6
Testing the GUI
So, in our actual code, we want to now specify the skin for our windows which
we can refer to as Normal Windows, as defined within the definition file. This is
set using the SelectDefaultSkin method of the preference manager pointer as can
be seen in the following line of code:
aws->GetPrefMgr()->SelectDefaultSkin("Normal Windows");
Note that we can specify more than one skin within the definition files and also
load more than one definition file.
We then create an AWS window using the CreateWindowFrom method of the
iAws interface. Note that again we refer to the window by the name we supplied
for it within the definition file. So to create the ChatDialog window, we would
use the following line of code:
iAwsWindow *test = aws->CreateWindowFrom("ChatDialog");
Note that although in our code we refer to the type as an iAwsWindow, this is
simply a typedef for the iAwsComponent interface.
So, once we have a pointer to our new window in the variable test, we can
call the Show method, which tells AWS to make the component visible. This can
be seen in the following line of code:
if(test) test->Show();
return aws->HandleEvent(ev);
}
aws->Redraw();
aws->Print(g3d, 64);
#include "ivaria/stdrep.h"
#include "csutil/cmdhelp.h"
#include "csutil/csstring.h" // ADDED
#include "csutil/scfstr.h" // ADDED
CS_IMPLEMENT_APPLICATION
// Application Specific...
csRef<iFont> font;
// [END] Application Specific
Butterfly::~Butterfly ()
{
}
void Butterfly::SetupFrame ()
{
if(!g3d->BeginDraw (CSDRAW_2DGRAPHICS)) return;
g2d->Clear(0);
// draw text...
int fntcol = g2d->FindRGB (255, 255, 0);
char buf[256];
sprintf(buf, "Butterfly Grid Tutorial Game");
g2d->Write(font, 10,10, fntcol, -1, buf);
aws->Redraw ();
aws->Print (g3d, 128);
}
void Butterfly::FinishFrame ()
{
g3d->FinishDraw ();
g3d->Print (NULL);
}
bool Butterfly::Initialize ()
{
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("crystalspace.window.alternatemanager", iAws),
// ADDED
CS_REQUEST_END))
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Can't initialize plugins!");
return false;
}
// ADDED
// Set the window title...
iNativeWindow* nw = g2d->GetNativeWindow ();
if (nw) nw->SetTitle("Demo Game Tutorial 1");
// 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.butterfly",
"Error opening system!");
return false;
}
// ADDED part 2
// [END]
// load preferences...
if(!aws->GetPrefMgr()->Load ("/this/data/temp/demogame.def"))
csReport(object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"couldn't load definition file!");
// ADDED part 2
loginDialog = aws->CreateWindowFrom("LoginDialog");
signupDialog = aws->CreateWindowFrom("SignupDialog");
errorDialog = aws->CreateWindowFrom("ErrorDialog");
chatDialog = aws->CreateWindowFrom("ChatDialog");
r = loginDialog->Frame();
winWidth = r.xmax - r.xmin;
winHeight = r.ymax - r.ymin;
loginDialog->MoveTo((g2d->GetWidth()/2) - winWidth/2, (g2d->GetHeight()/2) -
winHeight/2);
r = signupDialog->Frame();
winWidth = r.xmax - r.xmin;
winHeight = r.ymax - r.ymin;
signupDialog->MoveTo((g2d->GetWidth()/2) - winWidth/2, (g2d->GetHeight()/2) -
winHeight/2);
r = errorDialog->Frame();
winWidth = r.xmax - r.xmin;
winHeight = r.ymax - r.ymin;
errorDialog->MoveTo((g2d->GetWidth()/2) - winWidth/2, (g2d->GetHeight()/2) -
winHeight/2);
(winHeight+5));
// [END]
loginDialog->Show();
font = g2d->GetFontServer()->LoadFont(CSFONT_LARGE);
return true;
}
Y
// Login dialog methods...
{ FL
void Butterfly::Login(void* awst, iAwsSource *)
iString *password;
TE
appPtr->loginDialog->Hide();
if(!appPtr->username->GetData() || strlen(appPtr->username->GetData()) == 0)
{
// show the error and set the error status...
csRef<iString> errStr = csPtr<iString> (new scfString ("Username
invalid"));
appPtr->errorFromLogin = true;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
else if(!password->GetData() || strlen(password->GetData()) == 0)
{
appPtr->errorFromLogin = true;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
Team-Fly®
306 | Chapter 6
Adding the Sinks
}
else
{
// attempt to log them into the game,
// for now we will just display the chat dialog...
appPtr->chatDialog->Show();
}
fflush (stdout);
}
appPtr->loginDialog->Hide();
appPtr->signupDialog->Show();
}
appPtr->signupDialog->Hide();
appPtr->loginDialog->Show();
}
iString *name;
iString *email;
iString *username;
iString *password;
iString *confirmPassword;
appPtr->signupDialog->FindChild("SignupFullname")->GetProperty("Text",
(void **) &name);
appPtr->signupDialog->FindChild("SignupEmail")->GetProperty("Text",
(void **) &email);
appPtr->signupDialog->FindChild("SignupUsername")->GetProperty("Text",
(void **) &username);
appPtr->signupDialog->FindChild("SignupPassword")->GetProperty("Text",
(void **) &password);
Demo Game Part 1 — Building the GUI | 307
Adding the Sinks
appPtr->signupDialog->FindChild("SignupConfirmPassword")->GetProperty("Text",
(void **) &confirmPassword);
if(!name->GetData() || strlen(name->GetData()) == 0)
{
// show the error and set the error status...
csRef<iString> errStr = csPtr<iString> (new scfString ("Name invalid,
please correct this."));
appPtr->errorFromLogin = false;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
else if(!email->GetData() || strlen(email->GetData()) == 0)
{
appPtr->errorFromLogin = false;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
else if(!username->GetData() || strlen(username->GetData()) == 0)
{
appPtr->errorFromLogin = false;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
else if(!password->GetData() || strlen(password->GetData()) == 0)
{
appPtr->errorFromLogin = false;
appPtr->errorDialog->Show();
308 | Chapter 6
Adding the Sinks
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
else if(!confirmPassword->GetData() || strcmp(password->GetData(),
confirmPassword->GetData()) != 0)
{
appPtr->errorFromLogin = false;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
else
{
// Perform the signup here...
appPtr->errorDialog->Hide();
if(appPtr->errorFromLogin)
appPtr->loginDialog->Show();
else
appPtr->signupDialog->Show();
}
iString *chatStr;
appPtr->chatDialog->FindChild("ChatInput")->GetProperty("Text", (void **)
&chatStr);
void Butterfly::Start ()
{
csDefaultRunLoop (object_reg);
}
font = NULL;
csInitializer::DestroyApplication (object_reg);
return 0;
}
#include <stdarg.h>
#include "csutil/ref.h"
struct iObjectRegistry;
struct iEngine;
struct iLoader;
struct iGraphics2D;
struct iGraphics3D;
struct iKeyboardDriver;
struct iVirtualClock;
struct iEvent;
struct iView;
struct iTextureManager;
struct iString;
class Butterfly
{
310 | Chapter 6
Adding the Sinks
private:
iObjectRegistry* object_reg;
csRef<iEngine> engine;
csRef<iLoader> loader;
csRef<iGraphics2D> g2d;
csRef<iGraphics3D> g3d;
csRef<iKeyboardDriver> kbd;
csRef<iVirtualClock> vc;
csRef<iView> view;
csRef<iTextureManager> txtmgr;
// ADDED
csRef<iAws> aws;
csRef<iAwsCanvas> awsCanvas;
// [END]
// ADDED 2
iString *username;
csRef<iAwsWindow> loginDialog;
csRef<iAwsWindow> signupDialog;
csRef<iAwsWindow> errorDialog;
csRef<iAwsWindow> chatDialog;
public:
Butterfly (iObjectRegistry* object_reg);
~Butterfly ();
#endif // __BUTTERFLY_H__
Demo Game Part 1 — Building the GUI | 311
Adding the Sinks
Figure 6-32 is a screen shot of how this looks once a player has completed the
(not yet implemented) login and enters a few lines of chat into the Chat dialog.
Compile the code and have a look at how the GUI operates. Let’s see what we
have added since the previous section to make it work.
First, in the header, we have added a pointer to an iString object called
username as a member of our Butterfly class. This is defined as follows:
iString *username;
The iString interface is used to contain string data and is commonly used with
AWS, however it is not fully compatible since some parts of AWS use char data.
However, the iString interface contains a method called GetData, which
retrieves the string as a char array.
We then create four references for the four windows we are going to be creat-
ing, i.e., the Login, Signup, Error, and Chat dialogs. We make these of type
iAwsWindow, which as mentioned before is a typedef for the iAwsComponent
interface. These can be seen here:
csRef<iAwsWindow> loginDialog;
csRef<iAwsWindow> signupDialog;
csRef<iAwsWindow> errorDialog;
csRef<iAwsWindow> chatDialog;
Next, we create methods that will be called upon an action being performed
within the GUI, i.e., when a button is pressed.
312 | Chapter 6
Adding the Sinks
First we create the following two prototypes for the Login dialog:
// Login dialog methods...
static void Login(void* awst, iAwsSource *);
static void CreatePlayer(void* awst, iAwsSource *);
The first method will be called when the Login button is pressed and the second
will be called when the Create Player button is pressed. Note that the names of
these methods do not relate to the names assigned to the buttons within Qt
Designer. We’ll see how they get linked to the button actions when we look at
the main source code.
We then have another four methods for the other three dialogs prototyped as
follows:
// Signup dialog methods...
static void SignupCancel(void* awst, iAwsSource *);
static void DoSignup(void* awst, iAwsSource *);
Note also the Boolean variable errorFromLogin. This is used later to determine
which dialog the error was created from so that the GUI can revert to the correct
dialog after the user has read the error message.
Now to the source file. Since the previous section, we have added the follow-
ing header files:
#include "iaws/awsparm.h"
#include "csutil/csstring.h"
#include "csutil/scfstr.h"
#include "iaws/aws.h"
#include "iaws/awscnvs.h"
#include "ivideo/natwin.h"
}
}
Remembering that this is for the Login button, notice in the connect section that
we have linked the signalClicked event to the loginSink, and more directly to the
Login trigger.
So, to refer to this within our code, we first acquire a pointer to the AWS Sink
Manager by calling GetSinkMgr on the aws object. Once we have this, we can
call the CreateSink method, passing in our current instance of the class as a
parameter. This can be seen in the following line of code:
iAwsSink* sink = aws->GetSinkMgr()->CreateSink ((void*)this);
Now that we have a pointer to an iAwsSink object called sink, we can use this to
assign the triggers for the buttons that this sink relates to. In this case we are cre-
ating the loginSink, so we need to assign triggers for the Login button and the
Create Player button being pressed.
So, in our definition file, we have the triggers defined as follows for the two
buttons:
signalClicked -> loginSink::Login
and
signalClicked -> loginSink::CreatePlayer
Note that the methods that are called must have the correct signature and be
static (i.e., not an instance method). The correct signature is as follows:
void MethodName(void *, iAwsSource *)
After the two triggers are registered, we need to register the sink with the AWS
Sink Manager, which is done by again acquiring a pointer to the sink manager,
then making a call to the RegisterSink method. Into this method, we first specify
the name for the sink (which again relates to our definition file), followed by the
iAwsSink object for which we have registered the triggers. The line of code that
does this can be seen here:
aws->GetSinkMgr()->RegisterSink ("loginSink", sink);
We repeat this process for all the other sinks and triggers we have defined within
our definition file, as shown in the following code segment:
sink = aws->GetSinkMgr ()->CreateSink((void*)this);
sink->RegisterTrigger ("SignupCancel", &SignupCancel);
sink->RegisterTrigger ("DoSignup", &DoSignup);
aws->GetSinkMgr ()->RegisterSink ("signupSink", sink);
After the sinks are all registered with AWS, the next part is to create (but not
show) all the dialogs, storing pointers to each of them within the members we
created within our Butterfly class. The code used to create the four dialogs can
be seen here:
loginDialog = aws->CreateWindowFrom("LoginDialog");
signupDialog = aws->CreateWindowFrom("SignupDialog");
errorDialog = aws->CreateWindowFrom("ErrorDialog");
chatDialog = aws->CreateWindowFrom("ChatDialog");
Once we have our dialogs created, we then want to ensure they are all centered
on the screen (initially at least). So we first create three temporary variables to
perform this. These are two doubles to hold the width and height of the dialog
window we are trying to center and also a csRect structure that we will use to
hold the dimensions of the window when we retrieve them from the
iAwsComponent objects (i.e., our dialogs).
double winWidth, winHeight;
csRect r;
The first dialog we center is the Login dialog. To do this we obtain the bound-
aries it extends to by calling the Frame method of the iAwsComponent object,
loginDialog, and storing the result in our csRect object r, as shown below:
r = loginDialog->Frame();
Once we have the boundaries of the dialog, we can then work out the width and
height using the following segment of code:
winWidth = r.xmax - r.xmin;
winHeight = r.ymax - r.ymin;
Then, with the width and height, we can place the dialog in the center of the
screen by calling the MoveTo method of the dialog, passing in the center of the
screen, minus half the width and half the height of the dialog, respectively. This
can be seen here:
loginDialog->MoveTo((g2d->GetWidth()/2) - winWidth/2, (g2d->GetHeight()/2) -
winHeight/2);
We repeat this for the Signup and Error dialogs as shown in the following code
segment:
r = signupDialog->Frame();
winWidth = r.xmax - r.xmin;
winHeight = r.ymax - r.ymin;
signupDialog->MoveTo((g2d->GetWidth()/2) - winWidth/2, (g2d->GetHeight()/2) -
winHeight/2);
r = errorDialog->Frame();
winWidth = r.xmax - r.xmin;
winHeight = r.ymax - r.ymin;
Demo Game Part 1 — Building the GUI | 315
Adding the Sinks
Instead of centering the Chat dialog vertically, we have opted to move it to the
center of the bottom of the screen. This is so we can keep the Chat dialog on the
screen while the user is playing our demo game. To do this, we use the following
code segment:
r = chatDialog->Frame();
winWidth = r.xmax - r.xmin;
winHeight = r.ymax - r.ymin;
chatDialog->MoveTo((g2d->GetWidth()/2) - winWidth/2, g2d->GetHeight() -
(winHeight+5));
Notice how the height has been changed to just move it the height of itself, plus
Y
5 pixels, from the bottom of the screen.
FL
Now that the dialogs are positioned correctly, we complete the Initialize
method by making the loginDialog visible by calling its Show method:
loginDialog->Show();
AM
There are no changes to the rendering, so let’s now look at the methods we cre-
ated to handle the GUI, i.e., the methods we linked our triggers to.
TE
Next, we output the fact the button has been pressed to the console, so we know
the sink/trigger is working.
printf("Login Button Pressed\n");
After this, we create a pointer to an iString to hold the player’s password infor-
mation, which is not allocated any memory:
iString *password;
Team-Fly®
316 | Chapter 6
Adding the Sinks
component we wish to gain access to, which in this case is the Username text
box and the Password text box. This FindChild method then returns the
iAwsComponent object for the GUI object, from which we can call the
GetProperty method, passing in the property we wish to obtain (which is the
Text of the text box) and a pointer to a pointer of an iString object. Note that the
GetProperty method allocates the correct memory required to hold the string
data. The two lines of code to do all this can be seen here:
appPtr->loginDialog->FindChild("Username")->GetProperty("Text", (void **)
&appPtr->username);
appPtr->loginDialog->FindChild("Password")->GetProperty("Text", (void **)
&password);
Next, we hide the Login dialog using the following line of code:
appPtr->loginDialog->Hide();
Then, we make a check to ensure the user has entered something in the
username field. We do this by first checking that the char array contained within
the username iString object is valid (i.e., not null) or the string length of the char
array is not zero.
If the data was null or the string was empty, we create a new iString, which
states that the username was invalid. We then set the Boolean errorFromLogin to
true, so that the Error dialog knows this error came from a login attempt. Then
we show the Error dialog and set the label to read “Username invalid” by calling
the SetProperty method on the ErrorLabel child — passing in Caption as the
property to set and a pointer to the errStr iString as the string to set it to. This
can all be seen in the following segment of code:
if(!appPtr->username->GetData() || strlen(appPtr->username->GetData()) == 0)
{
// show the error and set the error status...
csRef<iString> errStr = csPtr<iString> (new scfString ("Username invalid"));
appPtr->errorFromLogin = true;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
This process is then repeated for the password, using the following block of
code:
else if(!password->GetData() || strlen(password->GetData()) == 0)
{
// show the error and set the error status...
csRef<iString> errStr = csPtr<iString> (new scfString ("Password invalid"));
Demo Game Part 1 — Building the GUI | 317
Adding the Sinks
appPtr->errorFromLogin = true;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
If both the username and password fields contained valid data the user would be
able to log in. For now we simply show the Chat dialog to simulate a successful
login to the game. This can be seen in the final else clause here:
else
{
// attempt to log them into the game,
// for now we will just display the chat dialog...
appPtr->chatDialog->Show();
}
In the next chapter we will actually be performing the login at this point, and
will know if the login was successful or not.
Then we flush the standard output using this final line of code:
fflush (stdout);
Finally, we hide the Login dialog and then show the Signup dialog, using the
following two lines of code:
appPtr->loginDialog->Hide();
appPtr->signupDialog->Show();
appPtr->signupDialog->Hide();
appPtr->loginDialog->Show();
}
We then create five iString pointers to hold the information we are about to grab
from the Signup dialog. The declaration for these can be seen here:
iString *name;
iString *email;
iString *username;
iString *password;
iString *confirmPassword;
Next, we call the GetProperty method on each of the text boxes on the Signup
dialog to obtain all the information. This is done in the same way that we col-
lected the information from the Login dialog and can be seen in the following
code segment:
appPtr->signupDialog->FindChild("SignupFullname")->GetProperty("Text",
(void **) &name);
appPtr->signupDialog->FindChild("SignupEmail")->GetProperty("Text",
(void **) &email);
appPtr->signupDialog->FindChild("SignupUsername")->GetProperty("Text",
(void **) &username);
appPtr->signupDialog->FindChild("SignupPassword")->GetProperty("Text",
(void **) &password);
appPtr->signupDialog->FindChild("SignupConfirmPassword")->GetProperty("Text",
(void **) &confirmPassword);
After this, we print out the collected information and hide the Signup dialog
using the following block of code:
printf("Name was %s\n", name->GetData());
printf("Email was %s\n", email->GetData());
printf("Username was %s\n", username->GetData());
printf("Password was %s\n", password->GetData());
printf("Confirmed Password was %s\n", confirmPassword->GetData());
appPtr->signupDialog->Hide();
Then we check the five fields to ensure that data has been entered into each of
them. For the password and confirm password fields, we also check that the
strings match using the following else if clause:
else if(!confirmPassword->GetData() || strcmp(password->GetData(),
confirmPassword->GetData()) != 0)
{
Demo Game Part 1 — Building the GUI | 319
Adding the Sinks
appPtr->errorFromLogin = false;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
Finally, if everything is okay, we would perform the actual signup on the server
in the else statement (this will be implemented in the next chapter), then we
show the Login dialog (we have already hidden the Signup dialog). This can be
seen in the following section of code:
else
{
// Perform the signup here...
appPtr->errorDialog->Hide();
if(appPtr->errorFromLogin)
appPtr->loginDialog->Show();
else
appPtr->signupDialog->Show();
}
Butterfly object and output that the button has been pressed to the console. This
can be seen here:
Butterfly* appPtr = (Butterfly *)awst;
printf("Chat Send Button Pressed\n");
fflush(stdout);
Next, we obtain the chat string the user has entered from the text box called
ChatInput and store it in a local variable we have defined called chatStr:
iString *chatStr;
appPtr->chatDialog->FindChild("ChatInput")->GetProperty("Text", (void **)
&chatStr);
Once we have the string entered by the user, we next validate it to ensure some-
thing was entered by checking that the GetData method call does not return a
null pointer and also that the string length is greater than zero. This can be seen
in the following if statement:
if(chatStr->GetData() && strlen(chatStr->GetData()) > 0)
{
If the user did enter a string, this would be sent to the server and distributed to
all players within range; however, since we are only constructing the GUI in this
chapter, we simply add the chat message to the chat area above.
To do this, we first create a pointer to an iAwsParmList using the
CreateParmList method of our aws object. This can be seen in the following line
of code:
csRef<iAwsParmList> params = appPtr->aws->CreateParmList();
To insert a new line into the multiline edit, we need to specify which row it
should be inserted at (which we always want to be the top), followed by the
string we wish to insert. Therefore, in the parameter list, we first add an integer
value to specify which row this should be inserted into. This can be seen in the
following line of code:
params->AddInt("row", 0);
Next, we add the actual chat string by placing the user’s username, a colon, and
the chat message that was entered. This is added to the parameter list with the
following line of code:
params->AddString("string", (*appPtr->username+": "+*chatStr).GetData());
Once the parameters are set up, we call the Execute method on the
iAwsComponent, i.e., the multiline edit, which is found by using the FindChild
method on the chatDialog. The Execute method is then passed the InsertRow
command, along with the parameter list. This can be seen here:
appPtr->chatDialog->FindChild("ChatArea")->Execute("InsertRow", params);
After this is called, you will notice that the chat area now contains the new chat
string, providing you entered at least one character into the chat input.
Demo Game Part 1 — Building the GUI | 321
Summary
Finally, once the string has been added to the chat area, we clear the chat
input box by setting its Text property to be an empty string. This can be seen in
the following two final lines of code:
csRef<iString> chatStr = csPtr<iString> (new scfString (""));
appPtr->chatDialog->FindChild("ChatInput")->SetProperty("Text", (void *) chatStr);
Summary
In this chapter, we created our GUI in the Qt Designer package, then imported it
successfully into our Crystal Space application. We also used the AWS sinks and
triggers to make the GUI interact with the user and link together correctly.
In the next tutorial, we will be building upon this by making the GUI interact
with the server to actually process signup and login attempts. Additionally, we
will be making the chat interface work so that players can communicate with
each other.
This page intentionally left blank.
Chapter 7
Introduction
In this second part of the demo game tutorial, we are going to integrate the
Object Management System into the code we created in Chapter 6, using the
GUI sinks to trigger events such as logging in and signing up to a new game.
We are first going to create another class called CNetworkHandler, which
will contain all the functionality of the OMS (i.e., it will extend the
COMSWrapper class) and it will also provide the additional network functional-
ity required by our demo game (such as logging in players, moving them about,
and sending chat messages).
public:
CNetworkHandler();
~CNetworkHandler();
323
324 | Chapter 7
Creating a Skeleton CNetworkHandler Class
As you can see, our CNetworkHandler class inherits the COMSWrapper class
and implements the standard methods from the COMSWrapper as we saw in
Chapter 5, “Integrating the OMS with an Existing Application.”
Also, in the header file for the CNetworkHandler class, we again define the
THINGITEM structure, which is used to represent things within our game. This
can be seen here again for reference.
typedef struct ThingItem
{
BNGUID guidThing;
csRef<iMeshWrapper> spThing;
#include "ivideo/graph2d.h"
#include "ivideo/natwin.h"
#include "ivideo/txtmgr.h"
#include "ivideo/fontserv.h"
#include "ivaria/conout.h"
#include "imesh/sprite2d.h"
#include "imesh/object.h"
#include "imap/parser.h"
#include "iengine/mesh.h"
#include "iengine/engine.h"
#include "iengine/sector.h"
#include "iengine/camera.h"
#include "iengine/movable.h"
#include "iengine/material.h"
Y
#include "imesh/thing/polygon.h"
#include "imesh/thing/thing.h"
#include
#include
#include
FL
"ivaria/reporter.h"
"igraphic/imageio.h"
"iutil/comp.h"
AM
#include "iutil/eventh.h"
#include "iutil/eventq.h"
#include "iutil/event.h"
#include "iutil/objreg.h"
TE
#include "iutil/csinput.h"
#include "iutil/virtclk.h"
#include "iutil/vfs.h"
#include "imesh/sprite3d.h"
#include "ClientCreates.h"
#include "CNetworkHandler.h"
CNetworkHandler::CNetworkHandler ()
{
}
CNetworkHandler::~CNetworkHandler ()
{
}
Team-Fly®
326 | Chapter 7
Creating a Skeleton CNetworkHandler Class
' http://www.butterflyguide.net
Instead of this, we are now going to create a new method in the CNetwork-
Handler class called DoLogin, into which we will pass the username and
password as collected by the GUI. This replacement else clause can be seen
here:
else
{
// attempt to log them into the game,
// NEW ->
networkHandler->DoLogin(appPtr->username->GetData(), password->GetData());
// <- NEW
}
Next, we need to add this new method to the class definition in CNetwork-
Handler as follows.
void DoLogin(char *username, char *password);
Now let’s look at the DoLogin function that we are going to create. The first step
is to create an instance of the OMS from within the OMSWrapper (which is
extended by our CNetworkHandler class). To do this, we call the CreateOMS
function of the OMSWrapper, passing in the game number, version, server IP,
328 | Chapter 7
Implementing the Login
port, and the local send and receive ports. This can be seen in the following few
lines of code:
void CNetworkHandler::DoLogin(char *username, char *password)
{
if(CreateOMS(7, 1, "wordware.butterfly.net", "9907", "8001", "8001"))
{
Ü Butterfly
Note Remember to change the specifics to match your own installation of the
Grid.
After this, we set the username, password, and avatar name using the methods
provided within the OMSWrapper class. Note how we use the string data passed
into the DoLogin method from the Login GUI.
SetUsername(username);
SetPassword(password);
SetAvatarName(username);
The final part of this method is to simply output a message to the console if the
OMS could not be initialized correctly. This can be seen in this final block of
code:
}
else
{
printf("Unable to Create OMS");
}
The next thing we should expect is a response to the login attempt. As we are
using the wrapper, this response will come through as an event, so we need to
ensure we are polling the events within our application. To do this we need to
add the following line of code to the beginning of the Butterfly::SetupFrame
method:
networkHandler->Update(100, 10); // NEW
Therefore, when a login pass or failed event occurs, it will be handled within the
COMSWrapper::HandleEvent method of the OMSWrapper (which is inherited
Demo Game Part 2 — Signup/Login | 329
Implementing the Login
by our CNetworkHandler class). If the login was successful, the following case
within the HandleEvent method will be executed:
case OMS_EVENT_LOGON_PASS:
TRACE0("Logged In Successfully as %s\n", GetUsername());
EventLogonPass();
break;
We then make the Chat dialog visible by calling a new GetChatDialog method to
first obtain a pointer to it. We have defined this new method within the Butterfly
class as follows:
csRef<iAwsWindow> GetChatDialog(void) { return chatDialog; }
We call the GetChatDialog method and from this, we can call the Show method
of our Chat dialog. This can be seen here:
butterfly->GetChatDialog()->Show();
After this, we place a simple welcome message into the newly visible chat area
using the following few lines of code:
csRef<iString> usernameStr = csPtr<iString> (new scfString (m_pcUsername));
csRef<iAwsParmList> params = butterfly->GetAWS()->CreateParmList();
params->AddInt("row", 0);
params->AddString("string", ("Welcome "+*usernameStr+" to the Butterfly Demo
Game!").GetData());
butterfly->GetChatDialog()->FindChild("ChatArea")->Execute("InsertRow", params);
butterfly->GetChatDialog()->Show();
butterfly->SetGameActive(true);
The opposite of a successful login is a failed login, so let’s handle that now. If
we look back to the HandleEvent method in the OMSWrapper, you will see that
the case for handling a failed login looks as follows:
case OMS_EVENT_LOGON_FAIL:
TRACE0("Logon Failed\n");
EventLogonFail();
break;
After this, we create the string for the Error dialog using the following line of
code:
csRef<iString> errStr = csPtr<iString> (new scfString ("Server rejected login!"));
Then we set that the error was from a login attempt, set the label, and finally
show the Error dialog. This can be seen in the following three lines of code:
butterfly->SetErrorFromLogin(true);
butterfly->GetErrorDialog()->Show();
butterfly->GetErrorDialog()->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
Note that the SetErrorFromLogin and GetErrorDialog methods within the But-
terfly class are also new. These methods are simply helper methods to allow
access to the private members of the Butterfly class and can be seen here:
void SetErrorFromLogin(bool state) { errorFromLogin = state; }
csRef<iAwsWindow> GetErrorDialog(void) { return errorDialog; }
butterfly->SetErrorFromLogin(true);
butterfly->GetErrorDialog()->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
butterfly->GetErrorDialog()->Show();
}
So, if we now attempt a login with invalid data, the Error dialog should appear
with the text “Server rejected login!” Then the Login dialog should appear when
the user clicks OK on the Error dialog (from the code we developed in the previ-
ous chapter). A screen shot of this in action can be seen in Figure 7-2.
332 | Chapter 7
Summary
Summary
In this chapter we started the integration of the OMS and validated the login of a
player as he attempted to connect to the server. In the next chapter we are going
to develop the innards of the game, such as displaying the world, displaying
players within the world, and giving the players the ability to send chat mes-
sages to each other.
Chapter 8
Introduction
In this final chapter, we will be completing our demo game example by adding
the ability to chat and move around within an enclosed room environment. So
let’s start by looking first at how we can add the chat feature to the application.
Later in the chapter we look at adding the world.
' http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html
For our Wordware account, we then use the following PSCP command from the
command prompt to retrieve the XML GCS called wordware.xml.
pscp wordware@wordware.butterfly.net:schema-1.6/wordware.xml wordware.xml
333
334 | Chapter 8
Adding Player Communication
Once transferred, the XML GCS file will be placed in whichever directory the
PSCP utility was executed from; in this case, it’s in the root of the C drive. Let’s
first take a look at the complete XML GCS that was created by default and then
look at each section of it in more detail to see how it all pieces together.
Listing 8.1: XML GCS
<?xml version='1.0'?>
<!--
<!-- The World. Everything is in the world, other than accounts -->
<world game="7" version="1">
<!-- Define the locale for the game. Only one locale in this -->
Demo Game Part 3 — The World | 335
Adding Player Communication
<locale number="0">
<boundary>
<origin>(-100.0,-100.0,0.0)</origin>
<normal>(1.0,0.0,0.0)</normal>
</boundary>
<boundary>
<origin>(-100.0,-100.0,0.0)</origin>
<normal>(0.0,1.0,0.0)</normal>
</boundary>
<boundary>
<origin>(100.0,100.0,0.0)</origin>
<normal>(-1.0,0.0,0.0)</normal>
Y
</boundary>
<boundary>
FL<origin>(100.0,100.0,0.0)</origin>
<normal>(0.0,-1.0,0.0)</normal>
</boundary>
AM
</locale>
<!-- Properties for the game. We'll keep it simple and only define -->
<!-- one property -->
<!-- Begin Identity/Avatar/Object record ========================== -->
TE
<!-- Defaults. If not defined, this will be put into place -->
<defaults type="1">
<set>
<prop name="Name" value="Please set me"></prop>
</set>
</defaults>
<defaults type="2">
<set>
<prop name="Name" value="Random Object"></prop>
</set>
</defaults>
<!-- OK. That defined our world. Now we'll define accounts and -->
<!-- identities -->
<!-- Begin Identity/Avatar/Object record ========================== -->
<!-- Identity -->
<identity number="1">
<Name value="wordware1"></Name>
<description value="wordware1"></description>
<account value="1"></account>
<avatar value="1"></avatar>
</identity>
<!-- Avatar -->
<avatar number="1">
Team-Fly®
336 | Chapter 8
Adding Player Communication
<identity number="4">
<Name value="wordwaredaemon"></Name>
<description value="wordwaredaemon"></description>
<account value="4"></account>
<avatar value="4"></avatar>
</identity>
<!-- Avatar -->
<avatar number="4">
<object type="1" GUID="104" Avatar="4">
<Position>(1,1,0)</Position>
<Orientation>(0,0,0)</Orientation>
<range>10</range>
<presence>0</presence>
<active value="true"></active>
<!-- Note no properties. Defaults should take -->
<!-- care of it -->
</object>
</avatar>
The first section of the GCS is simply the XML document type declaration fol-
lowed by a comment that states what the file is. This can be seen here:
<?xml version='1.0'?>
<!--
We then open a <Grid> tag for which there is an organization property, which in
our case we specify as "Wordware Publishing, Inc".
<Grid organization="Wordware Publishing, Inc">
Right after this, we declare three game accounts (which can be used by players
to log in) and one daemon account (which can be used by the server to perform
game related tasks). This is shown in the following code:
<account number ="1">
<name value="wordware1"></name>
<password value="BocFemp1"></password>
<permission game="7" version="1" value="3"></permission>
</account>
<account number ="2">
<name value="wordware2"></name>
<password value="BocFemp2"></password>
<permission game="7" version="1" value="3"></permission>
</account>
<account number ="3">
<name value="butterfly"></name>
<password value="ejmullI"></password>
<permission game="7" version="1" value="3"></permission>
</account>
<account number ="4">
<name value="wordwaredaemon"></name>
<password value="BocFempD"></password>
<permission game="7" version="1" value="7"></permission>
</account>
As you can see, we assign each account an account number within the
<account> tag, a name (which is used as the player’s username for logging in), a
password (again which is used to login), and a permission attribute for the game
and version that is defined in the next section.
After our user accounts are set up, the next thing that needs to be defined is
the actual game world. This is initiated by opening a <world> tag and specifying
the game and version number for the world we are creating (which is associated
with the previously declared user accounts):
<world game="7" version="1">
Since this tutorial game is very simple, we only need a single locale (area). The
locale is defined by a hyperplane (i.e., an origin and normal to the plane) that
represents its boundaries. This can be seen here:
<locale number="0">
<boundary>
<origin>(-100.0,-100.0,0.0)</origin>
<normal>(1.0,0.0,0.0)</normal>
</boundary>
<boundary>
<origin>(-100.0,-100.0,0.0)</origin>
<normal>(0.0,1.0,0.0)</normal>
</boundary>
Demo Game Part 3 — The World | 339
Adding Player Communication
<boundary>
<origin>(100.0,100.0,0.0)</origin>
<normal>(-1.0,0.0,0.0)</normal>
</boundary>
<boundary>
<origin>(100.0,100.0,0.0)</origin>
<normal>(0.0,-1.0,0.0)</normal>
</boundary>
</locale>
For more detailed information on creating worlds with multiple locales, please
refer to the ServerScriptingDatabaseCreation.pdf, which is available on the com-
panion CD.
After the locale is defined, we then define any properties (states) that our ava-
tars (or other game objects) will require within the game. The only property we
have defined here is a Name property, which we have assigned to number 258.
Note that the minimum for this number is 256, as the first 255 are reserved for
internal use within the server.
<Property number="258" type="clientset">
<Name value="Name"></Name>
<Type value="String"></Type>
<Allowed type="1"></Allowed>
<Allowed type="2"></Allowed>
</Property>
Note how we have set the name of the property’s Name value to "Name", and
we have made it of type "String". Also note how we have specified that types 1
and 2 are allowed to use this property. This is referring to types of objects within
our game; however, we will only really have one type of object (type 1) — our
players’ avatars.
Next, we declare default values for our Name property, dependent on the type
of object the property was assigned to. If it was assigned to an object of type 1
(i.e., an avatar), its default will be "Please set me", as shown in the following
code:
<defaults type="1">
<set>
<prop name="Name" value="Please set me"></prop>
</set>
</defaults>
Then we start to declare our identities and objects within the game. Note that an
identity simply links an avatar to a user account (as we saw above), referencing
their numbers respectively.
So we now create the first identity for the wordware1 user account as
follows:
<identity number="1">
<Name value="wordware1"></Name>
<description value="wordware1"></description>
<account value="1"></account>
<avatar value="1"></avatar>
</identity>
Note how we refer to account 1 and avatar 1. Account 1 was declared earlier in
this section, but we must now declare avatar 1. We do this by first opening an
<avatar> tag and assigning it the number 1:
<avatar number="1">
Within this, we then start an object definition, stating that it should be of type 1,
have a unique GUID of 100, and be related to avatar 1. Note that the GUID is
used within the client code to reference the object uniquely, along with the
object type to determine what sort of object it is:
<object type="1" GUID="100" Avatar="1">
Within the object definition we define all the default properties of the object, as
shown here:
<Position>(1,1,0)</Position>
<Orientation>(0,0,0)</Orientation>
<range>10</range>
<presence>0</presence>
<active value="true"></active>
<properties>
<prop name="Name" value="wordware1"></prop>
</properties>
Note that the position, orientation, range, presence, and active properties are
standard to all objects within the Butterfly Grid. After these are defined, we
define our custom properties within the <properties> tag, using the name of the
custom properties to set them. We will be adding more custom properties to
objects later in this chapter, but for now we will just be using the Name property.
We repeat this process for the other two user accounts (wordware2 and but-
terfly), creating an identity and avatar for each and ensuring they have a unique
GUID.
Finally, we declare three other objects within the world of type 2 as follows:
<object type="2" GUID="105">
<Position>(10,10,0)</Position>
<Orientation>(0,0,0)</Orientation>
<range>10</range>
<presence>1</presence>
<active value="true"></active>
</object>
<object type="2" GUID="106">
Demo Game Part 3 — The World | 341
Adding Player Communication
<Position>(10,15,0)</Position>
<Orientation>(0,0,0)</Orientation>
<range>10</range>
<presence>1</presence>
<active value="true"></active>
</object>
<object type="2" GUID="107">
<Position>(10,20,0)</Position>
<Orientation>(0,0,0)</Orientation>
<range>10</range>
<presence>1</presence>
<active value="true"></active>
</object>
Note that we won’t actually be referencing these objects, but it shows how we
can add objects of other types to the game environment.
The only thing left to do once all the objects are declared within the world is
to close the <world> and <Grid> tags as follows:
</world>
</Grid>
Once any changes have been made to the XML GCS, we need to transfer it back
to the Butterfly server. This is done with the PSCP utility we downloaded ear-
lier. Assuming we are in the same directory as the saved XML GCS, we can use
something similar to the following command to transfer it back to the server:
pscp wordware.xml wordware@wordware.butterfly.net:schema-1.6/wordware.xml
Again, the underlined section of the command should reflect your own Butter-
fly.net account details. Upon execution, the PSCP utility will ask you for the
Butterfly.net account password. Once this is entered, you will see the following:
Figure 8-2: Using PSCP to transfer the XML GCS back to the server
Once transferred back, we can use the XML GCS to create our database schema
for our game. To do this, log in to your Butterfly.net account using SSH (with
Putty or another SSH client). If you change to the schema-1.6 directory, you
should see something similar to the following in your own account:
342 | Chapter 8
Adding Player Communication
it will recreate our game database with the new XML GCS we created and
uploaded to the server. Sample output of this can be seen in the following screen
shot.
Now that our new schema is created, we need to restart the server in order for
our changes to have any effect. To do this, first change the current directory to
usr/local/bin. If you execute the ls command here, you should see something
similar to the following:
As you can see, there is a wordware-restart command here, which we can use to
restart the server and hence activate our database schema changes. To do this,
we execute the following command from the /usr/local/bin directory:
./wordware-restart
Before we move on to implementing the chat, let’s first test that we have in fact
made changes to the database, since we haven’t really seen anything different as
such. So let’s test this by adding another game account to the XML GCS. This
new account will have the username wordware3 and a password of book.
First, in the XML, after the definition of account 4, let’s add the following:
<account number ="5">
<name value="wordware3"></name>
<password value="book"></password>
<permission game="7" version="1" value="3"></permission>
</account>
Then, for this account, we need to create an identity, so after the definition of
avatar 4, add in the following identity for the new player:
<identity number="5">
<Name value="wordware3"></Name>
<description value="wordware3"></description>
<account value="5"></account>
<avatar value="5"></avatar>
</identity>
Then finally, we want to create an avatar for this new player as follows:
<avatar number="5">
<object type="1" GUID="105" Avatar="5">
<Position>(1,1,0)</Position>
<Orientation>(0,0,0)</Orientation>
<range>10</range>
<presence>0</presence>
<active value="true"></active>
<properties>
<prop name="Name" value="wordware3"></prop>
</properties>
344 | Chapter 8
Adding Player Communication
</object>
</avatar>
Now save the changes to the XML GCS, transfer it to the server, and create the
new schema with the create-schema command we saw before. However, do not
restart the server just yet.
Try logging into the new account with the tutorial application we created in
the previous chapter, using the username wordware3 and the password book.
You should get the following screen when you click the Login -> button:
Y
FL
AM
TE
Now, remembering that the first 255 types are reserved by Butterfly, the value of
BUTTERFLY_SUBTYPES_MAX is therefore 255, meaning the enumerated
value of BN_ATTRIB_NAME will be 258, which if you remember back to our
XML GCS, relates to the Name property that we defined as follows:
<Property number="258" type="clientset">
<Name value="Name"></Name>
<Type value="String"></Type>
<Allowed type="1"></Allowed>
<Allowed type="2"></Allowed>
</Property>
Team-Fly®
346 | Chapter 8
Implementing the Chat
We know that the enumeration maps to the number of our property in the data-
base, but we also need to tell the client code information about the property, i.e.,
what type it is and how it should be handled.
As we are only interested in this property for our players, we add a pointer to
a CStringAttrib called pName in our CAvatar class that is defined in our
ClientObject.h header file. This gives us the new class definition as follows:
class CAvatar : public CLiving
{
public:
CAvatar(BNGUID guidObject, bool bIsClientControlled, BNGUID
guidParentObject = GUID_INVALID, bool bPreventClobber = false);
virtual ~CAvatar();
Next, in ClientObject.cpp, we have to add the handling for our new string attrib-
ute within the constructor and destructor of our CAvatar class. In the constructor,
we make a call to the NewStringType method (which is a member of the CThing
class that our CAvatar class inherits). Into this, we first pass in the ID number of
the state (property) that this will relate to; hence, we pass in our
BN_ATTRIB_NAME enumeration which relates to our Name property in the
XML GCS. Then we specify that our game client will set the value of it and that
its maximum length should be 32 characters. This can be seen in the following
code segment:
pName = NewStringType(BN_ATTRIB_NAME, TYPE_CLIENT_SET, 32);
if ( pName )
pName->SetAutoProcess(true);
To summarize, our new constructor and destructor for the CAvatar class now
look as follows.
CAvatar::CAvatar(BNGUID guidObject, bool bIsClientControlled, BNGUID
guidParentObject /*= GUID_INVALID*/, bool bPreventClobber /*= false*/)
: CLiving(BN_THING_AVATAR, guidObject, bIsClientControlled,
guidParentObject, bPreventClobber)
{
pIdentity = NewEnumType( BN_ATTRIB_IDENTITY, BN_ATTRIB_IDENTITY,
BNAttribIdentityVALUES, sizeof(BNAttribIdentityVALUES) / sizeof(INT) );
if ( pIdentity )
pIdentity->SetAutoProcess(true);
CAvatar::~CAvatar()
{
DeleteType( BN_ATTRIB_IDENTITY, BN_ATTRIB_IDENTITY, pIdentity );
DeleteType( BN_ATTRIB_NAME, TYPE_CLIENT_SET, pName );
}
This updates the attribute name string for non-client controlled objects (i.e.,
other players in the world). Then we need to add in the case for the client con-
trolled object as follows:
case BN_ATTRIB_NAME:
vAttributes[uAttrib].m_Attribute.Value.String.pcData[vAttributes
[uAttrib].m_Attribute.Value.String.iLength] = 0;
printf("\n\nGot name as: %s\n\n", vAttributes
[uAttrib].m_Attribute.Value.String.pcData);
break;
Once this is done, our updated UpdateThing method should look as follows:
bool CNetworkHandler::UpdateThing(BNGUID guidThing, bool bClientControlled)
{
bool bRetVal = false;
static std::vector<CThingAttributeValue> vAttributes;
UINT uAttrib;
char *pcTemp = NULL;
if ( !m_pOMS )
return false;
UpdateThingView(guidThing, bClientControlled);
case BUTTERFLY_POSITION:
bRetVal = SetThingPosition(guidThing,
vAttributes[uAttrib].m_Attribute.Value.vVector.x,
vAttributes[uAttrib].m_Attribute.Value.vVector.y,
vAttributes[uAttrib].m_Attribute.Value.vVector.z);
break;
case BUTTERFLY_ORIENTATION:
bRetVal = SetThingOrientation(guidThing,
vAttributes[uAttrib].m_Attribute.Value.vVector.x,
vAttributes[uAttrib].m_Attribute.Value.vVector.y,
vAttributes[uAttrib].m_Attribute.Value.vVector.z);
break;
case BUTTERFLY_VELOCITY:
bRetVal = SetThingVelocity(guidThing,
vAttributes[uAttrib].m_Attribute.Value.vVector.x,
vAttributes[uAttrib].m_Attribute.Value.vVector.y,
vAttributes[uAttrib].m_Attribute.Value.vVector.z);
break;
case BN_ATTRIB_NAME:
printf("\n\nNon Client name change");
if ((vAttributes[uAttrib].m_Attribute.Type ==
PROPERTY_STRING) && vAttributes
[uAttrib].m_Attribute.Value.String.pcData)
{
vAttributes[uAttrib].m_Attribute.Value.String.
pcData[vAttributes[uAttrib].m_Attribute.
Value.String.iLength] = 0;
}
break;
default:
DisplayOtherState(guidThing, &(vAttributes
[uAttrib]));
break;
}
if ( vAttributes[uAttrib].m_Attribute.Value.Blob.pvData !=
NULL )
delete [] (UBYTE *)vAttributes[uAttrib].m_Attribute.
Value.Blob.pvData;
vAttributes[uAttrib].m_Attribute.Value.Blob.pvData = NULL;
vAttributes[uAttrib].m_Attribute.Value.Blob.iLength = 0;
vAttributes[uAttrib].m_Attribute.Type = PROPERTY_NULL;
}
}
}
else
{
// Try the new GetStatesByGUID function
m_pOMS->GetStatesByGUID(guidThing, vAttributes, false);
case BN_ATTRIB_NAME:
vAttributes[uAttrib].m_Attribute.Value.String.pcData
vAttributes[uAttrib].m_Attribute.Value.String.
iLength] = 0;
printf("\n\nGot name as: %s\n\n", vAttributes
[uAttrib].m_Attribute.Value.String.pcData);
break;
default:
DisplayOtherState(guidThing, &(vAttributes
[uAttrib]));
break;
}
delete [] vAttributes[uAttrib].m_Attribute.
Value.String.pcData;
vAttributes[uAttrib].m_Attribute.Value.String.pcData =
NULL;
vAttributes[uAttrib].m_Attribute.Value.String.iLength = 0;
vAttributes[uAttrib].m_Attribute.Type = PROPERTY_NULL;
}
return bRetVal;
}
Once a player has logged in and his avatar has been embodied, a call is made to
the CNetworkHandler::EventAvatarNew method. Within this method, we want
to set the Name property of the player’s avatar in the database so other players
can query the player’s name and display it in their chat window when any of the
players send a chat message.
To set the Name property, we first check that the GUID of the new player is
in fact the correct GUID for that avatar using the following lines of code:
void CNetworkHandler::EventAvatarNew(BNGUID guidThing, BNOBJECTTYPE typeThing)
{
// if this player...
if(guidThing == GetOMS()->GetGUIDAvatar())
{
We then output to the console that the name is being set, and we attempt to set
the name using a call to the SetStateByGUID method:
printf("\nSetting name as: %s\n", m_pcUsername);
int res;
if(BN_SUCCESS(res = GetOMS()->SetStateByGUID(GetOMS()->GetGUIDAvatar(),
BN_ATTRIB_NAME, m_pcUsername, strlen(m_pcUsername))))
{
printf("\nAvatar Name Set to %s.\n", m_pcUsername);
}
else
printf("\nFailed to set Avatar Name\n");
Demo Game Part 3 — The World | 351
Implementing the Chat
Notice that into this method we pass in the GUID of the player’s avatar, the ID
of the state/attribute we wish to set (i.e., BN_ATTRIB_NAME), the value for
the attribute, which in this case is the player’s username, and the length of the
string we are setting, which is obtained by a simple call to the strlen method.
Next, we make a call to MessageFind, passing in the player’s GUID and
username:
GetOMS()->MessageFind(GetOMS()->GetGUIDAvatar(), m_pcUsername);
This transparently retrieves the public key for the player that is used in the distri-
bution of messages from the server.
When another player logs in to the server, we want to display this in the chat
window. So within the CNetworkHandler::EventThingNew method, we first
check that this new thing is an avatar by checking if the type of the object is
equal to 1. This can be seen in the following segment of code:
void CNetworkHandler::EventThingNew(BNGUID guidThing, BNOBJECTTYPE typeThing)
{
printf("\n\nNew THING created: GUID = %i\n\n", guidThing);
if(typeThing == BN_THING_AVATAR)
{
Therefore, if the new thing was an avatar, we can obtain the name of the player
by retrieving the BN_ATTRIB_NAME state, using the GetStateByGUID
method as seen in the following code segment:
unsigned int uLength = 0;
char *usernameData = new char[OMS_MAX_STRING_LEN];
if (BN_SUCCESS(GetOMS()->GetStateByGUID(guidThing, BN_ATTRIB_NAME, &usernameData,
uLength)))
{
If we successfully retrieved the name of the player, we can then append a short
message to the chat area using the following few lines of code:
usernameData[uLength] = 0;
csRef<iString> usernameStr = csPtr<iString> (new scfString (usernameData));
csRef<iAwsParmList> params = butterfly->GetAWS()->CreateParmList();
params->AddInt("row", 0);
params->AddString("string", ("[Player "+*usernameStr+" has joined the
game]").GetData());
butterfly->GetChatDialog()->FindChild("ChatArea")->Execute("InsertRow", params);
We then ping the new player using the MessageFind method, passing in the cur-
rent player’s GUID and the new player’s username we just retrieved. This
352 | Chapter 8
Implementing the Chat
ensures that the new player will receive all the messages from the player calling
the MessageFind method. The call to this can be seen here:
GetOMS()->MessageFind(GetOMS()->GetGUIDAvatar(), usernameData);
We also want to handle when a player logs out. This is done in the
CNetworkHandler::EventThingGone method, similar to when a player logs in.
The updated EventThingGone method can be seen in full here:
void CNetworkHandler::EventThingGone(BNGUID guidThing, BNOBJECTTYPE typeThing,
bool bClientControlled)
{
if(typeThing == BN_THING_AVATAR)
{
printf("A player has logged out...");
unsigned int uLength = 0;
char *usernameData = new char[OMS_MAX_STRING_LEN];
if (BN_SUCCESS(GetOMS()->GetStateByGUID(guidThing, BN_ATTRIB_NAME,
&usernameData, uLength)))
{
usernameData[uLength] = 0;
csRef<iString> usernameStr = csPtr<iString> (new scfString
(usernameData));
csRef<iAwsParmList> params = butterfly->GetAWS()->
CreateParmList();
params->AddInt("row", 0);
params->AddString("string", ("[Player "+*usernameStr+" has left
the game]").GetData());
butterfly->GetChatDialog()->FindChild("ChatArea")->
Execute("InsertRow", params);
// Ping this other user...
GetOMS()->MessageFind(GetOMS()->GetGUIDAvatar(), usernameData);
}
else
printf("Failed to get left avatar name!\n");
}
RemoveThing(guidThing);
}
params->AddString("string", tempMessageStr);
butterfly->GetChatDialog()->FindChild("ChatArea")->Execute("InsertRow",
params);
}
Once the player is logged in, clicking the Send button on the chat interface trig-
gers a call to Butterfly::SendChatMessage as we have seen previously. Now we
need to fill out this method so it actually does what it says.
In this method we first retrieve what the user has input by calling the
GetProperty method of the ChatInput child of the chatDialog object and store it
in the iString reference chatMessage. This can be seen here:
iString *chatMessage;
appPtr->chatDialog->FindChild("ChatInput")->GetProperty("Text", (void **)
&chatMessage);
Next, we create a char array to hold each user’s username as we cycle through
the online list of players and we also declare an unsigned integer to record the
length of the username as we retrieve each one, as shown below:
char *pcUsername = new char[OMS_MAX_STRING_LEN];
unsigned int uLength = 0;
Once our variables are declared, we retrieve a list of the GUIDs for the players
who are currently online by means of a call to the GetGuidList method, which is
a member of the COMS class, passing in the vector object vGUIDList we just
declared. This can be seen in the following line of code:
networkHandler->GetOMS()->GetGuidList(vGUIDList);
We can then iterate through the vector of GUIDs that we have just retrieved
from the OMS with the following segment of code:
for (unsigned int uIndex = 0; uIndex < vGUIDList.size(); uIndex++)
{
Then for the current GUID in the vector, we attempt to retrieve the player’s
username with a call to the GetStateByGUID method of the OMS, passing in the
current GUID (i.e., vGUIDList[uIndex].guid), the BN_ATTRIB_NAME (so the
OMS knows which property ID to retrieve), a pointer to pcUsername to store it,
and our unsigned integer uLength to hold the length of the returned username.
This can all be seen here:
if(BN_SUCCESS(networkHandler->GetOMS()->GetStateByGUID(vGUIDList[uIndex].guid,
BN_ATTRIB_NAME, &pcUsername, uLength)))
{
After this we can then attempt to send the chat message to the currently retrieved
user using the MessageSend method of the COMS class. Into this we pass in the
retrieved username, what the message is (i.e., a chat message), the name of the
354 | Chapter 8
Implementing the Chat
player who sent the message (retrieved easily with the GetAvatarName method),
the message string, and finally the length of the message string. This can be seen
here for reference:
if(BN_FAILURE(networkHandler->GetOMS()->MessageSend(pcUsername,
BN_MESSAGE_TYPE_TEXT_CHAT, networkHandler->GetAvatarName(),
chatMessage->GetData(), strlen(chatMessage->GetData()))))
{
Notice here that we check whether the message sending failed. The only way
this would really occur is if the user’s public key could not be found, so we can
retrieve this by means of the MessageFind method, to which we pass in our own
GUID, followed by the username of the user’s public key we wish to retrieve.
This can be seen here:
networkHandler->GetOMS()->MessageFind(networkHandler->GetOMS()->GetGUIDAvatar(),
pcUsername);
Then finally, we blank out the chat input box with the following two lines of
code:
csRef<iString> emptyStr = csPtr<iString> (new scfString (""));
appPtr->chatDialog->FindChild("ChatInput")->SetProperty("Text", emptyStr);
iString *chatMessage;
appPtr->chatDialog->FindChild("ChatInput")->GetProperty("Text", (void **)
&chatMessage);
std::vector<BNGUIDLIST> vGUIDList;
if(pcUsername)
{
networkHandler->GetOMS()->GetGuidList(vGUIDList);
if(BN_FAILURE(networkHandler->GetOMS()->
MessageSend(pcUsername, BN_MESSAGE_TYPE_TEXT_CHAT,
networkHandler->GetAvatarName(), chatMessage->
GetData(), strlen(chatMessage->GetData()))))
{
// Ping the user so the next message goes through
networkHandler->GetOMS()->MessageFind
(networkHandler->GetOMS()->GetGUIDAvatar(),
pcUsername);
printf("Pinging User.\n");
}
}
}
delete[] pcUsername;
Y
}
}
FL
csRef<iString> emptyStr = csPtr<iString> (new scfString (""));
appPtr->chatDialog->FindChild("ChatInput")->SetProperty("Text", emptyStr);
AM
implement the actual world in which the avatars will move around.
For this final section, we are going to merge our demo game so far, our
demoskyOMS example, and our butterfly3d3 application we created previously.
First, we are going to merge our butterfly3d3 third-person style mini-engine into
our game tutorial code.
Let’s now look at what we have to add into the header file. The first addition
is the extra interfaces we require for the world, which can be seen in the follow-
ing few lines of code:
struct iFont; // ADDED
struct iSector; // ADDED
struct iSprite3DState; // ADDED
struct iMeshWrapper; // ADDED
struct iCollideSystem; // ADDED
struct iCollider; // ADDED
Here we have the font, sector, 3d sprite, mesh, and collision detection interfaces.
Next, inside our Butterfly class, we need to add references to our room (i.e.,
our 3D world) and the collision detection system (which we named cdsys):
csRef<iSector> room; // ADDED
csRef<iCollideSystem> cdsys; // ADDED
We then add references to a font interface as well as variables to hold our avatar
sprite:
csRef<iFont> font; // ADDED
csRef<iMeshWrapper> player; // ADDED
csRef<iSprite3DState> playerstate; // ADDED
Team-Fly®
356 | Chapter 8
Adding the World
Then we add in the references for the collision detection and the mesh wrapper
reference for the walls of the world as follows:
iCollider* playerCollider; // NEW
iCollider* mapCollider; // NEW
csRef<iMeshWrapper> walls; // NEW
After this we need to declare another two private methods that will be used to
handle the chat input focus. This can be seen here:
static void TextBoxClicked(void* awst, iAwsSource *); // ADDED
static void TextBoxLostFocus(void* awst, iAwsSource *); // ADDED
The last private member is an additional Boolean to determine whether the ava-
tar should be accepting keyboard input or not (i.e., it should not accept input if
the player is currently typing text in the chat window). This can be seen here:
static bool acceptKeyboardInput; // ADDED for movement / chat window
The only further addition to the header is another public method for setting up
the collision detection, which should be added as follows:
iCollider* CreateCollider (iMeshWrapper* mesh); // ADDED
Now that we have modified the header, we need to make changes to the imple-
mentation of the class in demogame.cpp. Starting at the top of the file, we first
need to add the following includes that are required by our mini 3D world
engine.
#include "cstool/collider.h" // ADDED
#include "imesh/sprite3d.h" // ADDED
#include "ivaria/collider.h" // ADDED
#include "igeom/polymesh.h" // ADDED
After this, we then add the following two static Boolean declarations.
bool Butterfly::active; // ADDED
bool Butterfly::acceptKeyboardInput; // ADDED
In the constructor we then reset the active variable to false, signifying that the
game engine is inactive (i.e., the player is not logged in) and we also default the
acceptKeyboardInput variable to true, as the player will not initially be focused
within the chat window after he logs in. The new constructor can be seen here:
Butterfly::Butterfly (iObjectRegistry* object_reg)
{
Butterfly::object_reg = object_reg;
active = false;
acceptKeyboardInput = true;
}
Next, within the SetupFrame method of the Butterfly class, we add the following
section of code below the call to networkHandler->Update:
if(active)
{
// ADDED ->
// Store the current transformations (before any movement, etc.)
csReversibleTransform oldPlayerTrans = player->GetMovable()->GetTransform();
Demo Game Part 3 — The World | 357
Adding the World
// Check input...
// Get elapsed time from the virtual clock.
csTicks elapsed_time = vc->GetElapsedTicks ();
iCamera* c = view->GetCamera();
if(acceptKeyboardInput)
{
if (kbd->GetKeyState (CSKEY_PGUP))
c->Move(csVector3(0, 1, 0) * 4 * speed);
if (kbd->GetKeyState (CSKEY_PGDN))
c->Move(csVector3(0, 1, 0) * 4 * speed * -1);
if (kbd->GetKeyState (CSKEY_UP))
c->Move (CS_VEC_FORWARD * 4 * speed);
if (kbd->GetKeyState (CSKEY_DOWN))
c->Move (CS_VEC_BACKWARD * 4 * speed);
// Player movement...
if(kbd->GetKeyState ('w'))
{
if(isWalking == false)
playerstate->SetAction("run");
isWalking = true;
player->GetMovable()->SetPosition(player->GetMovable()->
GetTransform().This2Other(csVector3(1,0,0) * speed
* 25));
player->GetMovable()->UpdateMove();
// NEW ->
// check for player/wall collisions...
cdsys->ResetCollisionPairs ();
csReversibleTransform ft1 = player->GetMovable ()->
GetFullTransform ();
csReversibleTransform ft2 = walls->GetMovable ()->
GetFullTransform ();
bool collision = cdsys->Collide(playerCollider, &ft1,
mapCollider, &ft2);
if (collision)
{
// Restore old transforms.
player->GetMovable ()->SetTransform (oldPlayerTrans);
player->GetMovable ()->UpdateMove ();
}
// <- NEW
}
else
{
if(isWalking == true)
playerstate->SetAction("stand");
358 | Chapter 8
Adding the World
isWalking = false;
}
if(kbd->GetKeyState ('a'))
{
player->GetMovable ()->GetTransform ().RotateThis
(csVector3(0, -1, 0), speed * 4);
player->GetMovable()->UpdateMove();
}
if(kbd->GetKeyState ('d'))
{
player->GetMovable ()->GetTransform ().RotateThis
(csVector3(0, 1, 0), speed * 4);
player->GetMovable()->UpdateMove();
}
} // end of 'accept keyboard input'
Notice in the preceding code how we only perform this if the active variable is
true and also how we only accept input for the player if the acceptKeyboard-
Input variable is true.
After the world rendering, we make a call to the AWS redraw and print meth-
ods as follows so we render the GUI on a layer above the 3D rendering:
aws->Redraw();
aws->Print (g3d, 64);
Next, in the Initialize method, we need to load the additional collision detection
plug-in as shown here:
CS_REQUEST_PLUGIN("crystalspace.collisiondetection.rapid", iCollideSystem),
// ADDED
iMaterialWrapper* tm =
engine->GetMaterialList ()->FindByName ("butterflytexture");
iPolygon3D* p;
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-20, 0, 20));
p->CreateVertex (csVector3 (20, 0, 20));
p->CreateVertex (csVector3 (20, 0, -20));
p->CreateVertex (csVector3 (-20, 0, -20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-20, 20, -20));
p->CreateVertex (csVector3 (20, 20, -20));
p->CreateVertex (csVector3 (20, 20, 20));
p->CreateVertex (csVector3 (-20, 20, 20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-20, 20, 20));
p->CreateVertex (csVector3 (20, 20, 20));
p->CreateVertex (csVector3 (20, 0, 20));
p->CreateVertex (csVector3 (-20, 0, 20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (20, 20, 20));
p->CreateVertex (csVector3 (20, 20, -20));
p->CreateVertex (csVector3 (20, 0, -20));
p->CreateVertex (csVector3 (20, 0, 20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-20, 20, -20));
p->CreateVertex (csVector3 (-20, 20, 20));
p->CreateVertex (csVector3 (-20, 0, 20));
p->CreateVertex (csVector3 (-20, 0, -20));
360 | Chapter 8
Adding the World
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (20, 20, -20));
p->CreateVertex (csVector3 (-20, 20, -20));
p->CreateVertex (csVector3 (-20, 0, -20));
p->CreateVertex (csVector3 (20, 0, -20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
csRef<iStatLight> light;
iLightList* ll = room->GetLights ();
light = engine->CreateLight (NULL, csVector3 (-10, 7, 0), 20, csColor (1, 1, 1),
false);
ll->Add (light->QueryLight ());
light = engine->CreateLight (NULL, csVector3 (10, 7, 0), 20, csColor (1, 1, 1),
false);
ll->Add (light->QueryLight ());
light = engine->CreateLight (NULL, csVector3 (0, 7, 10), 20, csColor (1, 1, 1),
false);
ll->Add (light->QueryLight ());
light = engine->CreateLight (NULL, csVector3 (0, 7, -10), 20, csColor (1, 1, 1),
false);
ll->Add (light->QueryLight ());
iTextureWrapper* txt = loader->LoadTexture ("skin", "/lib/marine/brownie.png",
CS_TEXTURE_3D, txtmgr, true);
if (txt == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly", "Error loading texture!");
return false;
}
csMatrix3 m;
m.Identity ();
m *= 5.0;
Demo Game Part 3 — The World | 361
Adding the World
playerCollider = CreateCollider(player);
if (playerCollider == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Error creating playerCollider!");
return false;
}
mapCollider = CreateCollider(walls);
if (mapCollider == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Error creating mapCollider!");
return false;
}
engine->Prepare ();
After our update to the Initialize method, we need to add in the implementation
for the CreateCollider method as follows:
iCollider* Butterfly::CreateCollider(iMeshWrapper* mesh)
{
csRef<iPolygonMesh> polmesh (SCF_QUERY_INTERFACE (mesh->GetMeshObject (),
iPolygonMesh));
if (polmesh)
{
csColliderWrapper* wrap = new csColliderWrapper
(mesh->QueryObject (), cdsys, polmesh);
wrap->DecRef ();
return wrap->GetCollider ();
}
else
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.simpcd",
"Object doesn't support collision detection!");
return NULL;
}
}
362 | Chapter 8
Adding the World
Finally, we need to add the implementations for our two new methods for han-
dling the chat input focus and loss of focus. These are implemented as follows:
// ADDED ->
void Butterfly::TextBoxClicked(void* awst, iAwsSource *)
{
// stop movement while in the chat input
acceptKeyboardInput = false;
}
As you can see, all these methods do is update the state of our acceptKey-
boardInput variable, which is used in the Update method to determine if the
avatar should accept input from the user or not.
Note that in order to get these additional methods to work, we added the fol-
lowing two lines in the Initialize method:
sink->RegisterTrigger ("TextBoxClicked", &TextBoxClicked); // ADDED
sink->RegisterTrigger ("TextBoxLostFocus", &TextBoxLostFocus); // ADDED
And we also updated the demogame.def file in the data/temp directory with the
following small section.
connect
{
signalClicked -> chatSink::TextBoxClicked
signalTextBoxLostFocus -> chatSink::TextBoxLostFocus
}
For your reference, let’s now look at the complete demogame.def, demo-
game.cpp, and demogame.h files (all other files remain the same as before).
Listing 8-2: demogame.def
skin "Normal Windows"
{
Texture: "/aws/texture.png"
HighlightColor: 230,230,230
ShadowColor: 60,60,60
FillColor: 200,200,200
TextDisabledColor: 128,128,0
TextForeColor: 0,0,0
TextBackColor: 255,255,255
ButtonTextColor: 0,0,192
OverlayTextureAlpha: 128
ScrollBarHeight: 16
ScrollBarWidth: 16
WindowMin: "/aws/minimize.png"
WindowZoom: "/aws/zoom.png"
WindowClose: "/aws/close.png"
WindowMinAt: (46, 6) - (46-11, 6+10)
WindowZoomAt: (34, 6) - (34-11, 6+10)
Demo Game Part 3 — The World | 363
Adding the World
window "ErrorDialog"
{
Style: wfsNormal
Options: wfoGrip+wfoTitle+wfoControl
Frame: (0,0) - (345,148)
Title: "Error"
component "ErrorOk" is "Command Button"
{
Frame: (120,80) - (210,110)
Caption: "Ok"
connect
{
signalClicked -> errorSink::ErrorOk
}
}
component "ErrorLabel" is "Label"
{
Frame: (10,20) - (330,70)
Caption: "ErrorLabel"
}
}
window "LoginDialog"
{
Style: wfsNormal
Options: wfoGrip+wfoTitle+wfoControl
Frame: (0,0) - (458,229)
Title: "Login"
component "Logo" is "Image View"
{
364 | Chapter 8
Adding the World
window "SignupDialog"
{
Style: wfsNormal
Options: wfoGrip+wfoTitle+wfoControl
Frame: (0,0) - (369,405)
Title: "Signup"
component "textLabel1" is "Label"
{
Frame: (30,30) - (150,50)
Caption: "Fullname..."
Demo Game Part 3 — The World | 365
Adding the World
}
component "textLabel1_2" is "Label"
{
Frame: (30,90) - (150,110)
Caption: "Email Address..."
}
component "textLabel1_2_2" is "Label"
{
Frame: (30,150) - (150,170)
Caption: "Username..."
}
component "textLabel1_2_2_2" is "Label"
{
Frame: (30,200) - (150,220)
Y
Caption: "Password..."
}
{ FL
component "textLabel1_2_2_2_2" is "Label"
Team-Fly®
366 | Chapter 8
Adding the World
{
Frame: (220,330) - (331,371)
Caption: "Signup ->"
connect
{
signalClicked -> signupSink::DoSignup
}
}
}
window "ChatDialog"
{
Style: wfsNormal
Options: wfoGrip+wfoTitle+wfoControl
Frame: (0,0) - (575,160)
Title: "Chat"
component "ChatArea" is "Multiline Edit"
{
Frame: (10,10) - (550,100)
Style: fsSunken
}
component "ChatInput" is "Text Box"
{
Frame: (10,110) - (420,130)
connect
{
signalClicked -> chatSink::TextBoxClicked
signalTextBoxLostFocus -> chatSink::TextBoxLostFocus
}
}
component "ChatSend" is "Command Button"
{
Frame: (430,110) - (561,130)
Caption: "Send ->"
connect
{
signalClicked -> chatSink::Send
}
}
}
#include <stdarg.h>
#include "csutil/ref.h"
struct iObjectRegistry;
struct iEngine;
struct iLoader;
struct iGraphics2D;
struct iGraphics3D;
struct iKeyboardDriver;
Demo Game Part 3 — The World | 367
Adding the World
struct iVirtualClock;
struct iEvent;
struct iView;
struct iTextureManager;
struct iString;
struct iFont; // ADDED
struct iSector; // ADDED
struct iSprite3DState; // ADDED
struct iMeshWrapper; // ADDED
struct iCollideSystem; // ADDED
struct iCollider; // ADDED
class Butterfly
{
private:
iObjectRegistry* object_reg;
csRef<iEngine> engine;
csRef<iLoader> loader;
csRef<iGraphics2D> g2d;
csRef<iGraphics3D> g3d;
csRef<iKeyboardDriver> kbd;
csRef<iVirtualClock> vc;
csRef<iView> view;
csRef<iTextureManager> txtmgr;
csRef<iAws> aws;
csRef<iAwsCanvas> awsCanvas;
csRef<iSector> room; // ADDED
csRef<iCollideSystem> cdsys; // ADDED
iString *username;
csRef<iAwsWindow> loginDialog;
csRef<iAwsWindow> signupDialog;
csRef<iAwsWindow> errorDialog;
csRef<iAwsWindow> chatDialog;
public:
Butterfly (iObjectRegistry* object_reg);
~Butterfly ();
#endif // __BUTTERFLY_H__
#include "cssysdef.h"
#include "cssys/sysfunc.h"
#include "iutil/vfs.h"
#include "csutil/cscolor.h"
#include "cstool/csview.h"
#include "cstool/initapp.h"
#include "cstool/cspixmap.h"
#include "iaws/aws.h"
#include "iaws/awscnvs.h"
#include "iaws/awsparm.h"
Demo Game Part 3 — The World | 369
Adding the World
#include "grid-oms/OMSWrapper.h"
#include <list>
#include "../butterfly-grid/grid-common/thing/thing_types.h"
#include "../butterfly-grid/grid-common/butterfly_types.h"
#include "CNetworkHandler.h"
#include "ClientObjectDefines.h"
CS_IMPLEMENT_APPLICATION
// Application Specific...
csRef<iFont> font;
bool Butterfly::active; // ADDED
bool Butterfly::acceptKeyboardInput; // ADDED
// [END] Application Specific
370 | Chapter 8
Adding the World
Butterfly::~Butterfly ()
{
}
void Butterfly::SetupFrame ()
{
networkHandler->Update(100, 10); // NEW
if(active)
{
// ADDED ->
// Check input...
// Get elapsed time from the virtual clock.
csTicks elapsed_time = vc->GetElapsedTicks ();
iCamera* c = view->GetCamera();
if(acceptKeyboardInput)
{
if (kbd->GetKeyState (CSKEY_PGUP))
c->Move(csVector3(0, 1, 0) * 4 * speed);
if (kbd->GetKeyState (CSKEY_PGDN))
c->Move(csVector3(0, 1, 0) * 4 * speed * -1);
if (kbd->GetKeyState (CSKEY_UP))
c->Move (CS_VEC_FORWARD * 4 * speed);
if (kbd->GetKeyState (CSKEY_DOWN))
c->Move (CS_VEC_BACKWARD * 4 * speed);
// Player movement...
Demo Game Part 3 — The World | 371
Adding the World
if(kbd->GetKeyState ('w'))
{
if(isWalking == false)
playerstate->SetAction("run");
isWalking = true;
player->GetMovable()->SetPosition(player->
GetMovable()->GetTransform().This2Other
(csVector3(1, 0, 0) * speed * 25));
player->GetMovable()->UpdateMove();
// NEW ->
// <- NEW
}
else
{
if(isWalking == true)
playerstate->SetAction("stand");
isWalking = false;
}
if(kbd->GetKeyState ('a'))
{
player->GetMovable ()->GetTransform ().RotateThis
(csVector3(0, -1, 0), speed * 4);
player->GetMovable()->UpdateMove();
}
if(kbd->GetKeyState ('d'))
{
player->GetMovable ()->GetTransform ().RotateThis
(csVector3(0, 1, 0), speed * 4);
player->GetMovable()->UpdateMove();
}
} // end of 'accept keyboard input'
view->GetCamera()->GetTransform().LookAt(player->GetMovable()->
GetPosition()+csVector3(0, 1, 0)-view->GetCamera()->
GetTransform ().GetOrigin(), csVector3(0, 1, 0));
aws->Redraw();
g3d->FinishDraw();
// Begin 2D rendering...
if (!g2d->BeginDraw ())
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Failed to begin 2D frame");
return;
}
// <- ADDED
char buf[256];
sprintf(buf, "Butterfly Grid Tutorial Game");
// g2d->Write(font, 10,10, fntcol, -1, buf);
}
void Butterfly::FinishFrame ()
{
g2d->FinishDraw (); // MODIFIED
g2d->Print (NULL); // MODIFIED
}
bool Butterfly::Initialize ()
{
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("crystalspace.window.alternatemanager", iAws),
CS_REQUEST_PLUGIN("crystalspace.collisiondetection.rapid",
iCollideSystem), // ADDED
CS_REQUEST_END))
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Can't initialize plugins!");
return false;
}
csCommandLineHelper::Help (object_reg);
return false;
}
"crystalspace.application.butterfly",
"No iKeyboardDriver plugin!");
return false;
}
Y
// ADDED ->
// The collision detection system.
if (!cdsys)
{
FL
cdsys = CS_QUERY_REGISTRY (object_reg, iCollideSystem);
AM
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.simpcd",
"Can't find the collision detection system!");
return false;
TE
}
// <- ADDED
// 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.butterfly",
"Error opening system!");
return false;
}
txtmgr = g3d->GetTextureManager();
// ADDED ->
txtmgr->SetVerbose (true);
Team-Fly®
376 | Chapter 8
Adding the World
iMaterialWrapper* tm =
engine->GetMaterialList ()->FindByName ("butterflytexture");
iPolygon3D* p;
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-20, 0, 20));
p->CreateVertex (csVector3 (20, 0, 20));
p->CreateVertex (csVector3 (20, 0, -20));
p->CreateVertex (csVector3 (-20, 0, -20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-20, 20, -20));
p->CreateVertex (csVector3 (20, 20, -20));
p->CreateVertex (csVector3 (20, 20, 20));
p->CreateVertex (csVector3 (-20, 20, 20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-20, 20, 20));
p->CreateVertex (csVector3 (20, 20, 20));
p->CreateVertex (csVector3 (20, 0, 20));
p->CreateVertex (csVector3 (-20, 0, 20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (20, 20, 20));
p->CreateVertex (csVector3 (20, 20, -20));
p->CreateVertex (csVector3 (20, 0, -20));
p->CreateVertex (csVector3 (20, 0, 20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
p->CreateVertex (csVector3 (-20, 20, -20));
p->CreateVertex (csVector3 (-20, 20, 20));
p->CreateVertex (csVector3 (-20, 0, 20));
p->CreateVertex (csVector3 (-20, 0, -20));
p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 5);
p = walls_state->CreatePolygon ();
p->SetMaterial (tm);
Demo Game Part 3 — The World | 377
Adding the World
csRef<iStatLight> light;
iLightList* ll = room->GetLights ();
csMatrix3 m;
m.Identity ();
m *= 5.0;
imeshfact->HardTransform (l_rT);
playerCollider = CreateCollider(player);
if (playerCollider == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Error creating playerCollider!");
return false;
}
mapCollider = CreateCollider(walls);
if (mapCollider == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Error creating mapCollider!");
return false;
}
engine->Prepare ();
// <- ADDED
// load preferences...
if(!aws->GetPrefMgr()->Load ("/this/data/temp/demogame.def"))
csReport(object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly", "couldn't load definition
file!");
loginDialog = aws->CreateWindowFrom("LoginDialog");
signupDialog = aws->CreateWindowFrom("SignupDialog");
errorDialog = aws->CreateWindowFrom("ErrorDialog");
chatDialog = aws->CreateWindowFrom("ChatDialog");
r = loginDialog->Frame();
winWidth = r.xmax - r.xmin;
winHeight = r.ymax - r.ymin;
loginDialog->MoveTo((g2d->GetWidth()/2) - winWidth/2,
(g2d->GetHeight()/2) - winHeight/2);
r = signupDialog->Frame();
winWidth = r.xmax - r.xmin;
winHeight = r.ymax - r.ymin;
signupDialog->MoveTo((g2d->GetWidth()/2) - winWidth/2,
(g2d->GetHeight()/2) - winHeight/2);
r = errorDialog->Frame();
winWidth = r.xmax - r.xmin;
winHeight = r.ymax - r.ymin;
errorDialog->MoveTo((g2d->GetWidth()/2) - winWidth/2,
(g2d->GetHeight()/2) - winHeight/2);
loginDialog->Show();
font = g2d->GetFontServer()->LoadFont(CSFONT_LARGE);
return true;
}
// ADDED ->
iCollider* Butterfly::CreateCollider(iMeshWrapper* mesh)
{
csRef<iPolygonMesh> polmesh (SCF_QUERY_INTERFACE (mesh->GetMeshObject (),
iPolygonMesh));
if (polmesh)
{
csColliderWrapper* wrap = new csColliderWrapper
(mesh->QueryObject (), cdsys, polmesh);
wrap->DecRef ();
return wrap->GetCollider ();
}
else
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.simpcd",
"Object doesn't support collision detection!");
return NULL;
}
}
// <- ADDED
iString *password;
appPtr->loginDialog->Hide();
if(!appPtr->username->GetData() || strlen(appPtr->username->GetData()) == 0)
{
// show the error and set the error status...
Demo Game Part 3 — The World | 381
Adding the World
appPtr->errorFromLogin = true;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
else if(!password->GetData() || strlen(password->GetData()) == 0)
{
appPtr->errorFromLogin = true;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
else
{
// attempt to log them into the game
// NEW ->
networkHandler->DoLogin(appPtr->username->GetData(),
password->GetData());
// <- NEW
}
fflush (stdout);
}
appPtr->loginDialog->Hide();
appPtr->signupDialog->Show();
}
appPtr->signupDialog->Hide();
appPtr->loginDialog->Show();
}
iString *name;
iString *email;
iString *username;
iString *password;
iString *confirmPassword;
appPtr->signupDialog->FindChild("SignupFullname")->GetProperty("Text",
(void **) &name);
appPtr->signupDialog->FindChild("SignupEmail")->GetProperty("Text",
(void **) &email);
appPtr->signupDialog->FindChild("SignupUsername")->GetProperty("Text",
(void **) &username);
appPtr->signupDialog->FindChild("SignupPassword")->GetProperty("Text",
(void **) &password);
appPtr->signupDialog->FindChild("SignupConfirmPassword")->GetProperty
("Text", (void **) &confirmPassword);
if(!name->GetData() || strlen(name->GetData()) == 0)
{
// show the error and set the error status...
csRef<iString> errStr = csPtr<iString> (new scfString ("Name invalid,
please correct this."));
appPtr->errorFromLogin = false;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
else if(!email->GetData() || strlen(email->GetData()) == 0)
{
appPtr->errorFromLogin = false;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
else if(!username->GetData() || strlen(username->GetData()) == 0)
{
appPtr->errorFromLogin = false;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
else if(!password->GetData() || strlen(password->GetData()) == 0)
{
appPtr->errorFromLogin = false;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
else if(!confirmPassword->GetData() || strcmp(password->GetData(),
confirmPassword->GetData()) != 0)
{
appPtr->errorFromLogin = false;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
else
{
// Perform the signup here...
appPtr->errorDialog->Hide();
if(appPtr->errorFromLogin)
appPtr->loginDialog->Show();
384 | Chapter 8
Adding the World
else
appPtr->signupDialog->Show();
}
iString *chatMessage;
appPtr->chatDialog->FindChild("ChatInput")->GetProperty("Text", (void **)
&chatMessage);
std::vector<BNGUIDLIST> vGUIDList;
if(pcUsername)
{
networkHandler->GetOMS()->GetGuidList(vGUIDList);
appPtr->chatDialog->FindChild("ChatInput")->SetProperty("Text", emptyStr);
}
// ADDED ->
void Butterfly::TextBoxClicked(void* awst, iAwsSource *)
{
// stop movement while in the chat input
acceptKeyboardInput = false;
}
Y
acceptKeyboardInput = true;
}
// <- ADDED
FL
void Butterfly::Start ()
AM
{
csDefaultRunLoop (object_reg);
}
TE
font = NULL;
csInitializer::DestroyApplication (object_reg);
return 0;
}
If we now run this updated example and log in as a valid user, something similar
to the following should appear:
Team-Fly®
386 | Chapter 8
Adding the World
Now that the first part of the code is merged, we need to merge in the
demoskyOMS code so that we can display the other users’ avatars within the
world.
Let’s now look at the final changes we need to make to let this work.
Firstly, in the demogame.h header we have moved the imeshfact reference we
previously declared locally in the Initialize method to be a private member of
the Butterfly class. This is simply so that we can access it for creating other
players as they connect to the game. The declaration for this can be seen here:
csRef<iMeshFactoryWrapper> imeshfact; // FINAL ADDED (moved)
The only other change to the header is the addition of three get methods that we
will be using in the CNetworkHandler class to access the private references of
the Butterfly class. The prototypes of these three helper methods can be seen
here:
iEngine* GetEngine() { return engine; }
iMeshFactoryWrapper* GetSpriteFactory() { return imeshfact; }
iSector* GetRoomSector() { return room; }
int res;
if(BN_SUCCESS(res = GetOMS()->SetStateByGUID(GetOMS()->GetGUIDAvatar(),
BN_ATTRIB_NAME, m_pcUsername, strlen(m_pcUsername))))
{
printf("\nAvatar Name Set to %s.\n", m_pcUsername);
}
else
printf("\nFailed to set Avatar Name\n");
After this, we need to rewrite the CreateThing method so that it will actually
create a new player sprite for each player as he or she joins the game. Let’s first
look at the complete new implementation and then we will look at it line by line.
bool CNetworkHandler::CreateThing(BNGUID guidThing)
{
bool bRetVal = false;
char pcTemp[256];
BNOBJECTTYPE typeObject;
if(typeObject == BN_THING_AVATAR)
{
FPOINT3 vPosition = {0.0f, 0.0f, 0.0f};
FPOINT3 vOrientation = {0.0f, 0.0f, 0.0f};
if (BN_SUCCESS(m_pOMS->GetMotionByGUID(guidThing,
vPosition, vOrientation)))
{
csVector3 pos;
pos.x = (vPosition.x - X_OFFSET) / X_SCALE;
pos.y = 3.1;
pos.z = (vPosition.y - Y_OFFSET) / Y_SCALE;
GetSpriteFactory(), "PlayerSprite",
butterfly->GetRoomSector(), csVector3
(pos.x, 3.1, pos.z)));
fflush(stdout);
return bRetVal;
}
In this new CreateThing method we first ensure we have a valid pointer to the
OMS. At the same time we check that the GUID of the object being created does
not already exist within our world. This is achieved with the following if
statement:
if (GetOMS() && FindThingItem(guidThing)==NULL)
We then retrieve the type of the thing that is being created into the variable
typeObject, using the following line of code:
if (BN_SUCCESS(GetOMS()->GetTypeByGUID(guidThing, typeObject)))
{
After this, we create a csVector3 object and assign the x, z position retrieved
from the OMS and also the fixed 3.1 height to ensure the player remains on the
floor. This can be seen here:
csVector3 pos;
pos.x = (vPosition.x - X_OFFSET) / X_SCALE;
pos.y = 3.1;
pos.z = (vPosition.y - Y_OFFSET) / Y_SCALE;
Next, we use the mesh factory to create a player sprite and add it to the world.
This can be seen in the following code section:
csRef<iMeshWrapper> sprite (butterfly->GetEngine()->CreateMeshWrapper(butterfly->
GetSpriteFactory(), "PlayerSprite", butterfly->GetRoomSector(), csVector3
(pos.x, 3.1, pos.z)));
Then finally, we make a call to the AddThingItem method so that we store a ref-
erence to the new sprite with the associated GUID in our internal list. This can
be seen here:
AddThingItem(guidThing, sprite);
The next method we need to modify is the RemoveThing method, which should
now look as follows:
bool CNetworkHandler::RemoveThing(BNGUID guidThing)
{
bool bRetVal = false;
char pcTemp[256];
bRetVal = true;
return bRetVal;
}
When a player has to be removed from the world, we can find the associated
sprite by making a call to our FindThingItem method, passing in the guidThing
variable. Once this returns the sprite, we can then simply call the RemoveObject
method of the iEngine interface and then finally we call RemoveThingItem to
get the object out of our own internal list.
390 | Chapter 8
Adding the World
spThing->GetMovable()->SetPosition (pos);
spThing->GetMovable()->UpdateMove ();
bRetVal = true;
return bRetVal;
}
In this new method, we first obtain the sprite from the GUID passed into the
method using the following line of code:
csRef<iMeshWrapper> spThing = FindThingItem(guidThing);
Once we have this, we check whether the reference is valid. We then proceed by
obtaining the updated position from the OMS using the following segment of
code:
FPOINT3 vPosition = {0.0f, 0.0f, 0.0f};
FPOINT3 vOrientation = {0.0f, 0.0f, 0.0f};
if (BN_SUCCESS(m_pOMS->GetMotionByGUID(guidThing, vPosition, vOrientation)))
{
Demo Game Part 3 — The World | 391
Adding the World
If this works, we can simply update the position of the sprite using the following
few lines of code:
csVector3 pos;
pos.x = (vPosition.x - X_OFFSET) / X_SCALE;
pos.y = 3.1; // fixed y pos
pos.z = (vPosition.y - Y_OFFSET) / Y_SCALE;
spThing->GetMovable()->SetPosition (pos);
spThing->GetMovable()->UpdateMove();
Since we have made many changes to the CNetworkHandler source, here is the
complete listing for reference.
Listing 8-5: CNetworkHandler.cpp
#include "grid-oms/OMSWrapper.h"
#include <list>
#include "../butterfly-grid/grid-common/thing/thing_types.h"
#include "cssysdef.h"
#include "cssys/sysfunc.h"
#include "grid-oms/OMSWrapper.h"
#include "cstool/proctex.h"
#include "cstool/prsky.h"
#include "cstool/csview.h"
#include "cstool/initapp.h"
#include "csutil/cmdhelp.h"
#include "ivideo/graph3d.h"
#include "ivideo/graph2d.h"
#include "ivideo/natwin.h"
#include "ivideo/txtmgr.h"
#include "ivideo/fontserv.h"
#include "ivaria/conout.h"
#include "imesh/sprite2d.h"
#include "imesh/object.h"
#include "imap/parser.h"
#include "iengine/mesh.h"
#include "iengine/engine.h"
#include "iengine/sector.h"
#include "iengine/camera.h"
#include "iengine/movable.h"
#include "iengine/material.h"
#include "imesh/thing/polygon.h"
#include "imesh/thing/thing.h"
#include "ivaria/reporter.h"
#include "igraphic/imageio.h"
#include "iutil/comp.h"
#include "iutil/eventh.h"
#include "iutil/eventq.h"
#include "iutil/event.h"
#include "iutil/objreg.h"
#include "iutil/csinput.h"
#include "iutil/virtclk.h"
#include "iutil/vfs.h"
392 | Chapter 8
Adding the World
CNetworkHandler::CNetworkHandler ()
{
}
CNetworkHandler::~CNetworkHandler ()
{
}
SetUsername(username);
SetPassword(password);
SetAvatarName(username);
void CNetworkHandler::EventLogonPass()
{
printf("Login Successful!");
butterfly->GetChatDialog()->Show();
butterfly->SetGameActive(true);
vPosition.x = 0;
vPosition.y = 0;
vPosition.z = 0;
FPOINT3 vOrientation = {0.0f,0.0f,0.0f};
GetOMS()->SetMotionByGUID(m_guidAvatar, vPosition, vOrientation);
}
void CNetworkHandler::EventLogonFail()
{
printf("Login Failed!\n");
butterfly->SetErrorFromLogin(true);
butterfly->GetErrorDialog()->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
butterfly->GetErrorDialog()->Show();
}
if(typeThing == BN_THING_AVATAR)
{
printf("Another player has logged in...");
unsigned int uLength = 0;
char *usernameData = new char[OMS_MAX_STRING_LEN];
if (BN_SUCCESS(GetOMS()->GetStateByGUID(guidThing, BN_ATTRIB_NAME,
&usernameData, uLength)))
{
usernameData[uLength] = 0;
csRef<iString> usernameStr = csPtr<iString> (new scfString
(usernameData));
csRef<iAwsParmList> params = butterfly->GetAWS()->
CreateParmList();
params->AddInt("row", 0);
params->AddString("string", ("[Player "+*usernameStr+" has
joined the game]").GetData());
butterfly->GetChatDialog()->FindChild("ChatArea")->
Execute("InsertRow", params);
}
else
printf("Failed to get new avatar name!\n");
CreateThing(guidThing);
}
394 | Chapter 8
Adding the World
int res;
if(BN_SUCCESS(res = GetOMS()->SetStateByGUID(GetOMS()->GetGUIDAvatar(),
BN_ATTRIB_NAME, m_pcUsername, strlen(m_pcUsername))))
{
printf("\nAvatar Name Set to %s.\n", m_pcUsername);
}
else
printf("\nFailed to set Avatar Name\n");
(usernameData));
csRef<iAwsParmList> params = butterfly->GetAWS()->
CreateParmList();
params->AddInt("row", 0);
params->AddString("string", ("[Player "+*usernameStr+" has
left the game]").GetData());
butterfly->GetChatDialog()->FindChild("ChatArea")->
Execute("InsertRow", params);
// Ping this other user...
GetOMS()->MessageFind(GetOMS()->GetGUIDAvatar(), usernameData);
}
else
printf("Failed to get left avatar name!\n");
Y
}
} FL
RemoveThing(guidThing);
AM
bool CNetworkHandler::UpdateThing(BNGUID guidThing, bool bClientControlled)
{
bool bRetVal = false;
TE
if ( !m_pOMS )
return false;
UpdateThingView(guidThing, bClientControlled);
Team-Fly®
396 | Chapter 8
Adding the World
Value.vVector.z);
break;
case BUTTERFLY_VELOCITY:
bRetVal = SetThingVelocity(guidThing, vAttributes
[uAttrib].m_Attribute.Value.vVector.x,
vAttributes[uAttrib].m_Attribute.Value.
vVector.y, vAttributes[uAttrib].m_Attribute.
Value.vVector.z);
break;
case BN_ATTRIB_NAME:
printf("\n\nNon Client name change");
if ((vAttributes[uAttrib].m_Attribute.Type ==
PROPERTY_STRING) && vAttributes
[uAttrib].m_Attribute.Value.String.pcData)
{
vAttributes[uAttrib].m_Attribute.Value.
String.pcData[vAttributes[uAttrib]
.m_Attribute.Value.String.iLength] = 0;
}
break;
default:
DisplayOtherState(guidThing, &(vAttributes
[uAttrib]));
break;
}
}
else
{
// Try the new GetStatesByGUID function
m_pOMS->GetStatesByGUID(guidThing, vAttributes, false);
case BN_ATTRIB_NAME:
vAttributes[uAttrib].m_Attribute.Value.String.
pcData[vAttributes[uAttrib].m_Attribute.Value.
String.iLength] = 0;
printf("\n\nGot name as: %s\n\n", vAttributes
[uAttrib].m_Attribute.Value.String.pcData);
break;
default:
DisplayOtherState(guidThing, &(vAttributes
[uAttrib]));
break;
}
if (( vAttributes[uAttrib].m_Attribute.Type == PROPERTY_BLOB ))
// ( vAttributes[uAttrib].m_Attribute.Type ==
// PROPERTY_LIST_BLOB ))
{
if ( vAttributes[uAttrib].m_Attribute.Value.Blob.pvData
!= NULL )
delete [] (UBYTE *)vAttributes[uAttrib].m_Attribute.
Value.Blob.pvData;
vAttributes[uAttrib].m_Attribute.Value.Blob.pvData = NULL;
vAttributes[uAttrib].m_Attribute.Value.Blob.iLength = 0;
vAttributes[uAttrib].m_Attribute.Type = PROPERTY_NULL;
}
}
}
return bRetVal;
}
printf("Creating THING!!!");
if (GetOMS())
{
float fRange = 0;
GetOMS()->GetStateByGUID(guidThing, BUTTERFLY_RANGE, fRange);
if (BN_SUCCESS(GetOMS()->GetTypeByGUID(guidThing, typeObject)))
{
printf("Created Thing %ld of type %d with range %4.3f\n",
guidThing, typeObject, sqrt(fRange));
}
else
printf("Created Thing FAILED could not get thing type\n");
}
else
printf("Created Thing FAILED could not get oms pointer\n");
UpdateThing(guidThing, false);
return bRetVal;
}*/ // REMOVED
BNOBJECTTYPE typeObject;
if(typeObject == BN_THING_AVATAR)
{
FPOINT3 vPosition = {0.0f, 0.0f, 0.0f};
FPOINT3 vOrientation = {0.0f, 0.0f, 0.0f};
if (BN_SUCCESS(m_pOMS->GetMotionByGUID(guidThing,
vPosition, vOrientation)))
{
csVector3 pos;
pos.x = (vPosition.x - X_OFFSET) / X_SCALE;
pos.y = 3.1;
pos.z = (vPosition.y - Y_OFFSET) / Y_SCALE;
fflush(stdout);
400 | Chapter 8
Adding the World
return bRetVal;
}
// <- FINAL ADDED
bRetVal = true;
return bRetVal;
}
return bRetVal;
}*/ // REMOVED
bRetVal = true;
return bRetVal;
}
// <- FINAL ADDED
char pcTemp[256];
printf(" -> Thing %ld moved to position %4.2f, %4.2f, %4.2f\n", guidThing,
fX, fY, fZ);
bRetVal = true;
return bRetVal;
}*/ // REMOVED
spThing->GetMovable()->SetPosition (pos);
spThing->GetMovable()->UpdateMove ();
bRetVal = true;
return bRetVal;
}
// <- FINAL ADDED
return bRetVal;
}
return bRetVal;
}
if ( pcAnimation )
{
printf(" -> Thing %ld animation set to %s\n", guidThing, pcAnimation);
bRetVal = true;
}
return bRetVal;
}
return bRetVal;
}
return true;
}
Temp.guidThing = guidThing;
Temp.spThing = pThing;
m_vThingList.push_back(Temp);
}
return true;
}
return false;
}
return NULL;
}
m_vThingList.remove(*viterThing);
}
return spTemp;
}
if ( pAttribute )
{
switch ( pAttribute->m_Attribute.Type )
{
default:
case PROPERTY_NULL:
printf("State %i has an invalid property type (%i)\n",
pAttribute->m_idState, pAttribute->m_Attribute.Type);
break;
case PROPERTY_LIST_LONG:
printf("for index %i\n", pAttribute->m_Attribute.iListIndex);
case PROPERTY_LONG:
printf("State %i (long, %i) set to %i %s\n", pAttribute->
m_idState, pAttribute->m_typeObject, pAttribute->
m_Attribute.Value.lLong, pcIndex);
break;
case PROPERTY_LIST_FLOAT:
case PROPERTY_FLOAT:
printf("State %i (float, %i) set to %f %s\n", pAttribute->
m_idState, pAttribute->m_typeObject, pAttribute->
m_Attribute.Value.fFloat, pcIndex);
break;
case PROPERTY_LIST_VECTOR:
case PROPERTY_VECTOR:
printf("State %i (vector, %i) set to %4.3f, %4.3f, %4.3f %s\n",
pAttribute->m_idState, pAttribute->m_typeObject,
pAttribute->m_Attribute.Value.vVector.x, pAttribute->
Demo Game Part 3 — The World | 405
Adding the World
m_Attribute.Value.vVector.y, pAttribute->
m_Attribute.Value.vVector.z, pcIndex);
break;
case PROPERTY_LIST_ENUM:
case PROPERTY_ENUM:
printf("State %i (enum, %i) set to %i %s\n", pAttribute->
m_idState, pAttribute->m_typeObject, pAttribute->
m_Attribute.Value.lLong, pcIndex);
break;
case PROPERTY_LIST_BLOB:
case PROPERTY_BLOB:
printf("State %i (blob, %i) set to %s length %i %s\n",
pAttribute->m_idState, pAttribute->m_typeObject,
pAttribute->m_Attribute.Value.Blob.pvData,
Y
pAttribute->m_Attribute.Value.Blob.iLength, pcIndex);
break;
FL
case PROPERTY_LIST_STRING:
case PROPERTY_STRING:
printf("State %i (string, %i) set to %s length %i %s\n",
AM
pAttribute->m_idState, pAttribute->m_typeObject,
pAttribute->m_Attribute.Value.String.pcData,
pAttribute->m_Attribute.Value.String.iLength, pcIndex);
break;
TE
case PROPERTY_LIST_TOKEN:
case PROPERTY_TOKEN:
printf("State %i (token, %i) set to guid %i spec %i
type %i %s\n", pAttribute->m_idState, pAttribute->
m_typeObject, pAttribute->m_Attribute.Value.Token.guid,
pAttribute->m_Attribute.Value.Token.spec, pAttribute->
m_Attribute.Value.Token.type, pcIndex);
break;
}
}
}
The final changes we have to make are to the demogame.cpp source file.
Firstly, in the FinishFrame method, we need to send the updated player posi-
tion to the OMS so it can process it back to the server. The updated FinishFrame
method can be seen here:
void Butterfly::FinishFrame ()
{
g2d->FinishDraw (); // MODIFIED
g2d->Print (NULL); // MODIFIED
Team-Fly®
406 | Chapter 8
Summary
Notice how we can easily retrieve the player’s position by first obtaining the
iMovable interface and then making a call to GetPosition on it. Then once we
place the data into a suitable format, we call the SetMotionByGUID method of
the OMS, passing in the GUID of the player and its new position.
So if we now run this final example with two different user accounts, you
should be able to see the other player’s avatar move around. You should also be
able to communicate with the other user in the chat window.
Here are some screen shots showing this in action:
Summary
In this chapter we brought all our knowledge from throughout the book into one
demo. Although far from a complete game, it gives a starting point for you to
develop your own massively multiplayer games using the Butterfly Grid
technology.
As the Butterfly Grid is under constant development and redevelopment, I
have set up a web site at the following address for you to download all the
source code and any fixes that have become available since the publication of
this book.
' http://www.butterflyguide.net
Index | 407
Index
2D application, creating, 36-61 implementing, 345-355
3D rendering, 84-85 testing, 343-345
chatdialog.txt file, 280
A
CheckHelp method, 54
AddThingItem method, 220
CleanUp method, 208-209
Alternate Windowing System, see AWS
Client Libraries, 3
AWS,
CLiving class, 201
porting Qt Designer file to, 277
CNetworkHandler class, 323-324
window flags, 297
creating, 323-327
AWS definition file, converting files to,
code listings
275-276
butterfly.cpp, 38-44
B butterfly.h, 44-45
BeginDraw method, 84, 208 butterfly3d1.cpp, 67-74
BN_MESSAGE_TYPE enumeration, 223 butterfly3d1.h, 75
BN_MESSAGE_TYPE_BINARY_PROJECTI butterfly3d2.cpp, 85-94
LE enumeration, 224 butterfly3d2.h, 94-95
BRESULT enumeration values, 197 butterfly3d3.cpp, 106-116
Butterfly account, butterfly3d3.h, 116-117
accessing via Internet, 7-9 butterfly3d4.cpp, 134-144
accessing via SSH, 9-12 butterfly3d4.h, 144-145
creating, 7 ClientCreates.cpp, 182-183
Butterfly Grid, 3 ClientCreates.h, 183
as alternative to shard worlds, 4-5 ClientObject.cpp, 183-185
key aspects of, 3-4 ClientObject.h, 185-186
overview of, 4-5 ClientObjectDefines.h, 186-187
Butterfly Lab, 149-150 CNetworkHandler.cpp, 324-327, 391-405
benefits of, 7 demogame.cpp, 368-385
tools, 8-9 demogame.def, 362-366
Butterfly.net, creating account with, 7 demogame.h, 366-368
demogametut1.cpp, 286-291, 299-309
C
demogametut1.h, 291-292, 309
camera,
DemoSky.cpp, 152-179, 229-244
rotating, 84, 103
DemoSky.h, 179-182, 245-247
setting position of, 105
Collide method, 120-121
CAnimal class, 199-200
CollidePath method, 121
CAvatar class, 199, 346
collision detection,
Chat dialog, creating, 271-274
example, 106-121
chat system,
in Crystal Space, 106
adding, 333-345
command events, 64-65
408 | Index
O project,
Object Management System, see OMS creating, 25-26
objects, defining, 340-341 creating resource file for, 33-36
OMS, 3 setting up, 27-33
events, 227-228 Project Settings window, 27-33
integrating with OMSWrapper class,
Q
229-249
Qt,
integrating without wrapper, 151-209
installing, 253-256
obtaining client libraries, 149-150
using to create dialogs, 256-258
OMS_EVENT_EMBODY_DONE event,
Qt Designer, 256-257
211-217, 227
porting file to AWS, 277
OMS_EVENT_EMBODY_FAIL event, 218,
using, 257-258
227
Qt Evaluation Version Installation Wizard,
OMS_EVENT_IDENT_LIST_CHANGE
254-256
event, 210-211, 227
Quake 2 model example, 85-105
OMS_EVENT_LOGON_FAIL event, 210,
227 R
OMS_EVENT_LOGON_PASS event, 210, ReceiveMessage method, 224
227 ReceiveOffline method, 222-223
OMS_EVENT_MESSAGE_RECEIVED ReceivePing method, 223
event, 223-226, 228 ReceiveProjectile method, 224-226
OMS_EVENT_MESSAGE_RECEIVED_SEC ReceiveSecure method, 226
URE event, 226-227, 228 RegisterSink method, 313-314
OMS_EVENT_MESSAGE_USER_OFFLINE RegisterTrigger method, 313-314
event, 222-223, 228 RemoveThing method, 222, 389
OMS_EVENT_MESSAGE_USER_PING RemoveThingItem method, 222
event, 223, 228 RequestPlugins method, 52-53
OMS_EVENT_SCRIPT_EVENT event, 228 ResetCollisionPairs method, 120
OMS_EVENT_THING_DROP event, resource file, creating, 33-36
221-222, 227 RotateThis method, 84
OMS_EVENT_THING_GONE event, 222,
S
228
scripting logic, 5
OMS_EVENT_THING_HERE event, 221,
sector, 76
227
SelectDefaultSkin method, 298
OMS_EVENT_THING_NEW event, 218-221,
SendChatMessage method, 312, 319-321,
227
354-355
OMS_EVENT_THING_SET event, 221, 227
ServerInfo.cfg file, 188, 248
OMS_EVENT_TRANSFER_TYPE events,
ServerLogin method, 203, 328
202
ServerLogout method, 209
OMSWrapper class,
SetAction method, 104
implementing OMS with, 229-249
SetAvatar method, 211
implementing OMS without, 151-209
SetCanvas method, 297
OpenApplication method, 54-55
SetErrorFromLogin method, 331
P SetEventMessageHandle method, 203
polygon vertices, 78-79 SetFlag method, 297
Prepare method, 203 SetGameActive method, 329
Index | 411
SetLightingCacheMode method, 77 T
SetMotionByGUID method, 204, 208-209, TextBoxClicked method, 362
249 TextBoxLostFocus method, 362
SetPosition method, 220 textures,
SetStateByGUID method, 350-351 applying, 80-81
SetTextureSpace method, 78 creating, 192
SetThingPosition method, 214-215, 390-391 ThingItem structure, 214-215, 324
SetTransform method, 220
U
SetupCreateThingTable method, 198-199, 328
u-axis, 79
SetupEventHandler method, 53-54
UpdateMove method, 216, 220
SetupFrame method, 57-60, 83-85, 103-105,
UpdateThing method, 211-212, 216, 347-350
120-121, 204-208, 249, 298-299
UpdateThingView method, 212
shard worlds, 2-3
user accounts, creating, 338
Butterfly Grid as alternative to, 4-5
Shared Class Facility, 51-52 V
Signup dialog, creating, 267-269 Valve Hammer Editor, 121 see also map editor
signup system, implementing, 327 using, 121-132
SignupCancel method, 312, 317-318 Valve Hammer Editor window, 125-132
signupdialog.txt file, 279-280 view, creating, 82-83
sinks, adding, 299-311
W
skin, loading, 101
WinCVS,
smart pointers, 46-47
configuring, 14-17
sprite, converting model to, 99
installing, 13-14
SSH, using to access Butterfly account, 9-12
WinCvs Preferences window, 14-16
Start method, 56
world, see also map
structures
adding, 355-407
Command, 64-65
initializing, 358-361
CThingAttribute, 213-214
Worldcraft, see Valve Hammer Editor
EventInfo, 204-205
Write method, 60
Joystick, 64
Key, 61-62 X
Mouse, 63 XML format, converting files from, 275-276
ThingItem, 214-215, 324 XML GCS file, 334-337