You are on page 1of 417

TE

AM
FL
Y
Official Butterfly.net®
Game Developer’s
Guide

Andrew Mulholland

Wordware Publishing, Inc.


Library of Congress Cataloging-in-Publication Data

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

© 2005, Wordware Publishing, Inc.


All Rights Reserved

2320 Los Rios Boulevard


Plano, Texas 75074

No part of this book may be reproduced in any form or by any means


without permission in writing from Wordware Publishing, Inc.

Printed in the United States of America

ISBN 1-55622-044-8
10 9 8 7 6 5 4 3 2 1
0406

Butterfly.net is a registered trademark and Butterfly Grid is a trademark of Butterfly.net, Inc.


All brand names and product names mentioned in this book are trademarks or service marks of their respective
companies. Any omission or misuse (of any kind) of service marks or trademarks should not be regarded as
intent to infringe on the property of others. The publisher recognizes and respects all marks used by companies,
manufacturers, and developers as a means to distinguish their products.

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

Chapter 2 First Steps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7


Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Accessing Your Account via the Internet . . . . . . . . . . . . . . . . . . . . . . . . . 7
Accessing Your Account via SSH . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12

Chapter 3 Getting Started with Crystal Space . . . . . . . . . . . . . . . . . . 13


Introduction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Getting Crystal Space . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Using the CVS Client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Getting WinCVS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Configuring WinCVS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Connecting to the CVS Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Updating the Local Version . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Compiling Crystal Space . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Testing the Installation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Creating Your Own Project. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
Setting the Directories . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
Creating and Setting Up the Project . . . . . . . . . . . . . . . . . . . . . . . . . 25
Creating the Project Resource File . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Creating a Simple 2D Application . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
iv | Contents

Chapter 4 Moving into 3D . . . . . . . . . . . . . . . . . . . . . . . . . . . 67


Introduction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
Basics of 3D in Crystal Space . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
Quake 2 Model Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
Adding Simple Collision Detection . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
Creating and Loading a Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
Creating the World. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
Saving and Converting the World . . . . . . . . . . . . . . . . . . . . . . . . . . 132
Loading the World into Crystal Space . . . . . . . . . . . . . . . . . . . . . . . . 134
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148

Chapter 5 Integrating the OMS with an Existing Application . . . . . . . . . . 149


Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
Obtaining the Latest OMS Client Libraries . . . . . . . . . . . . . . . . . . . . . . . 149
Integrating the OMS without the Wrapper . . . . . . . . . . . . . . . . . . . . . . . 151
Events . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
OMS_EVENT_LOGON_PASS . . . . . . . . . . . . . . . . . . . . . . . . . 210
OMS_EVENT_LOGON_FAIL . . . . . . . . . . . . . . . . . . . . . . . . . 210
OMS_EVENT_IDENT_LIST_CHANGE . . . . . . . . . . . . . . . . . . . . 210
OMS_EVENT_EMBODY_DONE . . . . . . . . . . . . . . . . . . . . . . . 211
OMS_EVENT_EMBODY_FAIL . . . . . . . . . . . . . . . . . . . . . . . . 218
OMS_EVENT_THING_NEW . . . . . . . . . . . . . . . . . . . . . . . . . 218
OMS_EVENT_THING_HERE . . . . . . . . . . . . . . . . . . . . . . . . . 221
OMS_EVENT_THING_SET . . . . . . . . . . . . . . . . . . . . . . . . . . 221
OMS_EVENT_THING_DROP . . . . . . . . . . . . . . . . . . . . . . . . . 221
OMS_EVENT_THING_GONE . . . . . . . . . . . . . . . . . . . . . . . . . 222
OMS_EVENT_MESSAGE_USER_OFFLINE . . . . . . . . . . . . . . . . . 222
OMS_EVENT_MESSAGE_USER_PING . . . . . . . . . . . . . . . . . . . 223
OMS_EVENT_MESSAGE_RECEIVED . . . . . . . . . . . . . . . . . . . . 223
OMS_EVENT_MESSAGE_RECEIVED_SECURE . . . . . . . . . . . . . . 226
Integrating the OMS with the Wrapper . . . . . . . . . . . . . . . . . . . . . . . . . 229
Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250
EventThingNew . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250
EventAvatarNew . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250
EventThingHere . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250
EventThingSet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250
EventThingDrop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
EventThingGone . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
EventThingSetPosition. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252

Chapter 6 Demo Game Part 1 — Building the GUI . . . . . . . . . . . . . . 253


Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253
Installing Qt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253
Designing the GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256
The Login Dialog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258
The Signup Dialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
The Error Dialog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269
The Chat Dialog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
Converting the Dialogs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274
Contents | v

Testing the GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286


Adding the Sinks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299
The Login Method. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 315
The CreatePlayer Method . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 317
The SignupCancel Method . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 317
The DoSignup Method . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318
The ErrorOk Method . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319
The SendChatMessage Method . . . . . . . . . . . . . . . . . . . . . . . . . . . 319
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321

Chapter 7 Demo Game Part 2 — Signup/Login . . . . . . . . . . . . . . . . 323


Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323
Creating a Skeleton CNetworkHandler Class . . . . . . . . . . . . . . . . . . . . . . 323
Implementing the Signup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327
Implementing the Login. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332

Chapter 8 Demo Game Part 3 — The World . . . . . . . . . . . . . . . . . . 333


Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333
Adding Player Communication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333
Implementing the Chat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 345
Adding the World . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406

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/

Figure 1-2: Ultima Online

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.

What Is the Butterfly Grid?


The Butterfly Grid is a multilayered architecture that provides an absolute plat-
form-independent window into a massively multiplayer world.
To the gamer, this technology gives a fresh approach to massively multi-
player technology, virtually removing the need for shard worlds and true
platform independence and allowing varied devices to seamlessly connect to the
one world.
To the developer, the Butterfly Grid provides a clean, easy-to-use API and
toolset for integration with a current or future game engine. It removes the need
to develop network code and allows focus to return to the core of the project —
the game.

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.

Figure 1-3: Four shard worlds

The Grid basically joins these four worlds together,


providing a single world with 8,000 players that is
handled by the four servers and allowing communi-
cation between all players on all the different servers.
However, the fact that four servers are used is trans- Figure 1-4: Four shards to
parent to the end user. one world
Introduction | 5
Technology Overview

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.

Network Protocol Stack (NPS)


The Network Protocol Stack is a custom protocol based upon standard UDP
(User Datagram Protocol) transmission. The difference is the addition of a
“heartbeat” packet that notifies the sender if a packet is required for retransmis-
sion. Packets can also be flagged as reliable to ensure delivery to the receiver.

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).

Scripting Game Logic


The Butterfly Grid also provides advanced mechanisms for scripted AI and
game logic. This process involves creating Python scripts on the server which
can then be invoked via the OMS from the client. This more advanced topic is
not covered within this book; however, more information on this topic and oth-
ers mentioned within this introduction can be found in the Manifesto provided
on the companion CD-ROM. Also check the official web site for new tutorials.

' 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.

Accessing Your Account via the Internet


When your account is activated, you will have access to the online collaboration
tools provided by the Butterfly Lab. These benefits are as follows:
n Private project spaces with role-based access control to manage and main-
tain design documents, source code, binary files, and art assets exclusively
with specific colleagues on a development team.
n Shared project spaces for community members to collaborate on tools, util-
ities, and performance optimization strategies.
n A revision control system based on Concurrent Versions System (CVS) that
stores and tracks the versions of evolving code and art assets, managing the
details of distributed or centralized development.
n Mailing list creation and management for project team members to circu-
late ideas and communicate changes, while newly authorized individuals can
review the project history and get up to speed.
n Issue tracking for testers and other team members to enter issues as they
arise and follow them from concept to resolution. The Lab notifies those
experts best suited to addressing each of the issues and keeps everyone
involved.

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.

Figure 2-1: The login splash screen

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.

Figure 2-2: The project screen

Accessing Your Account via SSH


Although you will not need this until you move on to the more advanced fea-
tures of the Butterfly Grid, it’s useful to know how to access your account via
SSH. First, you will need an SSH client. I recommend a neat little application
called PuTTY, which can be downloaded from the following web site.

' 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

Figure 2-3: PuTTY Configuration window

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:

Figure 2-4: Butterfly login request


First Steps | 11
Accessing Your Account via SSH

Once you have entered your correct credentials, the screen will look as follows:

Figure 2-5: Logged in

If you now perform the ls command, you will see there are three folders and a
sql log file.

Figure 2-6: Folders


12 | Chapter 2
Summary

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

Getting Started with


Crystal Space

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!).

Getting Crystal Space


There are two methods for obtaining the Crystal Space engine. The first and best
method is to use the CVS (Concurrent Versions System) repository, which is
available online. The other way is to simply download the latest CVS snapshot
directly from a web browser.
The latest snapshot is always available from the following link and has the
filename “cs-current-snapshot.zip.”

' 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.

Using the CVS Client

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:

Figure 3-1: The WinCvs 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

Figure 3-3: The WinCvs Preferences window – default Globals tab

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

Figure 3-4: The WinCvs Preferences window – updated Globals tab

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:

Figure 3-5: The WinCVS Preferences window – updated WinCvs tab

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

Figure 3-6: The CVSROOT is now set up correctly.

Connecting to the CVS Server


The next step is to actually connect and retrieve the latest Crystal Space snap-
shot from the server. To do this select the Admin option from the main menu and
then click the Login… option. You will then be presented with a password
dialog.

Figure 3-7: Password authentication dialog

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)

*****CVS exited normally with code 0*****

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

Figure 3-8: The crystal folder

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:

Figure 3-9: Checkout settings window


Getting Started with Crystal Space | 19
Using the CVS Client

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.

Figure 3-10: The local copy of Crystal Space

Updating the Local Version


So now you have the current version of Crystal Space on your hard drive. How-
ever, as Crystal Space is continually being updated, the remote repository is also
being updated with newer code, such as bug fixes and extra functionality. There-
fore, before we look at compiling the Crystal Space engine, let’s finish off this
CVS section by looking at how we update our local copy of the Crystal Space
source code.
First, log in to the remote CVS server as we did in the “Connecting to the
CVS Server” section.
Once logged in successfully, expand the crystal folder, then right-click on the
CS folder. From the pop-up menu, select the Update selection… option. After
you click this, you will be presented with the following window:
20 | Chapter 3
Compiling Crystal Space

Figure 3-11: The Update settings window

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.

Compiling Crystal Space


Now that we have the latest version of the Crystal Space source code on our
local machine (either by downloading a snapshot from the web or using CVS), it
is time to compile the source. This will create the libraries we require to develop
our own applications using the Crystal Space engine as well as the premade
example programs that are included in the package.
We are only going to cover compiling Crystal Space on the Windows plat-
form using Visual Studio 6, although it is possible to use other platforms. If you
are planning to use Crystal Space with Visual Studio 7 or a different platform,
refer to the Crystal Space project home page, which is available at the following
link:

' 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

Once there, you will see a workspace called csall.dsw.


Getting Started with Crystal Space | 21
Compiling Crystal Space

Figure 3-12: Selecting the workspace

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

Figure 3-13: The FileView

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.

Figure 3-14: Setting the active project configuration


Getting Started with Crystal Space | 23
Compiling Crystal Space

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.

Testing the Installation


Now that we have Crystal Space compiled, let’s check that it is working before
we attempt to develop anything using it. As I mentioned before, there are exam-
ple applications included in the package that are also compiled at the same time
as the plug-ins and libraries, so to test it we will try running one of these exam-
ples now.
The best looking of the examples is WalkTest, as it shows a good range of
capabilities for the Crystal Space engine. If you take a look in the root directory
c:\crystal\CS you will find an executable called walktest.exe. Providing you
have followed the steps correctly so far, it should load and place you within a 3D
world in which you can walk about. Here is a sample screen shot of how the
WalkTest example looks.

Figure 3-15: The WalkTest example application


24 | Chapter 3
Creating Your Own Project

Creating Your Own Project

Setting the Directories


Now that Crystal Space is installed correctly, the next step is to create our own
applications using the engine.
Before we actually create a new project, however, we need to tell Visual Stu-
dio where to find the include files and static libraries that were created when we
compiled the Crystal Space engine in the last section. To do this, click on the
Tools option from the main menu, followed by the Options… option.
When you do this, you will be presented with the Options window, which
contains several tabs of options. Next, click the Directories tab and ensure that
the Show directories for pull-down is set to “Include files.”
In the list of directories, there should currently be four listed as follows:
c:\MSSDK\INCLUDE
c:\Program Files\Microsoft Visual Studio\VC98\INCLUDE
c:\Program Files\Microsoft Visual Studio\VC98\MFC\INCLUDE
c:\Program Files\Microsoft Visual Studio\VC98\ATL\INCLUDE

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

The window should now look as follows:

Figure 3-16: Setting the CS include directory

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:

Figure 3-17: Creating the new project


Team-Fly®
26 | Chapter 3
Creating Your Own Project

Next, click the OK button; the following screen should now be visible:

Figure 3-18: Win32 Application options

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.

Figure 3-19: Our appbutterfly project

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.

Figure 3-20: Project Settings 4 General tab

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

Figure 3-21: Project Settings 4 Debug tab

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

This can be seen in the following screen shot.

Figure 3-22: Project Settings 4 C/C++ tab 4 General category

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

Next, change the Category pull-down to show “Code Generation.” As Crystal


Space requires multithreading, we need to change the Use run-time library
pull-down to “Debug Multithreaded DLL.”

Figure 3-23: Project Settings 4 C/C++ tab 4 Code Generation category

Now change the Category pull-down to the “Precompiled Headers” option and
then select the “Not using precompiled headers” option that is now visible.

Figure 3-24: Project Settings 4 C/C++ tab 4 Precompiled Headers category

Next, change the Category pull-down to the “Preprocessor” option. Here we


need to set up the additional include directories that our project requires to be
able to find all the required plug-ins and libraries.
Add the following to the Additional include directories field (which should
initially be empty):
30 | Chapter 3
Creating Your Own Project

..\..\plugins,..\..,..\..\include\cssys\win32,..\..\include,..\..\libs,
..\..\support,..\..\apps

This can be seen in the following screen shot.

Figure 3-25: Project Settings 4 C/C++ tab 4 Preprocessor category

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

Figure 3-26: Project Settings 4 Link tab 4 General category

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

This can be seen in the following screen shot.

Figure 3-27: Project Settings 4 Link tab 4 Input category

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

Finally, we need to add CS_DEBUG to the Preprocessor definitions field so it


will then read as follows:
_DEBUG,CS_DEBUG

Once you have followed these steps, the Resources tab should look like this:

Figure 3-28: Project Settings 4 Resources tab

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:

Figure 3-29: Project Settings 4 Post-build step tab


Getting Started with Crystal Space | 33
Creating Your Own Project

That is all we need to do in the Project Settings window, so simply click OK


now.

Creating the Project Resource File


Now that we have configured the project settings, it’s time to create the resource
file for the project. Although this is not an essential step, it is good from a con-
formity point of view in that all the other Crystal Space applications contain a
resource detailing the version of the application.
So to do this, first click File in the main menu, followed by New…. Ensure
the File tab is selected on the window and then click on Resource Script in the
left-hand list. Enter the name as appbutterfly in the File name field on the
right-hand side. Also, ensure that the Location field is set to:
c:\crystal\CS\mk\visualc

Figure 3-30: Adding the Resource Script

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

Figure 3-31: Before and after…

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.

Figure 3-32: ResourceView

Once in this view, right-click on appbutterfly resources and then click the
Insert… option from the pop-up menu.

Figure 3-33: Inserting a new resource


Getting Started with Crystal Space | 35
Creating Your Own Project

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

You will then be presented with the following screen:


TE

Figure 3-35: The version information

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.

Creating a Simple 2D Application


Although as you know Crystal Space is a 3D engine, it also has support for 2D
drawing operations. What we are going to do now is create a basic Crystal Space
application in which you can move a graphic around the screen with the cursor
keys. Sound exciting? Well, the point of doing this is to understand the structure
of Crystal Space applications and see how input, rendering, and everything else
works, as there are some complex(ish) issues to look into before we even think
about rendering anything 3D to the screen using the engine.
Now that we have our project created and set up, the next step is to create a
folder to contain our source code. Let’s now create a folder called butterfly in
the c:\crystal\CS\apps location, giving our new folder the full path of:
c:\crystal\CS\apps\butterfly

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

The completed window is shown here:

Figure 3-36: Adding the source file

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.

Figure 3-38: Setting the dependencies


38 | Chapter 3
Creating Your Own Project

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.

Figure 3-39: Project dependencies

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

// The global pointer to our application...


Butterfly *butterfly;

Butterfly::Butterfly (iObjectRegistry* object_reg)


{
Butterfly::object_reg = object_reg;
}

Butterfly::~Butterfly ()
{
}

bool Butterfly::LoadPixMaps ()
{

csRef<iImage> ifile = loader->LoadImage ("/lib/butterfly/logo.jpg");


csRef<iTextureHandle> txt = txtmgr->RegisterTexture (ifile, CS_TEXTURE_2D);
txt->Prepare ();

logoImg = new csSimplePixmap (txt);

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

sprintf(buf, "Logo at (%i,%i)", logo_x, logo_y);


g2d->Write(font, 10,25, fntcol, -1, buf);

void Butterfly::FinishFrame ()
{
g2d->FinishDraw ();
g2d->Print (NULL);
}

bool Butterfly::HandleEvent (iEvent& ev)


{
if (ev.Type == csevBroadcast && ev.Command.Code == cscmdProcess)
{
butterfly->SetupFrame ();
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::SimpleEventHandler (iEvent& ev)


{
return butterfly->HandleEvent (ev);
}

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

csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,


"crystalspace.application.butterfly",
"Can't initialize plugins!");
return false;
}

if (!csInitializer::SetupEventHandler (object_reg, SimpleEventHandler))


{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Can't initialize event handler!");
return false;
}

// Check for commandline help.


if (csCommandLineHelper::CheckHelp (object_reg))
{
csCommandLineHelper::Help (object_reg);
return false;
}

// The virtual clock.


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

// Find the pointer to engine plug-in


engine = CS_QUERY_REGISTRY (object_reg, iEngine);
if (engine == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iEngine plugin!");
return false;
}

loader = CS_QUERY_REGISTRY (object_reg, iLoader);


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

g2d = CS_QUERY_REGISTRY (object_reg, iGraphics2D);


if (!g2d)
{
Getting Started with Crystal Space | 43
Creating Your Own Project

csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,


"crystalspace.application.butterfly",
"No iGraphics2D plugin!");
return false;
}

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;
}

kbd = CS_QUERY_REGISTRY (object_reg, iKeyboardDriver);


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

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

txtmgr = g3d->GetTextureManager ();

// Load in our pixmaps (for 2D)


LoadPixMaps();

font = g2d->GetFontServer()->LoadFont(CSFONT_LARGE);

return true;
}

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

int main (int argc, char* argv[])


{
44 | Chapter 3
Creating Your Own Project

iObjectRegistry* object_reg = csInitializer::CreateEnvironment (argc, argv);

butterfly = new Butterfly (object_reg);


if(butterfly->Initialize ())
butterfly->Start ();
delete butterfly;

font = NULL;

csInitializer::DestroyApplication (object_reg);
return 0;
}

Listing 3-2: butterfly.h


#ifndef __BUTTERFLY_H__
#define __BUTTERFLY_H__

#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;

static bool SimpleEventHandler (iEvent& ev);


bool HandleEvent (iEvent& ev);
void SetupFrame ();
void FinishFrame ();

bool LoadPixMaps ();


void DrawFrame2D ();
public:
Getting Started with Crystal Space | 45
Creating Your Own Project

Butterfly (iObjectRegistry* object_reg);


~Butterfly ();

bool Initialize ();


void Start ();
};

#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.

Figure 3-40: Our 2D example

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

we would now declare it as follows:


csRef<iGraphics3D> g3d;

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();

Notice how the constructor takes in a pointer to the iObjectRegistry interface;


we’ll see the point of this very shortly.
Finally, we have created a method to initialize and a method to start the appli-
cation. This can be seen here:
Getting Started with Crystal Space | 49
Creating Your Own Project

bool Initialize();
void Start();

Let’s now look into the source file, butterfly.cpp.


Starting at the top, you will notice that the first include file is cssysdef.h. This
should always be listed before any other include files in a Crystal Space applica-
tion and should be listed at the top of all source files in your application.
Next, we have a large list of included header files that are required by the
application and the Crystal Space libraries used within the application. The easi-
est way to find out which header files you require is to reference the public API
documentation for Crystal Space, as it lists all the headers required at the bottom
of each section.
After including the header files, we place the CS_IMPLEMENT_APPLI-
CATION definition before any source code to specify that this will be a Crystal
Space application. Failing to add this line could cause the following link errors
headache:
Linking...
libcstool_d.lib(initapp.obj) : error LNK2001: unresolved external symbol "void
__cdecl cs_static_var_cleanup(void (__cdecl*)(void))"
(?cs_static_var_cleanup@@YAXP6AXXZ@Z)
libcssys_d.lib(findlib.obj) : error LNK2001: unresolved external symbol "void
__cdecl cs_static_var_cleanup(void (__cdecl*)(void))"
(?cs_static_var_cleanup@@YAXP6AXXZ@Z)
MSVCRTD.lib(crtexew.obj) : error LNK2001: unresolved external symbol _WinMain@16
libcssys_d.lib(win32.obj) : error LNK2001: unresolved external symbol "struct
HINSTANCE__ * ModuleHandle" (?ModuleHandle@@3PAUHINSTANCE__@@A)
libcssys_d.lib(win32.obj) : error LNK2001: unresolved external symbol "int
ApplicationShow" (?ApplicationShow@@3HA)
csdebug\temp\appbutterfly/appbutterfly.exe : fatal error LNK1120: 4 unresolved
externals
Error executing link.exe.

appbutterfly.exe - 6 error(s), 0 warning(s)

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

int main (int argc, char* argv[])


{

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);

So our actual class constructor looks as follows:


Butterfly::Butterfly (iObjectRegistry* object_reg)
{
Butterfly::object_reg = 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

The Shared Class Facility (SCF)


Unless you are going to be contributing to the Crystal Space engine, you
don’t need to know much about this other than it works, and works well.
The idea behind the Shared Class Facility is to have all the methods you
wish to be accessible from your object in a C++ structure (or completely
public class), all defined as virtual methods.
So, as an example, let’s say we wanted to create an Alien plug-in for
Crystal Space (don’t ask why! J). And in this plug-in, all we wanted to do
was give the alien the ability to speak (by means of setting what it was
meant to say and also allowing it to “speak” it). We would then define the
interface for the alien as follows:
struct iAlien
{
virtual void SetPhrase(char *phrase);
virtual void Speak();
}

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"

class Alien : public iAlien


{
private:
char *phrase;

public:
virtual void SetPhrase(char *phrase);
virtual void Speak();
}

void Alien::SetPhrase(char *phrase)


{
this->phrase = phrase;
}

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

So finally, to then use it in an application we would first need to load in


the library that contained the implementation and then use the class fac-
tory method we created in the interface to obtain a reference to the Alien
object. Once we had this, we could use the methods just as we could with
a normal pointer to an object:
#include "ialien.h"

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:

Figure 3-41: A fatal error

If you were to change the severity down a level to CS_REPORTER_SEVER-


ITY_WARNING, the message box is not displayed. Note that the
CS_REPORTER_SEVERITY_NOTIFY severity level is also useful to simply
output information to the console window.
After we have loaded the required plug-ins, we proceed by setting up a
method to handle the event queue. We do this by calling the static method
SetupEventHandler, which is also a member of the csInitializer class. As
54 | Chapter 3
Creating Your Own Project

previously mentioned, this method requires a standard C-style function pointer,


so we first pass on our object registry (object_reg), followed by a pointer to our
static class method SimpleEventHandler, which we will look at shortly. Here is
the code used to set this up:
if (!csInitializer::SetupEventHandler (object_reg, SimpleEventHandler))
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Can't initialize event handler!");
return false;
}

After this, we call the static CheckHelp method of the csCommandLineHelper


class, which examines the command line to detect if the -help command was
issued. If it was, it calls the static Help method of the csCommandLineHelper
class, which will display any relevant help for the plug-ins that have been
loaded. This can be seen here:
// Check for commandline help.
if (csCommandLineHelper::CheckHelp (object_reg))
{
csCommandLineHelper::Help (object_reg);
return false;
}

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

This concludes the LoadPixMaps method, so we now return to the Initialize


method, which we complete by acquiring one of the standard fonts from the font
server within the Crystal Space engine. Note that we use the iGraphics2D inter-
face to first acquire the font server by means of the GetFontServer method,
which we then use to load a standard font by means of the LoadFont method:
font = g2d->GetFontServer()->LoadFont(CSFONT_LARGE);

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

Table 3-1: Event types


Event Type Purpose
csevNothing No event or unknown event.
csevKeyDown A key has been pressed.
csevKeyUp A key has been released.
csevMouseMove The mouse has been moved.
csevMouseDown A button has been pressed on the mouse.
csevMouseUp A button has been released on the mouse.
csevMouseClick A button has been pressed and released.
csevMouseDoubleClick A button has been pressed and released twice quickly.
csevJoystickMove The joystick has been moved.
csevJoystickDown A button on the joystick has been pressed.
csevJoystickUp A button on the joystick has been released.
csevCommand The event is a command.
csevBroadcast The event was broadcast to all event listeners.
csevNetwork A network event, such as an incoming packet.
csevMouseEnter The mouse has entered the application window.
csevMouseExit The mouse has exited the application window.
csevLostFocus The application lost keyboard focus.
csevGainFocus The application gained keyboard focus.
csevGroupOff A component in a group has been selected, and everyone else
should go to their off state.
csevFrameStart The frame is about to draw.

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

When we receive the cscmdFinalProcess command, we make a call to our


FinishFrame method, which handles the final drawing of the scene to the back
buffer (again, we’ll look at this method in a moment).
The handling for the cscmdFinalProcess command can be seen here:
else if (ev.Type == csevBroadcast && ev.Command.Code == cscmdFinalProcess)
{
butterfly->FinishFrame ();
return true;
}

Finally, in the HandleEvent method, we handle a csevKeyDown key event,


which is sent each time a key is pressed on the keyboard. If this event is
detected, we then check the value of Code for the key that was pressed, access-
ing the Code variable from within the Keyboard structure in the iEvent interface.
If we then find that the Escape key was pressed, we attempt to acquire a pointer
to the iEventQueue interface, which we can use to send and broadcast messages.
Once we have a pointer to the iEventQueue interface, we can call the
GetEventOutlet method, which returns a pointer to an iEventOutlet interface.
Once we have this, we can then easily call the Broadcast method, passing in the
command we require, which in this example is the cscmdQuit command as we
wish to terminate the application.
This can be seen in full in the following code segment:
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;
}

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;

Finally, we make a call to the static DestroyApplication method, which is a


member of the csInitializer class. All we need to do is pass in our object_reg
variable, which we still have a pointer to in the main method, and this will clean
up and terminate the application. This final line of code can be seen here:
csInitializer::DestroyApplication (object_reg);

Now let’s explain the possible events that can be sent.

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

Command Code Macro Purpose


cscmdContextClose This is sent if the graphics context is destroyed (or is in the
process of being destroyed). With this command, the Info pointer
will point to an iGraphics2D interface, which will reference the
context that has been destroyed.
cscmdCommandLineHelp This is broadcast when the system driver displays its
commandline help, so that every loaded plug-in is also given the
opportunity to display its own help.
cscmdPreProcess This command is broadcast just before cscmdProcess on each
frame of rendering.
cscmdProcess This command is broadcast on each frame of rendering.
cscmdPostProcess This command is broadcast right after cscmdProcess on each

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.

Basics of 3D in Crystal Space


In this first section, we are going to look at how to modify the example from the
previous chapter. What we’re going to do is create six flat polygons to represent
a room, texture them, add lighting, and then allow the user to move about the
camera in a first-person view. The best way to do this is to look at the complete
working source code for this example, see it working, then go into detail about
the major changes to the code. Note that you can modify (or replace) the code in
your existing appbutterfly project, or create a new one following the instructions
in the previous chapter. Here is the complete source code for this first example.
Listing 4-1: butterfly3d1.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 "butterfly3d1.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"

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

// The global pointer to our application...


Butterfly *butterfly;

Butterfly::Butterfly (iObjectRegistry* object_reg)


{
Butterfly::object_reg = object_reg;
}

Butterfly::~Butterfly ()
{
}

bool Butterfly::LoadPixMaps ()
{

csRef<iImage> ifile = loader->LoadImage ("/lib/butterfly/logo.jpg");


csRef<iTextureHandle> txt = txtmgr->RegisterTexture (ifile, CS_TEXTURE_2D);
txt->Prepare ();

logoImg = new csSimplePixmap (txt);

return true;
}

void Butterfly::SetupFrame ()
Moving into 3D | 69
Basics of 3D in Crystal Space

{
// Check input...

// NEW ->

// Get elapsed time from the virtual clock.


csTicks elapsed_time = vc->GetElapsedTicks ();

// Rotate the camera according to keyboard state


float speed = (elapsed_time / 1000.0) * (0.03 * 20);

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);

// Tell 3D driver we're going to display 3D things...


if (!g3d->BeginDraw(engine->GetBeginDrawFlags() | CSDRAW_3DGRAPHICS))
return;

// Render the world...


view->Draw ();

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);
}

bool Butterfly::HandleEvent(iEvent& ev)


{
if (ev.Type == csevBroadcast && ev.Command.Code == cscmdProcess)
{
butterfly->SetupFrame ();
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::SimpleEventHandler (iEvent& ev)


{
return butterfly->HandleEvent (ev);
}

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;
}

if (!csInitializer::SetupEventHandler (object_reg, SimpleEventHandler))


{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Can't initialize event handler!");
return false;
}

// Check for commandline help.


if (csCommandLineHelper::CheckHelp (object_reg))
{
csCommandLineHelper::Help (object_reg);
return false;
}

// The virtual clock.


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

// Find the pointer to engine plug-in


engine = CS_QUERY_REGISTRY (object_reg, iEngine);
if (engine == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iEngine plugin!");
return false;
}

loader = CS_QUERY_REGISTRY (object_reg, iLoader);


if (loader == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iLoader plugin!");
return false;
}
72 | Chapter 4
Basics of 3D in Crystal Space

g2d = CS_QUERY_REGISTRY (object_reg, iGraphics2D);


if (!g2d)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iGraphics2D plugin!");
return false;
}

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;
}

kbd = CS_QUERY_REGISTRY (object_reg, iKeyboardDriver);


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

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

txtmgr = g3d->GetTextureManager ();

// NEW ->
txtmgr->SetVerbose (true);

// Disable the lighting cache.


engine->SetLightingCacheMode (0);

if (!loader->LoadTexture ("butterflytexture", "/lib/butterfly/logo.jpg"))


{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Error loading 'butterflytexture' texture!");
return false;
}

iMaterialWrapper* tm = engine->GetMaterialList ()->FindByName


("butterflytexture");
Moving into 3D | 73
Basics of 3D in Crystal Space

room = engine->CreateSector ("room");

csRef<iMeshWrapper> walls(engine->CreateSectorWallsMesh (room, "walls"));


csRef<iThingState> walls_state (SCF_QUERY_INTERFACE (walls->GetMeshObject (),
iThingState));

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

p->SetTextureSpace (p->GetVertex (0), p->GetVertex (1), 2.5);

csRef<iStatLight> light;
iLightList* ll = room->GetLights ();

light = engine->CreateLight (NULL, csVector3 (-3, 5, 0), 10,


csColor (1, 1, 1), false);
ll->Add (light->QueryLight ());

light = engine->CreateLight (NULL, csVector3 (3, 5, 0), 10,


csColor (1, 0, 0), false);
ll->Add (light->QueryLight ());

light = engine->CreateLight (NULL, csVector3 (0, 5, -3), 10,


csColor (0, 0, 1), false);
ll->Add (light->QueryLight ());

engine->Prepare ();

view = csPtr<iView> (new csView (engine, g3d));


view->GetCamera ()->SetSector (room);
view->GetCamera ()->GetTransform ().SetOrigin (csVector3 (0, 5, -3));
view->SetRectangle (0, 0, g2d->GetWidth (), g2d->GetHeight ());

// <- NEW

// Load in our pixmaps (for 2D)


LoadPixMaps();

font = g2d->GetFontServer()->LoadFont(CSFONT_LARGE);

return true;
}

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

int main (int argc, char* argv[])


{
iObjectRegistry* object_reg = csInitializer::CreateEnvironment (argc, argv);

butterfly = new Butterfly (object_reg);


if(butterfly->Initialize ())
butterfly->Start ();
delete butterfly;

csInitializer::DestroyApplication (object_reg);
return 0;
}
Moving into 3D | 75
Basics of 3D in Crystal Space

Listing 4-2: butterfly3d1.h


#ifndef __BUTTERFLY_H__
#define __BUTTERFLY_H__

#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;

static bool SimpleEventHandler (iEvent& ev);


bool HandleEvent (iEvent& ev);
void SetupFrame ();
void FinishFrame ();

bool LoadPixMaps ();


void DrawFrame2D ();
public:
Butterfly(iObjectRegistry* object_reg);
~Butterfly();

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:

Figure 4-1: The Butterfly Room

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

csRef<iSector> room; // NEW

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;
}

Now that we have loaded the texture, we then obtain a pointer to an


iMaterialWrapper interface by finding the material using the name we specified
when we loaded it. Note that we will need the iMaterialWrapper when we set the
texture for the walls in a moment. Here is the line of code required to get the
pointer:
iMaterialWrapper* tm = engine->GetMaterialList()->FindByName("butterflytexture");

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

Next, we need to obtain a reference to an iThingState interface so that we can


actually define the geometry for our walls. We get this by using the
SCF_QUERY_INTERFACE macro, passing in a pointer to the walls
iMeshObject and a reference to the iThingState interface (structure). Once we
have the reference, we store it in a smart pointer called walls_state. This can all
be seen in the following line of code:
csRef<iThingState> walls_state (SCF_QUERY_INTERFACE (walls->GetMeshObject (),
iThingState));

Next, we declare a pointer to an iPolygon3D interface, which we can use to cre-


ate generic polygons of any amount of vertices. The declaration can be seen
here:
iPolygon3D* p;

Then we create a new polygon object by calling the CreatePolygon method of


the iThingState interface, which we have a reference to in our walls_state
variable:
p = walls_state->CreatePolygon ();

Next, we assign our butterflytexture material by calling the SetMaterial method


on our iPolygon3D reference p, passing in the iMaterialWrapper reference tm
that we created a minute ago. This can be seen here:
p->SetMaterial (tm);

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);

So what we do is call the SetTextureSpace method of the iPolygon3D interface,


called p. As you can see, it takes three parameters. The first is a csVector3 struc-
ture that represents the origin of the texture coordinates. Bear with me; this will
make sense in a second. The second parameter is also a csVector3, which repre-
sents the other end of the u-axis. Just to make this clear, take a look at the
following diagram of a rectangular polygon.
Moving into 3D | 79
Basics of 3D in Crystal Space

Figure 4-2: A polygon

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.

Figure 4-3: The u-axis

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:

Figure 4-4: u and v axes


80 | Chapter 4
Basics of 3D in Crystal Space

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.

Figure 4-5: This shows the texture tiled four times.

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

Figure 4-6: This shows the texture tiled 10 times.

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.

Figure 4-7: One single texture stretched


82 | Chapter 4
Basics of 3D in Crystal Space

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 ());

This result of this can be seen in the following screen shot.

Figure 4-8: The 2D clipping rectangle

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.

Quake 2 Model Example

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

// The global pointer to our application...


Butterfly *butterfly;

Butterfly::Butterfly (iObjectRegistry* object_reg)


{
Butterfly::object_reg = object_reg;

isWalking = false;
}

Butterfly::~Butterfly ()
{
}

bool Butterfly::LoadPixMaps ()
{

csRef<iImage> ifile = loader->LoadImage ("/lib/butterfly/logo.jpg");


csRef<iTextureHandle> txt = txtmgr->RegisterTexture (ifile, CS_TEXTURE_2D);
txt->Prepare ();

logoImg = new csSimplePixmap (txt);

return true;
}

void Butterfly::SetupFrame ()
{
// Check input...
Moving into 3D | 87
Quake 2 Model Example

// Get elapsed time from the virtual clock.


csTicks elapsed_time = vc->GetElapsedTicks ();

// Rotate the camera according to keyboard state


float speed = (elapsed_time / 1000.0) * (0.03 * 20);

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();
}

// Point the camera at the player...


view->GetCamera()->GetTransform().LookAt(player->GetMovable()->
GetPosition()+csVector3(0, 1, 0)-view->GetCamera()->
GetTransform ().GetOrigin(), csVector3(0, 1, 0));
88 | Chapter 4
Quake 2 Model Example

// <- NEW

// Tell 3D driver we're going to display 3D things...


if (!g3d->BeginDraw(engine->GetBeginDrawFlags() | CSDRAW_3DGRAPHICS))
return;

// Render the world...


view->Draw ();

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);
}

bool Butterfly::HandleEvent(iEvent& ev)


{
if (ev.Type == csevBroadcast && ev.Command.Code == cscmdProcess)
{
butterfly->SetupFrame ();
return true;
}
else if (ev.Type == csevBroadcast && ev.Command.Code == cscmdFinalProcess)
{
Moving into 3D | 89
Quake 2 Model Example

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::SimpleEventHandler (iEvent& ev)


{
return butterfly->HandleEvent (ev);
}

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;
}

if (!csInitializer::SetupEventHandler (object_reg, SimpleEventHandler))


{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Can't initialize event handler!");
return false;
}

// Check for commandline help.


if (csCommandLineHelper::CheckHelp (object_reg))
{
csCommandLineHelper::Help (object_reg);
return false;
}

// The virtual clock.


90 | Chapter 4
Quake 2 Model Example

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;
}

// Find the pointer to engine plug-in


engine = CS_QUERY_REGISTRY (object_reg, iEngine);
if (engine == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iEngine plugin!");
return false;
}

loader = CS_QUERY_REGISTRY (object_reg, iLoader);


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

g2d = CS_QUERY_REGISTRY (object_reg, iGraphics2D);


if (!g2d)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iGraphics2D plugin!");
return false;
}

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;
}

kbd = CS_QUERY_REGISTRY (object_reg, iKeyboardDriver);


if (kbd == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iKeyboardDriver plugin!");
return false;
}
Moving into 3D | 91
Quake 2 Model Example

// 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);

// Disable the lighting cache.


engine->SetLightingCacheMode (0);

if (!loader->LoadTexture ("butterflytexture", "/lib/butterfly/logo.jpg"))


{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.simple",
"Error loading 'stone4' texture!");
return false;
}

iMaterialWrapper* tm = engine->GetMaterialList ()->FindByName


("butterflytexture");

room = engine->CreateSector ("room");

csRef<iMeshWrapper> walls(engine->CreateSectorWallsMesh (room, "walls"));


csRef<iThingState> walls_state (SCF_QUERY_INTERFACE (walls->GetMeshObject
(), iThingState));

// 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 ();

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 ());

// <- MODIFIED

// NEW ->

iTextureWrapper* txt = loader->LoadTexture ("skin", "/lib/marine/brownie.png",


Moving into 3D | 93
Quake 2 Model Example

CS_TEXTURE_3D, txtmgr, true);


if (txt == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Error loading texture!");
return false;
}

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;
}

// Create the sprite and add it to the engine.


player = (engine->CreateMeshWrapper(imeshfact, "PlayerSprite", room,
csVector3 (-3, 3, 3)));

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);

playerstate = (SCF_QUERY_INTERFACE (player->GetMeshObject(), iSprite3DState));


playerstate->SetAction ("stand");
player->DeferUpdateLighting (CS_NLIGHT_STATIC|CS_NLIGHT_DYNAMIC, 10);

// <- NEW

engine->Prepare ();

view = csPtr<iView> (new csView (engine, g3d));


view->GetCamera ()->SetSector (room);
view->GetCamera ()->GetTransform ().SetOrigin (csVector3 (-3, 5, -3));
view->SetRectangle (0, 0, g2d->GetWidth (), g2d->GetHeight ());

// Load in our pixmaps (for 2D)


LoadPixMaps();

font = g2d->GetFontServer()->LoadFont(CSFONT_LARGE);

return true;
}
94 | Chapter 4
Quake 2 Model Example

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

int main (int argc, char* argv[])


{
iObjectRegistry* object_reg = csInitializer::CreateEnvironment (argc, argv);

butterfly = new Butterfly (object_reg);


if(butterfly->Initialize ())
butterfly->Start ();
delete butterfly;

csInitializer::DestroyApplication (object_reg);
return 0;
}

Listing 4-4: butterfly3d2.h


#ifndef __BUTTERFLY_H__
#define __BUTTERFLY_H__

#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

static bool SimpleEventHandler (iEvent& ev);


bool HandleEvent (iEvent& ev);
void SetupFrame ();
void FinishFrame ();

bool LoadPixMaps ();

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.

Figure 4-9: Quake 2 model example – screen shot 1

Team-Fly®
96 | Chapter 4
Quake 2 Model Example

Figure 4-10: Quake 2 model example – screen shot 2

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

For this model, the list of files should look as follows:

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

Figure 4-12: The model’s skin texture

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

You should then see something similar to the following:

Figure 4-13: The mdl2spr tool

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.

Figure 4-14: The cs2xml tool


100 | Chapter 4
Quake 2 Model Example

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.

Figure 4-15: tris.spr in XML format

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"

Next, we have added three global variables to our application:


csRef<iMeshWrapper> player;
csRef<iSprite3DState> playerstate;
bool isWalking = false;

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 ());

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 ());

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

/lib/marine/brownie.PNG, as this is where it is located in the VFS. Then we


specify CS_TEXTURE_3D to indicate that this will be a texture used for 3D
mapping. We pass in a pointer to the texture manager, which in this example is
txtmgr. Then finally we specify true, which indicates to the method that we wish
the texture to be registered with the texture manager so that it can be referenced
by name later.
As you can see, the LoadTexture method returns a pointer to an
iTextureWrapper interface, so we then check to see that the texture was loaded
successfully by testing if the return value is a valid pointer or not, using the
following:
if(txt == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Error loading texture!");
return false;
}

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;

csReversibleTransform l_rT=csReversibleTransform ();


l_rT.SetT2O ( m );
l_rT.SetOrigin ( csVector3(0, 0, 0) );
imeshfact->HardTransform (l_rT);

Next, we obtain a reference to the iSprite3Dstate interface associated with our


mesh by querying the mesh object using the following line of code:
playerstate = (SCF_QUERY_INTERFACE(player->GetMeshObject(), iSprite3DState));

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));

Argh. J First we obtain a reference to the camera by means of the GetCamera


method. Then we get its current transformation by calling GetTransform and call
the LookAt method, which takes two parameters. The first parameter is the x, y,
z position it should be pointing at, specified as a csVector3, and the second is
simply the up vector.
To find the position we wish the camera to look at, we first get the position of
the player using the following code:
player->GetMovable()->GetPosition()

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

Adding Simple Collision Detection


You’re probably pleased to hear that there is absolutely no math required in this
section, which is pretty unusual for anything related to collision detection.
Actually there is math involved, but it’s hidden from us; we just get a Boolean
value from the engine to tell us if objects have collided or not.
The only collision detection system implemented in Crystal Space is called
RAPID and it works very well. The only limitation it has is that it does not han-
dle sprites that change size, or scale. Of course, you can adjust the size of your
sprite first, but after your application is running, this is not possible (without an
expensive operation to rescale the actual model).
As usual, we are going to look at a working example first. This example is an
extension of the previous one; however, in this example the player will not be
able to play God and walk through walls.
Let’s look at the complete source code for the simple collision detection
example.
Listing 4-5: butterfly3d3.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 "cstool/collider.h" // NEW
#include "butterfly3d3.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"
#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"
#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"
Moving into 3D | 107
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

// The global pointer to our application...


Butterfly *butterfly;

Butterfly::Butterfly (iObjectRegistry* object_reg)


{
Butterfly::object_reg = object_reg;

isWalking = false;

playerCollider = NULL; // NEW


mapCollider = NULL; // NEW
}

Butterfly::~Butterfly ()
{
}

bool Butterfly::LoadPixMaps ()
{

csRef<iImage> ifile = loader->LoadImage ("/lib/butterfly/logo.jpg");


csRef<iTextureHandle> txt = txtmgr->RegisterTexture (ifile, CS_TEXTURE_2D);
txt->Prepare ();

logoImg = new csSimplePixmap (txt);

return true;
}

void Butterfly::SetupFrame ()
{
// NEW ->

// Store the current transformations (before any movement, etc.)


csReversibleTransform oldPlayerTrans = player->GetMovable()->GetTransform();

// <- NEW
108 | Chapter 4
Adding Simple Collision Detection

// Check input...
// Get elapsed time from the virtual clock.
csTicks elapsed_time = vc->GetElapsedTicks ();

// Rotate the camera according to keyboard state


float speed = (elapsed_time / 1000.0) * (0.03 * 20);

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 ->

// 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");
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();
}

// Point the camera at the player...


view->GetCamera()->GetTransform().LookAt(player->GetMovable()->
GetPosition()+csVector3(0, 1, 0)-view->GetCamera()->GetTransform
().GetOrigin(), csVector3(0, 1, 0));

// Tell 3D driver we're going to display 3D things...


if (!g3d->BeginDraw(engine->GetBeginDrawFlags() | CSDRAW_3DGRAPHICS))
return;

// Render the world...


view->Draw ();

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);
}

bool Butterfly::HandleEvent(iEvent& ev)


{
if (ev.Type == csevBroadcast && ev.Command.Code == cscmdProcess)
{
butterfly->SetupFrame ();
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::SimpleEventHandler (iEvent& ev)


{
return butterfly->HandleEvent (ev);
}

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

if (!csInitializer::SetupEventHandler (object_reg, SimpleEventHandler))


{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Can't initialize event handler!");
return false;
}

// Check for commandline help.


if (csCommandLineHelper::CheckHelp (object_reg))
{
csCommandLineHelper::Help (object_reg);
return false;
}

// The virtual clock.


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

// Find the pointer to engine plug-in


engine = CS_QUERY_REGISTRY (object_reg, iEngine);
if (engine == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iEngine plugin!");
return false;
}

loader = CS_QUERY_REGISTRY (object_reg, iLoader);


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

g2d = CS_QUERY_REGISTRY (object_reg, iGraphics2D);


if (!g2d)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iGraphics2D plugin!");
return false;
}
112 | Chapter 4
Adding Simple Collision Detection

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;
}

kbd = CS_QUERY_REGISTRY (object_reg, iKeyboardDriver);


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

// NEW ->

// The collision detection system.


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;
}

// <- 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);

// Disable the lighting cache.


engine->SetLightingCacheMode (0);

if (!loader->LoadTexture ("butterflytexture", "/lib/butterfly/logo.jpg"))


{
Moving into 3D | 113
Adding Simple Collision Detection

csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,


"crystalspace.application.simple",
"Error loading 'butterflytexture' texture!");
return false;
}

iMaterialWrapper* tm =
engine->GetMaterialList ()->FindByName ("butterflytexture");

room = engine->CreateSector ("room");

walls = (engine->CreateSectorWallsMesh (room, "walls")); // MODIFIED


csRef<iThingState> walls_state (SCF_QUERY_INTERFACE (walls->GetMeshObject (),
iThingState));

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->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 ();

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;
}

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;
}

// Create the sprite and add it to the engine.


player = (engine->CreateMeshWrapper(imeshfact, "PlayerSprite", room,
csVector3 (-3, 3.1, 3)));

csMatrix3 m;
m.Identity ();
Moving into 3D | 115
Adding Simple Collision Detection

m *= 5.0;

csReversibleTransform l_rT=csReversibleTransform ();


l_rT.SetT2O (m);
l_rT.SetOrigin (csVector3(0, 0, 0));
imeshfact->HardTransform (l_rT);

playerstate = (SCF_QUERY_INTERFACE (player->GetMeshObject(), iSprite3DState));


playerstate->SetAction("stand");
player->DeferUpdateLighting (CS_NLIGHT_STATIC|CS_NLIGHT_DYNAMIC, 10);

// 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 ();

view = csPtr<iView> (new csView (engine, g3d));


view->GetCamera ()->SetSector (room);
view->GetCamera ()->GetTransform ().SetOrigin (csVector3 (-3, 5, -3));
view->SetRectangle (0, 0, g2d->GetWidth (), g2d->GetHeight ());

// Load in our pixmaps (for 2D)


LoadPixMaps();

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

int main (int argc, char* argv[])


{
iObjectRegistry* object_reg = csInitializer::CreateEnvironment (argc, argv);

butterfly = new Butterfly (object_reg);


if(butterfly->Initialize ())
butterfly->Start ();
delete butterfly;

csInitializer::DestroyApplication (object_reg);
return 0;
}

Listing 4-6: butterfly3d3.h


#ifndef __BUTTERFLY_H__
#define __BUTTERFLY_H__

#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;

iCollider* playerCollider; // NEW


iCollider* mapCollider; // NEW
csRef<iMeshWrapper> walls; // NEW

static bool SimpleEventHandler (iEvent& ev);


bool HandleEvent (iEvent& ev);
void SetupFrame ();
void FinishFrame ();

bool LoadPixMaps ();


void DrawFrame2D ();

iCollider* CreateCollider (iMeshWrapper* mesh); // NEW


public:
Butterfly(iObjectRegistry* object_reg);
~Butterfly();

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)

This makes the complete RequestPlugins call look as follows:


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 | 119
Adding Simple Collision Detection

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));

Then we check whether it is a valid pointer (i.e., not NULL).


if(polmesh)
{

If it is valid, we create a new csColliderWrapper object by passing in a call to


QueryObject on the mesh object (which returns it as a pointer to an iObject
interface). Then we pass in the collision detection system, cdsys, followed by
the iPolygonMesh pointer we just retrieved, called polmesh. This can all be seen
in the following line of code:
120 | Chapter 4
Adding Simple Collision Detection

csColliderWrapper* wrap = new csColliderWrapper(mesh->QueryObject (), cdsys,


polmesh);

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();

If the retrieval of a pointer to an iPolygonMesh failed, then we simply display


this to the screen and return NULL, as shown in the following code. This con-
cludes the CreateCollider method.
else
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.simpcd",
"Object doesn't support collision detection!");
return NULL;
}

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

In this method, we first pass in playerCollider, followed by the player’s full


transformation. Then we pass mapCollider and the full transformation of the
map (the walls).
If a collision occurred (i.e., collision == true), we simply set the transform of
the player back to how it was before we moved it and then call UpdateMove.
player->GetMovable()->SetTransform(oldPlayerTrans);
player->GetMovable()->UpdateMove();

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.

Creating and Loading a Map


In this final section, we will look at how to create a map using the Valve Ham-
mer Editor and then load it into Crystal Space. Once it has been loaded in, we
will be able to move our player about and use collision detection as well.

Creating the World


The best place to start is to download the map editor so that we can make our
map. The editor is called the Valve Hammer Editor (previously called
Worldcraft) and can be downloaded from the following link.

' http://www.valve-erc.com/content/?page=utilities

Once downloaded, install it to the


following directory.
c:\crystal\hammer

Next we need to configure it. So


run the editor (via the Start menu)
and you should be presented with
a configuration window. If not,
select Tools, then Options from the
main menu.
Once the Configure window is
visible, ensure that the General,
2D views, and 3D views tabs
match the following three screen
shots.

Figure 4-16: The General tab


122 | Chapter 4
Creating and Loading a Map

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

Here are the links for the two files:

' 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

Figure 4-19: The Textures tab

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.

Figure 4-20: Edit Game Configurations 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.

Figure 4-21: Game Configurations once


completed

That is all we need to do here, so now click OK on the Configure window.


We are now ready to create our first map, but before we do that let’s create a
folder to store our maps in:
c:\crystal\maps

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

Figure 4-22: The main interface

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

Figure 4-23: Creating our room — part 1

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.

Figure 4-24: Creating our room — part 2


Moving into 3D | 127
Creating and Loading a Map

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.

Figure 4-25: Creating our room — part 3

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

Figure 4-26: The room hollowed out

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

Figure 4-27: A room with a view (or should I say box J)

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.

Figure 4-28: Selecting the light entity

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

Figure 4-29: Placing the light

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.

Figure 4-30: Object Properties window


Moving into 3D | 131
Creating and Loading a Map

Select the Brightness attribute and you will then see the following:

Figure 4-31: Adjusting the color and radius

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

Figure 4-32: Selecting the floor

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.

Saving and Converting the World


Our simple map is now complete so next we need to export it in a format that
the Crystal Space tools will understand. Select File from the main menu, then
the Export to .MAP option. Now save the map in the c:\crystal\maps folder we
created earlier with the name testmap.map.
Once we have our map saved there, we need to use the tools supplied with
Crystal Space to convert it into a suitable format for loading. To do this, go to
the command prompt and change the directory to the c:\crystal folder.
To convert our map from the .map format into the Crystal Space format, we
use the map2cs tool. So ensuring we are in the c:\crystal folder, execute the fol-
lowing command:
cs\map2cs maps\testmap.map cs\data\testmap.zip
Moving into 3D | 133
Creating and Loading a Map

When you do this, the screen should look similar to the following:

Figure 4-33: Converting the map

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).

Figure 4-34: Deleting the <moveable /> tag


134 | Chapter 4
Creating and Loading a Map

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

Loading the World into Crystal Space


Now that we have our map, let’s see how we load it into Crystal Space. We are
going to be expanding the previous example, so we will be able to move our
player about and collide with the world; however, this does require some extra
code. Let’s first look at the complete source listing for this example and then we
will look at the changes and additions in more detail.
Listing 4-7: butterfly3d4.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 "cstool/collider.h"
#include "csutil/ptrarr.h" // NEW
#include "butterfly3d4.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"
#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"
#include "ivideo/graph3d.h"
#include "ivideo/graph2d.h"
#include "ivideo/txtmgr.h"
#include "ivideo/texture.h"
Moving into 3D | 135
Creating and Loading a Map

#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::Butterfly (iObjectRegistry* object_reg)


{
TE

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");

// Load the level file which is called 'world'.


if (!loader->LoadMapFile ("world"))
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Couldn't load level!");
return false;
}
engine->Prepare ();

Team-Fly®
136 | Chapter 4
Creating and Loading a Map

return true;
}
// <- NEW

bool Butterfly::LoadPixMaps ()
{

csRef<iImage> ifile = loader->LoadImage ("/lib/butterfly/logo.jpg");


csRef<iTextureHandle> txt = txtmgr->RegisterTexture (ifile, CS_TEXTURE_2D);
txt->Prepare ();

logoImg = new csSimplePixmap (txt);

return true;
}

void Butterfly::SetupFrame ()
{

// Store the current transformations (before any movement, etc.)


csReversibleTransform oldPlayerTrans = player->GetMovable()->GetTransform();

// Check input...
// Get elapsed time from the virtual clock.
csTicks elapsed_time = vc->GetElapsedTicks ();

// Rotate the camera according to keyboard state


float speed = (elapsed_time / 1000.0) * (0.03 * 20);

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();

// check for player/wall collisions...

csReversibleTransform ft1 = player->GetMovable


()->GetFullTransform ();

csRef<iMeshList> ml (engine->GetMeshes());

for(int i=0; i<mapColliderArray.Length(); i++)


{
cdsys->ResetCollisionPairs ();
csReversibleTransform ft2 = ml->Get(i)->GetMovable
()->GetFullTransform ();
bool collision = cdsys->Collide(playerCollider, &ft1,
mapColliderArray.Get(i), &ft2);
if (collision)
{
// Restore old transforms and reverse turning
// directions.
player->GetMovable ()->SetTransform
(oldPlayerTrans);
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();
}

// Point the camera at the player...


view->GetCamera()->GetTransform().LookAt(player->GetMovable()->
GetPosition()+csVector3(0, 1, 0)-view->GetCamera()->GetTransform
().GetOrigin(), csVector3(0, 1, 0));
138 | Chapter 4
Creating and Loading a Map

// NEW ->
iLight *lights;
int lightcount = engine->GetNearbyLights (room, player->GetMovable()->
GetPosition(), CS_NLIGHT_STATIC, &lights, 20);
player->UpdateLighting(&lights, lightcount);
// <- NEW

// Tell 3D driver we're going to display 3D things...


if (!g3d->BeginDraw(engine->GetBeginDrawFlags() | CSDRAW_3DGRAPHICS))
return;

// Render the world...


view->Draw ();

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);
}

bool Butterfly::HandleEvent(iEvent& ev)


{
if (ev.Type == csevBroadcast && ev.Command.Code == cscmdProcess)
{
butterfly->SetupFrame ();
Moving into 3D | 139
Creating and Loading a Map

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::SimpleEventHandler (iEvent& ev)


{
return butterfly->HandleEvent (ev);
}

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;
}

if (!csInitializer::SetupEventHandler (object_reg, SimpleEventHandler))


{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Can't initialize event handler!");
return false;
}

// Check for commandline help.


if (csCommandLineHelper::CheckHelp (object_reg))
140 | Chapter 4
Creating and Loading a Map

{
csCommandLineHelper::Help (object_reg);
return false;
}

// The virtual clock.


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;
}

// Find the pointer to engine plug-in


engine = CS_QUERY_REGISTRY (object_reg, iEngine);
if (engine == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iEngine plugin!");
return false;
}

loader = CS_QUERY_REGISTRY (object_reg, iLoader);


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

g2d = CS_QUERY_REGISTRY (object_reg, iGraphics2D);


if (!g2d)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iGraphics2D plugin!");
return false;
}

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;
}

kbd = CS_QUERY_REGISTRY (object_reg, iKeyboardDriver);


if (kbd == NULL)
{
Moving into 3D | 141
Creating and Loading a Map

csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,


"crystalspace.application.butterfly",
"No iKeyboardDriver plugin!");
return false;
}

// The collision detection system.


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;
}

// 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);

// Disable the lighting cache.


engine->SetLightingCacheMode (0);

if (!loader->LoadTexture ("butterflytexture", "/lib/butterfly/logo.jpg"))


{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.simple",
"Error loading 'butterflytexture' texture!");
return false;
}

iMaterialWrapper* tm =
engine->GetMaterialList ()->FindByName ("butterflytexture");

// NEW ->

view = csPtr<iView> (new csView (engine, g3d));


view->SetRectangle (0, 0, g2d->GetWidth (), g2d->GetHeight ());
142 | Chapter 4
Creating and Loading a Map

LoadMap();

room = engine->GetSectors ()->FindByName ("room");


view->GetCamera ()->SetSector (room);
view->GetCamera ()->GetTransform ().SetOrigin (csVector3 (-3, 5, -3));

// Create colliders for all meshes in the map...


csRef<iMeshList> meshlist (engine->GetMeshes());

for(int i=0; i < meshlist->GetCount(); i++)


{
csRef<iMeshWrapper> mw (meshlist->Get(i));
mapColliderArray.Push(CreateCollider(mw));
}

// <- NEW

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;
}

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;
}

// Create the sprite and add it to the engine.


player = (engine->CreateMeshWrapper(imeshfact, "PlayerSprite", room,
csVector3 (0, 3.2, 0)));

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);

playerstate = (SCF_QUERY_INTERFACE (player->GetMeshObject(), iSprite3DState));


playerstate->SetAction("stand");
Moving into 3D | 143
Creating and Loading a Map

player->DeferUpdateLighting (CS_NLIGHT_STATIC|CS_NLIGHT_DYNAMIC, 10);

playerCollider = CreateCollider(player);
if (playerCollider == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Error creating playerCollider!");
return false;
}

// Load in our pixmaps (for 2D)


LoadPixMaps();

font = g2d->GetFontServer()->LoadFont(CSFONT_LARGE);

return true;
}

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

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;
}
}

int main (int argc, char* argv[])


{
iObjectRegistry* object_reg = csInitializer::CreateEnvironment (argc, argv);
144 | Chapter 4
Creating and Loading a Map

butterfly = new Butterfly (object_reg);


if(butterfly->Initialize ())
butterfly->Start ();
delete butterfly;

csInitializer::DestroyApplication (object_reg);
return 0;
}

Listing 4-8: butterfly3d4.h


#ifndef __BUTTERFLY_H__
#define __BUTTERFLY_H__

#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

csPArray<iCollider> mapColliderArray; // NEW

static bool SimpleEventHandler (iEvent& ev);


bool HandleEvent (iEvent& ev);
void SetupFrame ();
void FinishFrame ();

bool LoadPixMaps ();


void DrawFrame2D ();

iCollider* CreateCollider (iMeshWrapper* mesh);


bool Butterfly::LoadMap(); // NEW

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.

Figure 4-35: Our map working in Crystal Space

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

Integrating the OMS with


an Existing Application

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.

Obtaining the Latest OMS Client Libraries


Before we can start integrating, we first need to obtain the latest client libraries
from the Butterfly Lab. First, go to the main Butterfly Lab web site at
http://www.lab.com and log in using your email address and the password you
were assigned when you signed up.
Once logged in, click on the Projects tab, located at the top left of the web
site.

Figure 5-1: The Projects tab

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.

Figure 5-2: File sharing in the project

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.

Figure 5-3: Extracting the SDK

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.

Integrating the OMS without the Wrapper


The application we are going to integrate the OMS with is the DemoSky exam-
ple, located within the Crystal Space SDK. However, rather than edit the
example directly, we are going to leave it intact, copying its source and header
file into a new project, which we are going to call appdemoskyOMS.
Create this new project now, just as we have done in the previous two chap-
ters, and then proceed by copying the files demosky.cpp and demosky.h (located
in c:\crystal\CS\apps\demosky) to our new project folder, which should be called
something similar to:
c:\crystal\CS\apps\demoskyOMS

Right-click on the appdemoskyOMS project you just created in Visual Studio


(on the left-hand panel), then select the Add Files to Project option from the
pop-up menu. Next, select the two files you copied into the c:\crystal\CS\
apps\demoskyOMS folder, as shown in Figure 5-4, and click OK.

Figure 5-4: Adding the existing files to our new project

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.

Figure 5-5: Standard DemoSky application

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

This library is free software; you can redistribute it and/or


modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,


but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Integrating the OMS with an Existing Application | 153
Integrating the OMS without the Wrapper

Library General Public License for more details.

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.

demosky - This application shows a (slow) way to produce a sky using


procedural textures and some fractal algorithms

09/05/2003 - Modified for Butterfly.net Example


*/

#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

#define X_OFFSET 0.0f


#define Y_OFFSET 0.0f
#define Z_OFFSET 000.0f

#define X_SCALE 1.0f


#define Y_SCALE 1.0f
#define Z_SCALE 1.0f

// End

CS_IMPLEMENT_APPLICATION

// the global system driver variable


DemoSky *System;

void DemoSky::Report (int severity, const char* msg, ...)


{
va_list arg;
va_start (arg, msg);
csRef<iReporter> rep (CS_QUERY_REGISTRY (System->object_reg, iReporter));
if (rep)
rep->ReportV (severity, "crystalspace.application.demosky", msg, arg);
else
{
csPrintfV (msg, arg);
csPrintf ("\n");
}
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;
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);
}

void DemoSky::SetTexSpace(csProcSkyTexture *skytex, iPolygon3D *poly,


int size, const csVector3& orig, const csVector3& upt, float ulen,
const csVector3& vpt, float vlen)
{
csVector3 texorig = orig;
csVector3 texu = upt;
float texulen = ulen;
csVector3 texv = vpt;
float texvlen = vlen;
// copied, now adjust
csVector3 uvector = upt - orig;
csVector3 vvector = vpt - orig;
// to have 1 pixel going over the edges.
texorig -= uvector / float(size);
texorig -= vvector / float(size);
texu += uvector / float(size);
texv += vvector / float(size);
texulen += ulen * 2.0f / float(size);
texvlen += vlen * 2.0f / float(size);
poly->SetTextureSpace (texorig, texu, texulen, texv, texvlen);
skytex->SetTextureSpace(texorig, texu-texorig, texv-texorig);
}

static bool DemoSkyEventHandler (iEvent& ev)


{
if (ev.Type == csevBroadcast && ev.Command.Code == cscmdProcess)
{
System->SetupFrame ();
return true;

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;
}
}

bool DemoSky::Initialize (int argc, const char* const argv[],


const char *iConfigName)
{
object_reg = csInitializer::CreateEnvironment (argc, argv);
if (!object_reg) return false;

if (!csInitializer::SetupConfigManager (object_reg, iConfigName))


{
Report (CS_REPORTER_SEVERITY_ERROR, "Couldn't initialize app!");
return false;
}

if (!csInitializer::RequestPlugins (object_reg,
CS_REQUEST_VFS,
CS_REQUEST_SOFTWARE3D,
CS_REQUEST_ENGINE,
CS_REQUEST_FONTSERVER,
CS_REQUEST_IMAGELOADER,
CS_REQUEST_LEVELLOADER,
CS_REQUEST_CONSOLEOUT,
CS_REQUEST_END))
{
Report (CS_REPORTER_SEVERITY_ERROR, "Couldn't init app!");
return false;
}

if (!csInitializer::SetupEventHandler (object_reg, DemoSkyEventHandler))


{
Report (CS_REPORTER_SEVERITY_ERROR, "Couldn't init app!");
return false;
}

// Check for commandline help.


if (csCommandLineHelper::CheckHelp (object_reg))
{
csCommandLineHelper::Help (object_reg);
exit (0);
}

// The virtual clock.


vc = CS_QUERY_REGISTRY (object_reg, iVirtualClock);

// Find the pointer to engine plug-in


Integrating the OMS with an Existing Application | 157
Integrating the OMS without the Wrapper

engine = CS_QUERY_REGISTRY (object_reg, iEngine);


if (!engine)
{
Report (CS_REPORTER_SEVERITY_ERROR, "No iEngine plugin!");
exit (-1);
}

LevelLoader = CS_QUERY_REGISTRY (object_reg, iLoader);


if (!LevelLoader)
{
Report (CS_REPORTER_SEVERITY_ERROR, "No iLoader plugin!");
exit (-1);
}

myG3D = CS_QUERY_REGISTRY (object_reg, iGraphics3D);


if (!myG3D)
{
Report (CS_REPORTER_SEVERITY_ERROR, "No iGraphics3D plugin!");
exit (-1);
}

myG2D = CS_QUERY_REGISTRY (object_reg, iGraphics2D);


if (!myG2D)
{
Report (CS_REPORTER_SEVERITY_ERROR, "No iGraphics2D plugin!");
exit (-1);
}

kbd = CS_QUERY_REGISTRY (object_reg, iKeyboardDriver);


if (!kbd)
{
Report (CS_REPORTER_SEVERITY_ERROR, "No iKeyboardDriver!");
exit (-1);
}

// 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);
}

// Set up the texture manager


iTextureManager* txtmgr = myG3D->GetTextureManager ();
txtmgr->SetVerbose (true);

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);

// Create our world.


Report (CS_REPORTER_SEVERITY_NOTIFY, "Creating world!...");

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");

room = engine->CreateSector ("room");


csRef<iMeshWrapper> walls (engine->CreateSectorWallsMesh (room, "walls"));
csRef<iThingState> walls_state (SCF_QUERY_INTERFACE (walls->GetMeshObject (),
iThingState));
iPolygon3D* p;
p = walls_state->CreatePolygon ();
p->SetMaterial (imatd);
float size = 500.0; // size of the skybox — around 0,0,0 for now.
float simi = size; //*255./256.; // sizeminor
p->CreateVertex (csVector3 (-size, -simi, size));
p->CreateVertex (csVector3 (size, -simi, size));
p->CreateVertex (csVector3 (size, -simi, -size));
p->CreateVertex (csVector3 (-size, -simi, -size));

SetTexSpace (sky_d, p, 256, p->GetVertex (0), p->GetVertex (1), 2.0f * size,


p->GetVertex (3), 2.0f * size);
p->GetFlags ().Set(CS_POLY_LIGHTING, 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));

SetTexSpace (sky_u, p, 256, p->GetVertex (0), p->GetVertex (1), 2.0f * size,


p->GetVertex (3), 2.0f * size);
Integrating the OMS with an Existing Application | 159
Integrating the OMS without the Wrapper

p->GetFlags ().Set(CS_POLY_LIGHTING, 0);

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));

SetTexSpace (sky_f, p, 256, p->GetVertex (0), p->GetVertex (1), 2.0f * size,


p->GetVertex (3), 2.0f * size);
p->GetFlags ().Set(CS_POLY_LIGHTING, 0);

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));

SetTexSpace (sky_r, p, 256, p->GetVertex (0), p->GetVertex (1), 2.0f * size,


p->GetVertex (3), 2.0f * size);
p->GetFlags ().Set(CS_POLY_LIGHTING, 0);

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));

SetTexSpace (sky_l, p, 256, p->GetVertex (0), p->GetVertex (1), 2.0f * size,


p->GetVertex (3), 2.0f * size);
p->GetFlags ().Set(CS_POLY_LIGHTING, 0);

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));

SetTexSpace (sky_b, p, 256, p->GetVertex (0), p->GetVertex (1), 2.0f * size,


p->GetVertex (3), 2.0f * size);
p->GetFlags ().Set(CS_POLY_LIGHTING, 0);

LevelLoader->LoadTexture ("seagull", "/lib/std/seagull.gif");


iMaterialWrapper *sg = engine->GetMaterialList ()->FindByName("seagull");

// Modified
// flock = new Flock(engine, 10, sg, room);
// End

// Added
160 | Chapter 5
Integrating the OMS without the Wrapper

// Load a texture for our sprite.


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;
}

// Load a sprite template from disk.


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;
}

char *pcServerIP = "Temporary ServerIP";


char *pcServerPort = "Temporary ServerPort";
char *pcUsername = "Temporary Username";
char *pcPassword = "Temporary Password";
char *pcNPSInPort = "8002";
char *pcNPSOutPort = "8002";

char pcBuffer[1024];
char *pcTempBuffer;
char *pcTemp;

FILE *pConfig = fopen("ServerInfo.cfg", "r");


if ( !pConfig )
pConfig = fopen("..\\ServerInfo.cfg", "r");

//** Read the config file


if ( pConfig )
{
fgets(pcBuffer, 1024, pConfig);

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

if ( !pcTempBuffer ) return TRUE;


pcTempBuffer[0] = 0;
pcUsername = pcTemp;

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);

GetOMS()->ServerLogin(GetUsername(), GetPassword(), 0); // Don't wait

// End

engine->Prepare ();

Report (CS_REPORTER_SEVERITY_NOTIFY, "--------------------------------------");

// csView is a view encapsulating both a camera and a clipper.


// You don't have to use csView as you can do the same by
// manually creating a camera and a clipper but it makes things a little easier.
162 | Chapter 5
Integrating the OMS without the Wrapper

view = csPtr<iView> (new csView (engine, myG3D));


view->GetCamera ()->SetSector (room);
view->GetCamera ()->GetTransform ().SetOrigin (csVector3 (0, 0, 0));
view->SetRectangle (0, 0, myG2D->GetWidth (), myG2D->GetHeight ());

// set the initial position to the origin...


FPOINT3 vPosition;
vPosition.x = 0;
vPosition.y = 0;
vPosition.z = 0;
FPOINT3 vOrientation = {0.0f, 0.0f, 0.0f};
m_pOMS->SetMotionByGUID(m_guidAvatar, vPosition, vOrientation);

return true;
}

void DemoSky::SetupFrame ()
{
// Added
Update();
// End

csTicks elapsed_time, current_time;


elapsed_time = vc->GetElapsedTicks ();
current_time = vc->GetCurrentTicks ();

// Modified
//flock->Update(elapsed_time);
// End

// Now rotate the camera according to keyboard state


float speed = (elapsed_time / 1000.0f) * (0.03f * 20.0f);

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);

// Tell 3D driver we're going to display 3D things.


if (!myG3D->BeginDraw (engine->GetBeginDrawFlags () | CSDRAW_3DGRAPHICS))
return;

view->Draw ();

// Start drawing 2D graphics.


Integrating the OMS with an Existing Application | 163
Integrating the OMS without the Wrapper

if (!myG3D->BeginDraw (CSDRAW_2DGRAPHICS)) return;

// 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
}

bool DemoSky::HandleEvent (iEvent &Event)


{
if ((Event.Type == csevKeyDown) && (Event.Key.Code == 't'))
{
// toggle animation
sky->SetAnimated(object_reg, !sky->GetAnimated(), csGetTicks ());
return true;
}

if ((Event.Type == csevKeyDown) && (Event.Key.Code == CSKEY_ESC))


{
csRef<iEventQueue> q (CS_QUERY_REGISTRY (object_reg, iEventQueue));
if (q)
q->GetEventOutlet()->Broadcast (cscmdQuit);
return true;
}

return false;
}

// CUT UNREQUIRED 'Flock' class


164 | Chapter 5
Integrating the OMS without the Wrapper

/*---------------------------------------------------------------------*
* Main function
*---------------------------------------------------------------------*/
int main (int argc, char* argv[])
{
srand (time (NULL));

// Create our main class.


System = new DemoSky ();

// 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);

m_pOMS->ServerLogout(2000); // Wait 2 seconds max

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;
}

void DemoSky::SetUsername(const char *pcUsername)


{
if ( pcUsername )

Y
{
m_pcUsername = new char[strlen(pcUsername) + 1];

}
FL
if ( m_pcUsername )
strcpy(m_pcUsername, pcUsername);
AM
}

void DemoSky::SetPassword(const char *pcPassword)


{
TE

if ( pcPassword )
{
m_pcPassword = new char[strlen(pcPassword) + 1];
if ( m_pcPassword )
strcpy(m_pcPassword, pcPassword);
}
}

bool DemoSky::CreateOMS(char *pcIP, char *pcPort, char *pcNPSInPort, char


*pcNPSOutPort)
{
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 ( 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);
if ( !m_pOMS )
{
MessageBox(NULL, "Out of memory", "Out of memory", MB_OK);

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;
}
}

// set up the Create CThing callback table


// CreateArray and ObjectArray are defined in CreateObjects
m_pOMS->SetupCreateThingTable(NUM_CLIENT_OBJECTS,
CreateArray, ObjectArray);
m_pOMS->HandleSets(OMS_EVENT_TRANSFER_TYPE_EVENT);
m_pOMS->HandleLogin(OMS_EVENT_TRANSFER_TYPE_EVENT);

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;

// Get the first event from the OMS


pEventInfo = m_pOMS->GetFirstEvent(&lNumEventsLeft);

// Process up to 10 events or enough to bring the remaining number


// down to 50. This attempts to keep the frame rate of the
// application steady by limiting the number of events processed each
// time. But, to make sure the application does not get too far behind
// it also keeps the list from growing above 50 events.
// Tweak these numbers as you see fit.
for ( iIndex = 0; bStillMore && ((iIndex < 10) || (lNumEventsLeft > 50));
iIndex++ )
{
// If this is the second time through the loop, get the next event
if (pEventInfo == NULL)
pEventInfo = m_pOMS->GetNextEvent(&lNumEventsLeft);

// Make sure there is an event


if (pEventInfo != NULL)
{
bBored = false;

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());

printf("Grid Sample (logged in as %s)\n",


GetUsername());
}
break;
case OMS_EVENT_LOGON_FAIL:
printf("\n\n\nLogon Failed!!!\n\n\n");
break;
case OMS_EVENT_IDENT_LIST_CHANGE:
{
UINT uCount = 0;
std::vector<BNIDENTITY> vIdentities;

if (GetOMS() && BN_SUCCESS(GetOMS()->


GetIdentities(vIdentities)))
{
uCount = vIdentities.size();
168 | Chapter 5
Integrating the OMS without the Wrapper

if (uCount > 0)
{
printf("Selecting first
ident\n");

GetOMS()->SelectIdentity(
vIdentities[0]);
}
}
}
break;
case OMS_EVENT_EMBODY_DONE:
printf("Embodied Avatar Successfully\n");

// Save the Avatar GUID


SetAvatar(pEventInfo->guidThing);

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];

sprintf(pcMessage, "%s asks: %s",


pEventInfo->pcUsername,
pEventInfo->pcMessage);
strcpy(pcAccept, pEventInfo->pcAccept ?
pEventInfo->pcAccept : "Yes");
strcpy(pcReject, pEventInfo->pcReject ?
pEventInfo->pcReject : "No");
ReceiveSecure(pEventInfo, pcMessage);
pEventInfo = NULL; // Set to NULL so it
// will not be deleted
}
break;
default:
ASSERT_ERROR(false, "OMS: Received unknown
message type\n");
break;
}
if ( pEventInfo )
delete pEventInfo;
}
else
bStillMore = false;

pEventInfo = NULL;
}
}

bool DemoSky::UpdateThing(BNGUID guidThing, bool bClientControlled)


{
170 | Chapter 5
Integrating the OMS without the Wrapper

bool bRetVal = false;


static std::vector<CThingAttributeValue> vAttributes;
UINT uAttrib;
char *pcTemp = NULL;

if ( !m_pOMS )
return false;

UpdateThingView(guidThing, bClientControlled);

// Update the non-client objects


if ( !bClientControlled )
{
// Try the new GetAttributes function
m_pOMS->GetStatesByGUID(guidThing, vAttributes);

for (uAttrib = 0; uAttrib < vAttributes.size(); uAttrib++)


{
switch ( vAttributes[uAttrib].m_idState )
{
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 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;
}

// Must delete the returned memory


if (( vAttributes[uAttrib].m_Attribute.Type == PROPERTY_STRING )
|| ( vAttributes[uAttrib].m_Attribute.Type ==
Integrating the OMS with an Existing Application | 171
Integrating the OMS without the Wrapper

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;
}

// Must delete the returned memory


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;
}
}
}
else
{
// Try the new GetAttributes function
m_pOMS->GetStatesByGUID(guidThing, vAttributes);

for (uAttrib = 0; uAttrib < vAttributes.size(); uAttrib++)


{
switch ( vAttributes[uAttrib].m_idState )
{
case BN_ATTRIB_IDENTITY:
bRetVal = SetThingIdentity(guidThing,
vAttributes[uAttrib].m_Attribute.Value.lLong);
break;
}

// Must delete the returned memory


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;
172 | Chapter 5
Integrating the OMS without the Wrapper

// Must delete the returned memory


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;
}

bool DemoSky::CreateThing(BNGUID guidThing)


{
bool bRetVal = false;
char pcTemp[256];
BNOBJECTTYPE typeObject;

if (GetOMS())
{
if (BN_SUCCESS(GetOMS()->GetTypeByGUID(guidThing, typeObject)))
{
sprintf(pcTemp, "Created Thing %ld of type %d", guidThing,
typeObject);

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 = (vPosition.z - Z_OFFSET) / Z_SCALE;
pos.z = (vPosition.y - Y_OFFSET) / Y_SCALE;

iSector* room = engine->GetSectors ()->FindByName ("room");


if (room)
{
// Add the sprite to the engine.
csRef<iMeshWrapper> sprite
(engine->CreateMeshWrapper (imeshfact,
"MySprite", room, csVector3 (-3, 5, 3)));
csMatrix3 m; m.Identity ();
switch ( typeObject )
{
Integrating the OMS with an Existing Application | 173
Integrating the OMS without the Wrapper

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);

// Save the pointer to the thing


AddThingItem(guidThing, sprite);
}
}

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);

// TODO: Create and display a thing in the world here

return bRetVal;
}

bool DemoSky::UpdateThingView(BNGUID guidThing, bool bClientControlled)


{
bool bRetVal = false;
char pcTemp[256];

printf("Updated %s Thing %ld\n", bClientControlled ? "Client Controlled" :


"Server", guidThing);
174 | Chapter 5
Integrating the OMS without the Wrapper

fflush(stdout);

bRetVal = true;

return bRetVal;
}

bool DemoSky::RemoveThing(BNGUID guidThing)


{
bool bRetVal = false;
char pcTemp[256];

printf("Removed Thing %ld", guidThing);

csRef<iMeshWrapper> sprite = FindThingItem(guidThing);


engine->RemoveObject(sprite);

// Remove the pointer to the thing


RemoveThingItem(guidThing);

bRetVal = true;

return bRetVal;
}

bool DemoSky::ReceiveOffline(char *pcID, ULONG ulKey)


{
bool bRetVal = false;
char pcTemp[256];

printf("User %s (0x%04x) is offline", pcID ? pcID : "UNKNOWN", ulKey);


fflush(stdout);

bRetVal = true;

return bRetVal;
}

bool DemoSky::ReceivePing(char *pcID, ULONG ulKey)


{
bool bRetVal = false;
char pcTemp[256];

printf("User %s (0x%04x) is ONLINE\n", pcID ? pcID : "UNKNOWN", ulKey);


fflush(stdout);

bRetVal = true;

return bRetVal;
}

bool DemoSky::ReceiveMessage(char *pcUsername, char *pcMessage, ULONG ulKey)


{
bool bRetVal = false;
Integrating the OMS with an Existing Application | 175
Integrating the OMS without the Wrapper

char pcTemp[1024];

if ( pcMessage )
{
printf("User %s (0x%04x) said: %s\n", pcUsername ? pcUsername :
"UNKNOWN", ulKey, pcMessage);
fflush(stdout);
bRetVal = true;
}

return bRetVal;
}

bool DemoSky::ReceiveProjectile(char *pcData, UINT usDataLength)

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);

memcpy(&ulLong, pcData, sizeof(ULONG));


ulLong = ntohl(ulLong);
float fX = *((float *)&ulLong);
pcData += sizeof(ULONG);
memcpy(&ulLong, pcData, sizeof(ULONG));
ulLong = ntohl(ulLong);
float fY = *((float *)&ulLong);
pcData += sizeof(ULONG);
memcpy(&ulLong, pcData, sizeof(ULONG));
ulLong = ntohl(ulLong);
float fZ = *((float *)&ulLong);
pcData += sizeof(ULONG);

float fVelocity;
memcpy(&ulLong, pcData, sizeof(ULONG));
ulLong = ntohl(ulLong);
fVelocity = *((float *)&ulLong);
pcData += sizeof(ULONG);

char *pcProjectile = pcData;


pcData += strlen(pcProjectile) + 1;

char *pcExplosion = NULL;


if ((int)usDataLength > (pcData - pcStartPos))
{
pcExplosion = pcData;

Team-Fly®
176 | Chapter 5
Integrating the OMS without the Wrapper

// Put the particle effect


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);
}
else
{
printf("\n");
}
fflush(stdout);

// TODO: Display the projectile shot to the user


bRetVal = true;
}

return bRetVal;
}

bool DemoSky::ReceiveSecure(EventInfo *pData, char *pcQuestion)


{
bool bRetVal = false;
char pcTemp[256];

if (pcQuestion && GetOMS())


{
if (MessageBox(NULL, pcQuestion, "Secure Question", MB_YESNO) ==
IDYES)
{
GetOMS()->MessageSecureRespond(true, pData);
printf(pcTemp, " -> Accepted\n");
fflush(stdout);
}
else
{
GetOMS()->MessageSecureRespond(false, pData);
printf(pcTemp, " -> Rejected\n");
fflush(stdout);
}

bRetVal = true;
}

return bRetVal;
}

bool DemoSky::SetThingPosition(BNGUID guidThing, float fX, float fY, float fZ)


{
bool bRetVal = false;
Integrating the OMS with an Existing Application | 177
Integrating the OMS without the Wrapper

char pcTemp[256];

sprintf(pcTemp, " -> Thing %ld moved to position %4.2f, %4.2f, %4.2f",
guidThing, fX, fY, fZ);

// Update the position of the thing in the world


csRef<iMeshWrapper> spThing = FindThingItem(guidThing);
if ( spThing )
{

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 = (vPosition.z - Z_OFFSET) / Z_SCALE;
pos.z = (vPosition.y - Y_OFFSET) / Y_SCALE;

spThing->GetMovable()->SetPosition (pos);
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;
}

bool DemoSky::SetThingOrientation(BNGUID guidThing, float fX, float fY, float fZ)


{
bool bRetVal = false;
char pcTemp[256];

printf(" -> Thing %ld rotated to orientation %4.2f, %4.2f, %4.2f\n",


guidThing, fX, fY, fZ);
fflush(stdout);

// TODO: Update the thing's orientation in the world


bRetVal = true;

return bRetVal;
}

bool DemoSky::SetThingAnimation(BNGUID guidThing, char *pcAnimation)


{
bool bRetVal = false;
178 | Chapter 5
Integrating the OMS without the Wrapper

char pcTemp[256];

if ( pcAnimation )
{
printf(" -> Thing %ld animation set to %s\n", guidThing, pcAnimation);
fflush(stdout);

// TODO: Change the thing animation in the world


bRetVal = true;
}

return bRetVal;
}

bool DemoSky::SetThingIdentity(BNGUID guidThing, long lIdentity)


{
bool bRetVal = false;
char pcTemp[256];

printf(" -> Thing %d identity set to %ld\n", guidThing, lIdentity);


fflush(stdout);

// TODO: Change the thing's identity in the world


bRetVal = true;

return bRetVal;
}

bool DemoSky::AddThingItem(BNGUID guidThing, iMeshWrapper *pThing)


{
// return false;

// Make sure that a valid pointer has been passed in


if ( pThing )
{
// Make sure it is not already on the list
if (!FindThingItem(guidThing))
{
THINGITEM Temp;

Temp.guidThing = guidThing;
Temp.spThing = pThing;

m_vThingList.push_back(Temp);

}
return true;
}
return false;
}

iMeshWrapper *DemoSky::FindThingItem(BNGUID guidThing)


{
std::list< THINGITEM >::iterator viterThing;
Integrating the OMS with an Existing Application | 179
Integrating the OMS without the Wrapper

for (viterThing = m_vThingList.begin(); ((viterThing != m_vThingList.end())


&& (viterThing->guidThing != guidThing)); viterThing++);

if ((viterThing != m_vThingList.end()) && (viterThing->guidThing ==


guidThing))
return viterThing->spThing;

return NULL;
}

iMeshWrapper *DemoSky::RemoveThingItem(BNGUID guidThing)


{
csRef<iMeshWrapper> spTemp = 0;
std::list< THINGITEM >::iterator viterThing;

for (viterThing = m_vThingList.begin(); ((viterThing != m_vThingList.end())


&& (viterThing->guidThing != guidThing)); viterThing++);

if ((viterThing != m_vThingList.end()) && (viterThing->guidThing ==


guidThing))
{
spTemp = viterThing->spThing;
m_vThingList.remove(*viterThing);
}

return spTemp;
}

// End

DemoSky.h

/*
Copyright (C) 1998-2000 by Jorrit Tyberghein

This library is free software; you can redistribute it and/or


modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,


but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.

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.

09/05/2003 - Modified for Butterfly.net Example


*/
180 | Chapter 5
Integrating the OMS without the Wrapper

#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;

// Required to use the std::list::sort function


bool operator == (const ThingItem &Item1) const
{
return (guidThing == Item1.guidThing);
}
} THINGITEM;
// End

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 flock of birds


// Flock *flock; // Removed

// 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;

//** OMS Variables


COMS *m_pOMS;
char *m_pcServerIP;
char *m_pcServerPort;
char *m_pcUsername;
char *m_pcPassword;
int m_iGame;
int m_iVersion;
BNGUID m_guidAvatar;
// End

/** set texture space of poly, a size x size texture,


* given orig,u,ulen,v,vlen, so that you get no ugly
* edges (connecting to other polygons)
*/
void SetTexSpace(csProcSkyTexture *skytex, iPolygon3D *poly, int size,
const csVector3& orig, const csVector3& upt, float ulen,
const csVector3& vpt, float vlen);

// 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

bool UpdateThingView(BNGUID guidThing, bool bClientControlled);


bool RemoveThing(BNGUID guidThing);
bool ReceiveOffline(char *pcID, ULONG ulKey);
bool ReceivePing(char *pcID, ULONG ulKey);
bool ReceiveMessage(char *pcUsername, char *pcMessage, ULONG ulKey);
bool ReceiveProjectile(char *pcData, UINT usDataLength);
bool ReceiveSecure(EventInfo *pData, char *pcQuestion);
bool SetThingPosition(BNGUID guidThing, float fX, float fY,
float fZ);
bool SetThingOrientation(BNGUID guidThing, float fX, float fY,
float fZ);
bool SetThingAnimation(BNGUID guidThing, char *pcAnimation);
bool SetThingIdentity(BNGUID guidThing, long lIdentity);

std::list< THINGITEM > m_vThingList;


bool AddThingItem(BNGUID guidThing, iMeshWrapper *pThing);
iMeshWrapper *FindThingItem(BNGUID guidThing);
iMeshWrapper *RemoveThingItem(BNGUID guidThing);
// End

public:
DemoSky ();
virtual ~DemoSky ();

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

void Report (int severity, const char* msg, ...);


};

// REMOVED 'Flock' class from here.

#endif // DEMOSKY_H

ClientCreates.cpp

#include "ClientCreates.h"
#include "ClientObject.h"

CThing *CreateAvatar(BNOBJECTTYPE objecttype, BNGUID guidObject, bool


bIsClientControlled, BNGUID guidParentObject /*= GUID_INVALID*/, bool
bPreventClobber /*= false*/)
{
// Make sure the correct type of object is being created
ASSERT_ERROR(((objecttype == BN_THING_AVATAR ) || (objecttype == 0)),
"Wrong object type used to try to create Avatar");
if ((objecttype != BN_THING_AVATAR) && (objecttype != 0))
return NULL;
Integrating the OMS with an Existing Application | 183
Integrating the OMS without the Wrapper

CAvatar *o = new CAvatar(guidObject, bIsClientControlled, guidParentObject,


bPreventClobber);
return o;
}

CThing *CreateAnimal(BNOBJECTTYPE objecttype, BNGUID guidObject, bool


bIsClientControlled, BNGUID guidParentObject /*= GUID_INVALID*/, bool
bPreventClobber /*= false*/)
{
// Make sure the correct type of object is being created
ASSERT_ERROR((objecttype == BN_THING_ANIMAL), "Wrong object type used to
try to create Animal");
if (objecttype != BN_THING_ANIMAL)
return NULL;

CAnimal *o = new CAnimal(guidObject, bIsClientControlled, guidParentObject,


bPreventClobber);
return o;
}

ptr_CreateThingFunction CreateArray[NUM_CLIENT_OBJECTS] = { &CreateAvatar,


&CreateAvatar,
&CreateAnimal,
};

BNOBJECTTYPE ObjectArray[NUM_CLIENT_OBJECTS] = {0,


// Default object in the database (For testing only)
BN_THING_AVATAR,
BN_THING_ANIMAL,
};

ClientCreates.h

#include "../butterfly-grid/grid-common/thing/oms_cthing.h"
#include "ClientObject.h"

#define NUM_CLIENT_OBJECTS 2 + 1

extern ptr_CreateThingFunction CreateArray[];


extern BNOBJECTTYPE ObjectArray[];

CThing *CreateAvatars(BNOBJECTTYPE objecttype, BNGUID guidObject, bool


bIsClientControlled, BNGUID guidParentObject = GUID_INVALID, bool
bPreventClobber = false);
CThing *CreateAnimal(BNOBJECTTYPE objecttype, BNGUID guidObject, bool
bIsClientControlled, BNGUID guidParentObject = GUID_INVALID, bool
bPreventClobber = false);

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

const char *g_pcBNThingTypes[] = { "",


"Avatar",
"Animal",
};

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;

CPhysical::CPhysical(BNOBJECTTYPE objecttype, BNGUID guidObject, bool


bIsClientControlled, BNGUID guidParentObject /*= GUID_INVALID*/, bool
bPreventClobber /*= false*/) : CThing(objecttype, guidObject,
bIsClientControlled, guidParentObject, bPreventClobber)
{
pAnimation = NewStringType( BN_ATTRIB_ANIMATION, 0 );
if ( pAnimation )
pAnimation->SetAutoProcess(true);

#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(BNOBJECTTYPE objecttype, BNGUID guidObject, bool


bIsClientControlled, BNGUID guidParentObject /*= GUID_INVALID*/, bool
bPreventClobber /*= false*/) : CPhysical(objecttype, guidObject,
bIsClientControlled, guidParentObject, bPreventClobber)
{

CLiving::~CLiving()
{
Integrating the OMS with an Existing Application | 185
Integrating the OMS without the Wrapper

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);
}

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"

extern const char *g_pcBNThingTypes[];

/*************************** Class Defs ***********************************/


class CPhysical : public CThing
{
public:
CPhysical(BNOBJECTTYPE objecttype, BNGUID guidObject, bool
bIsClientControlled, BNGUID guidParentObject = GUID_INVALID,
bool bPreventClobber = false);
virtual ~CPhysical();

virtual void PureFunction() = 0;


private:
CStringAttrib *pAnimation;

Team-Fly®
186 | Chapter 5
Integrating the OMS without the Wrapper

};

class CLiving : public CPhysical


{
public:
CLiving(BNOBJECTTYPE objecttype, BNGUID guidObject, bool bIsClientControlled,
BNGUID guidParentObject = GUID_INVALID, bool bPreventClobber = false);
virtual ~CLiving();

virtual void PureFunction() = 0;


private:
};

class CAvatar : public CLiving


{
public:
CAvatar(BNGUID guidObject, bool bIsClientControlled, BNGUID
guidParentObject = GUID_INVALID, bool bPreventClobber = false);
virtual ~CAvatar();

virtual void PureFunction() {};


private:
CEnumAttrib *pIdentity;
};

class CAnimal : public CLiving


{
public:
CAnimal(BNGUID guidObject, bool bIsClientControlled, BNGUID
guidParentObject = GUID_INVALID, bool bPreventClobber = false);
virtual ~CAnimal();

virtual void PureFunction() {};


private:
};

#endif /*!CLIENTOBJECT_H_INCLUDED*/

ClientObjectDefines.h

#ifndef CLIENTOBJECTDEFINES_H_INCLUDED
#define CLIENTOBJECTDEFINES_H_INCLUDED

#include "../butterfly-grid/grid-common/butterfly_types.h"

/*************************** Message Flags ***********************************/


// Binary data is passed using the message string. Non-alpha values are put into
// the string using reinterpret casts. The length of the string is required
// for binary data.
enum BN_MESSAGE_TYPE
{
BN_MESSAGE_TYPE_TEXT_CHAT, // Text-based messages
BN_MESSAGE_TYPE_TEXT_MAX = 0x7F,
// All binary data comes after this
Integrating the OMS with an Existing Application | 187
Integrating the OMS without the Wrapper

BN_MESSAGE_TYPE_BINARY_PROJECTILE, // Origin (guid), Target Position


(float) X, Y, Z, Velocity (float), Name (string), [Explosion (string)]
BN_MESSAGE_TYPE_BINARY_MAX,
};

/*************************** Things Defines ***********************************/


enum BN_THING_ENUM
{
BN_THING_NULL= 0,
BN_THING_AVATAR,
BN_THING_ANIMAL,
BN_THING_MAX,
};

// Must update g_pcBNThingTypes in ClientObject.cpp file when thing types change!

/*************************** Attributes Defs ***********************************/

enum BN_ATTRIB_ENUM
{
BN_ATTRIB_ANIMATION = BUTTERFLY_SUBTYPES_MAX + 1, BN_ATTRIB_IDENTITY,
};

typedef enum BNAttribIdentity


{
BNATTRIB_IDENTITY_UNKNOWN,
BNATTRIB_IDENTITY_MAN,
BNATTRIB_IDENTITY_WOMAN,
BNATTRIB_IDENTITY_GHOST,
BNATTRIB_IDENTITY_MAX,
} BNATTRIB_IDENTITY;
// MAKE SURE CHANGES TO THE ABOVE ENUM ARE REFLECTED IN: BNAttribIdentityVALUES[]
// in ClientObject.cpp;

#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 add the following to the Library Files list:


c:\crystal\butterfly-grid\grid-oms\win32\debug

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

Listing 5-2: ServerInfo.cfg


wordware.butterfly.net:9907:wordware1:BocFemp1:7:1:8001:8001:
^ ^ ^ ^ ^ ^ ^ ^ ^
| | | | | | | | |
| | | | | | | | Identity Name
| | | | | | | | (optional)
| | | | | | | Local Send Port
| | | | | | |
| | | | | | Local Receive Port
| | | | | |
| | | | | Version Number
| | | | |
| | | | Game Number
| | | |
| | | Password
| | |
| | Username
| |
| Server Port Number
|
Server Address

// 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

Figure 5-6: Two players connected

If we then move player two for-


ward by pressing the up cursor
key, we should see something
similar to the following:

Figure 5-7: Player two moved


forward
190 | Chapter 5
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.

Figure 5-8: Sample debug information

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

variables are of type csProcSkyTexture, as defined within the DemoSky class in


the header file.
After this has been set up, we initialize our variables specific to the Butterfly
Grid. These are as follows:
m_pOMS = NULL;
m_pcServerIP = NULL;
m_pcServerPort = NULL;
m_pcUsername = NULL;
m_pcPassword = NULL;
m_iGame = 0;
m_iVersion = 0;
m_guidAvatar = 0;

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");

If we managed to load the file successfully (i.e., pConfig is a valid pointer), we


then read the first line of the file (up to a maximum of 1,024 characters) into our
pcBuffer char array.
if ( pConfig )
{
fgets(pcBuffer, 1024, pConfig);

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);
}
}

void DemoSky::SetPassword(const char *pcPassword)


{
if ( pcPassword )
{
m_pcPassword = new char[strlen(pcPassword) + 1];
if ( m_pcPassword )
strcpy(m_pcPassword, pcPassword);
}
}

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);

Let’s now look into the CreateOMS method.

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

The BNRESULT is enumerated to the following values.


Table 5-1: Enumeration values
Error Description

BNRESULT_ERROR Error: General error.

BNRESULT_NOT_FOUND Error: The requested item could not be


found.

BNRESULT_NOT_INITIALIZED Error: The item has not been initialized.

BNRESULT_ALREADY_EXISTS Error: The item already exists and a new


one cannot be created.

BNRESULT_OUT_OF_MEMORY Error: New returned null - out of memory.

BNRESULT_INVALID_PARAMETER Error: An invalid parameter was passed to


the function.

BNRESULT_NOT_LOGGED_ON Error: The OMS is not logged on so the


requested action cannot be completed.

BNRESULT_PACKET_OVERFLOW Error: The requested action cannot be


completed because it exceeds the
maximum packet size.

BNRESULT_SERVER_SET_NEWER_VALUE Error: The server set a value that has not


been read on a Thing with clobber
prevention enabled.

BNRESULT_OK Success.

BNRESULT_OK_DIRTY Success and the item is now dirty


(changed).

BNRESULT_OK_UPDATE_SERVER Success and the dead reckoning model


wants to update the server.

BNRESULT_OK_USE_SERVER_VALUE Success and the dead reckoning model


wants to use the server’s set value.

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

"username, 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.\nProbable "\
"cause network port is in use.\n\nPlease make sure "\
"no other instances of the program are running and "\
"try again.\nOr change the local port numbers used by "\
"one instance of the program.",
"OMS Failure", MB_OK);
return false;
break;
}

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);

Let’s now take a look at the contents of these arrays.


First, ObjectArray is defined within the ClientCreates.cpp file and looks as
follows:
BNOBJECTTYPE ObjectArray[NUM_CLIENT_OBJECTS] = {0,
// Default object in the database (For testing only)
BN_THING_AVATAR,
BN_THING_ANIMAL
};

Notice that the type is BNOBJECTTYPE, which is a type defined in the


thing_types.h of the OMS as BNTYPE, which in turn is defined in the butter-
fly_types.h file as an unsigned word; hence, the objects’ types are integer
values.
We leave our first object as 0, as this is always a test object within the data-
base. The second type, BN_THING_AVATAR, will be used to represent our
players in the world. The third type, BN_THING_ANIMAL, will be used to rep-
resent any static NPC (non-player characters) in the world.
Integrating the OMS with an Existing Application | 199
Integrating the OMS without the Wrapper

Note that BN_THING_AVATAR and BN_THING_ANIMAL are enumerated


in the ClientObjectDefines.h header as follows.
enum BN_THING_ENUM
{
BN_THING_NULL= 0,
BN_THING_AVATAR,
BN_THING_ANIMAL,
BN_THING_MAX,
};

In addition, we have a string representation of our object types defined in the


ClientObject.cpp file, in case we wish to display the type of the object easily.
This can be seen here:
const char *g_pcBNThingTypes[] = {"",
"Avatar",
"Animal",
};

Then, for CreateArray, we have the following:


ptr_CreateThingFunction CreateArray[NUM_CLIENT_OBJECTS] = {&CreateAvatar,
&CreateAvatar,
&CreateAnimal
};

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();

virtual void PureFunction() {};


private:
CEnumAttrib *pIdentity;
};

class CAnimal : public CLiving


200 | Chapter 5
Integrating the OMS without the Wrapper

{
public:
CAnimal(BNGUID guidObject, bool bIsClientControlled, BNGUID
guidParentObject = GUID_INVALID, bool bPreventClobber = false);
virtual ~CAnimal();

virtual void PureFunction() {};


private:
};

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

CIntAttrib Used to store integer data for an object.

CFloatAttrib Used to store a floating-point value for an object.

CEnumAttrib Used to hold an enumerated array of integers.

CBlobAttrib Used to hold a generic blob of data. The maximum size for this is
defined as OMS_MAX_BLOB_LEN within the OMS.

CVectorAttrib This attribute is used to hold a three-dimensional position; it is not a


dynamic array.

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;

CIntListAttrib Uses an STL vector to hold a list of integer values.

CFloatListAttrib Uses an STL vector to hold a list of floating-point values.


Integrating the OMS with an Existing Application | 201
Integrating the OMS without the Wrapper

Attribute Class Definition

CEnumListAttrib Uses an STL vector to hold a list of enumerated values.

CVectorListAttrib Uses an STL vector to hold a list of three-dimesional points.

CStringListAttrib Uses an STL vector to hold a list of char arrays (strings).

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;

Additionally, we define the different types of attributes that can be assigned,


again using an enumeration, with the starting value of BUTTERFLY_SUB-
TYPES_MAX + 1.
enum BN_ATTRIB_ENUM
{
BN_ATTRIB_ANIMATION = BUTTERFLY_SUBTYPES_MAX + 1,
BN_ATTRIB_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();

virtual void PureFunction() = 0;


private:
};

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

virtual void PureFunction() = 0;


private:
CStringAttrib *pAnimation;
};

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

OMS_EVENT_TRANSFER_TYPE_EVENT Adds the event to the event list.

OMS_EVENT_TRANSFER_TYPE_WINDOW_MESSAGE Sends the event to the window


specified by SetEventMessageHandle,
another method available within the
COMS class.

OMS_EVENT_TRANSFER_TYPE_POLL Does nothing because the client must


poll the OMS for information using
GetGUIDList and GetStatesByGUID.

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;}

const char *GetPassword() {return m_pcPassword;}

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 ();

Report (CS_REPORTER_SEVERITY_NOTIFY, "--------------------------------------");

// csView is a view encapsulating both a camera and a clipper.


// You don't have to use csView as you can do the same by
// manually creating a camera and a clipper but it makes things
// a little easier.
view = csPtr<iView> (new csView (engine, myG3D));
view->GetCamera ()->SetSector (room);
view->GetCamera ()->GetTransform ().SetOrigin (csVector3 (0, 0, 0));
view->SetRectangle (0, 0, myG2D->GetWidth (), myG2D->GetHeight ());

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

We define the position and orientation as three-dimensional positions, using


the FPOINT3 structure as defined within the thing_types.h header file. The
type-defined structure can be seen here:
typedef struct fPoint3
{
float x;
float y;
float z;
} FPOINT3;

So in our main code, we define the position and orientation as follows.


FPOINT3 vPosition;
vPosition.x = 0;
vPosition.y = 0;
vPosition.z = 0;
FPOINT3 vOrientation = {0.0f, 0.0f, 0.0f};

We then make a call to the COMS method SetMotionByGUID, which is used to


set the motion of a client-controlled object (i.e., the avatar). Into this method, we
pass the position and orientation of the object:
m_pOMS->SetMotionByGUID(m_guidAvatar, vPosition, vOrientation);

m_guidAvatar refers to the ID of the player we wish to update the information


for.
This concludes the Initialize method. After this, in the main method, the
csDefaultRunLoop method is called and the Crystal Space event processing
begins. So, the next place we are going to look at is the start of the SetupFrame
method, which is called by Crystal Space each time it is ready to render a new
frame.
The first method called in the SetupFrame method is our user-defined Update
method, which is also a member of the DemoSky class. Let’s look at this method
now, as it handles all the incoming messages from the server.
Within the Update method, we first define the following list of variables:
EventInfo *pEventInfo = NULL;
long lNumEventsLeft = 1;
bool bStillMore = true;
int iIndex = 0;

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

BNOBJECTTYPE typeTo; // This is the type of Thing that the


// event was sent to. It is for Daemon
// clients only.
OMS_EVENT eEventType; // This is the specific type of event
// that has occurred.
char *pcMessage; // This is the string of the message
// received. It is used only for
// instant message events, and it is
// self deleting.
unsigned short usMessageLength; //!< This is the length of the
// message received. It is used only
// for instant message events.
char *pcUsername; // This is the username of the client
// that sent the message. It is used

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

// message. It is used only for secure


// instant message events and it is
// self deleting.
CEventMsg *pMsg; // This is the pointer to the message
// for responding to a secure message.
// You need to hold a pointer to this
// EventInfo structure in order to
// respond to a secure message. It is
// used only for secure instant message
// events and it is self deleting.
std::vector<CTHINGATTRIBUTE> *pParameterList; // This is the pointer to a
// vector of parameters that were sent
// with a script event. It is used
// only for script events and it is
// self deleting.
EventInfo *pNext; // This is the pointer to the
// next event on the list. It is
// for internal use only.

/*
* 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

lNumEventsLeft variable is passed in, which is then updated internally within


the method with the number of events in the OMS awaiting processing.
Next, we check that we have an event by again testing the pEventInfo pointer
for validity. If the pointer was not valid, we set the bStillMore Boolean to false,
signifying that there are no more messages to process, hence making the for loop
terminate.
if (pEventInfo != NULL)
{
...
}
else
bStillMore = false;

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();

After this, we switch to 2D drawing mode by calling the BeginDraw method


again, this time passing in the CSDRAW_2DGRAPHICS parameter, as shown
here:
if(!myG3D->BeginDraw(CSDRAW_2DGRAPHICS)) return;

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;

if (GetOMS() && BN_SUCCESS(GetOMS()->GetIdentities(vIdentities)))


{
uCount = vIdentities.size();
if (uCount > 0)
{
printf("Selecting first ident\n");
GetOMS()->SelectIdentity(vIdentities[0]);
}
}
}
break;

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 is simply a Boolean to hold the outcome of the method — whether it


succeeded or failed. The vAttributes vector is used within the method to hold a
list of attributes associated with a Thing. uAttrib is used to hold a count of the
total number of attributes, while pcTemp is used to work with string (char array)
data.
After we have defined the local variables, we then check the OMS pointer for
validity using the following:
if ( !m_pOMS )
return false;

Next, we make a call to the UpdateThingView method, which simply outputs to


the console which Thing is being updated and whether it is client or server con-
trolled. This complete method can be seen here:
bool DemoSky::UpdateThingView(BNGUID guidThing, bool bClientControlled)
{
bool bRetVal = false;
char pcTemp[256];

printf("Updated %s Thing %ld\n", bClientControlled ? "Client Controlled" :


"Server", guidThing);
fflush(stdout);

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() {}

//! This is required to use the std::list::sort function.


bool operator < (const CThingAttributeValue &Attrib1) const
{
if ((m_idState < Attrib1.m_idState) || ((m_idState ==
Attrib1.m_idState) && (m_typeObject < Attrib1.m_typeObject)))
return true;
else
return false;
}
};

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

list_commands ListCommand; // !< This is the list command, e.g.,


// set, clear, etc.
UWORD Type; // This is the type of value contained
// in the union. It is defined as a
// UWORD because it can contain a
// property type or a Python type.
CTHINGATTRIBUTEUNION Value; // !< This is the union containing the
// actual value of the attribute.
cThingAttribute() {ListCommand = LIST_NULL; iListIndex = LIST_MAX;}
// Initialize invalid
} CTHINGATTRIBUTE;

In this structure, we then access the Value parameter, which is of type


CTHINGATTRIBUTEUNION. CTHINGATTRIBUTEUNION is shown here
for reference.
typedef union cThingAttributeUnion
{
INT lLong; // This is the long/int value.
FLOAT fFloat; // This is the float value.
FPOINT3 vVector; // This is the vector value containing
// three floats (X, Y, and Z).
TOKEN Token; // This is the token value, i.e., a
// 64-bit value containing the GUID,
// spec type, and type.
CTHINGBLOBATTRIBUTESTRUCT Blob; // This is the BLOB value.
CTHINGSTRINGATTRIBUTESTRUCT String; // This is the string value.
} CTHINGATTRIBUTEUNION;

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;

// Required to use the std::list::sort function


bool operator == (const ThingItem &Item1) const
{
return (guidThing == Item1.guidThing);
}
} THINGITEM;

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

THINGITEMs to find the correct iMeshWrapper. The complete method to do


this can be seen here:
iMeshWrapper *DemoSky::FindThingItem(BNGUID guidThing)
{
std::list< THINGITEM >::iterator viterThing;

for (viterThing = m_vThingList.begin(); ((viterThing != m_vThingList.end())


&& (viterThing->guidThing != guidThing)); viterThing++);

if ((viterThing != m_vThingList.end()) && (viterThing->guidThing ==


guidThing))
return viterThing->spThing;

return NULL;
}

Back in our SetThingPosition method, once we have the iMeshWrapper pointer


to the object, we then check if it is a valid pointer as follows:
if ( spThing )
{

If it is, we initialize two three-dimensional vectors as follows:


FPOINT3 vPosition = {0.0f, 0.0f, 0.0f};
FPOINT3 vOrientation = {0.0f, 0.0f, 0.0f};

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

if (BN_SUCCESS(m_pOMS->GetMotionByGUID(guidThing, vPosition, vOrientation)))


{

If the call to GetMotionByGUID was successful, we then update the position of


the object by creating a csVector3 object, performing any adjustment or scaling
required into our own world coordinates, then calling the SetPosition of the
iMeshWrapper interface. This can all be seen in the following code segment.
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;

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);

for (uAttrib = 0; uAttrib < vAttributes.size(); uAttrib++)


{

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())
{

Next, we make a call to the GetTypeByGUID method of the COMS, passing in


the unique Thing ID, called guidThing, and also the variable typeObject, which
will store the resulting type of object.
if (BN_SUCCESS(GetOMS()->GetTypeByGUID(guidThing, typeObject)))
{

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)));

Next, we create a csMatrix3 object, m, which we initialize to the identity matrix


by calling the Identity method of the csMatrix3 class:
csMatrix3 m;
m.Identity();

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))
{

As we have seen before, the FindThingItem method iterates through the


m_vThingList vector (which is a member of our DemoSky class), comparing
each guidThing ID within the THINGITEM structure with the one passed in. If
the method finds the ID within the vector, it returns true; otherwise it returns
false.
Integrating the OMS with an Existing Application | 221
Integrating the OMS without the Wrapper

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);

Finally, we make a call to our user-defined RemoveThingItem method, again


passing in the unique ID of the Thing we wish to remove. The call to this can be
seen here:
RemoveThingItem(guidThing);

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;

for (viterThing = m_vThingList.begin(); ((viterThing != m_vThingList.end())


&& (viterThing->guidThing != guidThing)); viterThing++);

if ((viterThing != m_vThingList.end()) && (viterThing->guidThing ==


guidThing))
{
spTemp = viterThing->spThing;
m_vThingList.remove(*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

printf("User %s (0x%04x) is offline", pcID ? pcID : "UNKNOWN", ulKey);


fflush(stdout);

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];

printf("User %s (0x%04x) is ONLINE\n", pcID ? pcID : "UNKNOWN", ulKey);


fflush(stdout);

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;
}

The second message type, BN_MESSAGE_TYPE_BINARY_PROJECTILE,


shows an example of how binary data can be stored within a message. When this
message is received, we pass the message and the message length into our
user-defined ReceiveProjectile method, as shown below:
case BN_MESSAGE_TYPE_BINARY_PROJECTILE:
ReceiveProjectile(pEventInfo->pcMessage, pEventInfo->usMessageLength);
break;

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 fZ = *((float *)&ulLong);


pcData += sizeof(ULONG);

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");

After this, we call our user-defined ReceiveSecure method, passing in


pEventInfo object and the pcMessage pointer:
ReceiveSecure(pEventInfo, pcMessage);

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 (pcQuestion && GetOMS())


{

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

printf(pcTemp, " -> Accepted\n");


fflush(stdout);
}
else
{
GetOMS()->MessageSecureRespond(false, pData);
printf(pcTemp, " -> Rejected\n");
fflush(stdout);
}

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

OMS_EVENT_LOGON_PASS OMS logged onto the server successfully.

OMS_EVENT_LOGON_FAIL OMS logon failed.

OMS_EVENT_IDENT_LIST_CHANGE The server sent a new list of identities that can be


embodied.

OMS_EVENT_EMBODY_DONE The requested identity was successfully embodied.


The EventInfo::guidThing contains the GUID and
the EventInfo::typeThing contains the type of avatar
that was embodied.

OMS_EVENT_EMBODY_FAIL The embody request failed.

OMS_EVENT_THING_NEW A new Thing is within the client’s range. The


EventInfo::guidThing contains the GUID and the
EventInfo::typeThing contains the type of new Thing
within the client’s range.

OMS_EVENT_THING_HERE A Thing is now within the the client’s presence


(colliding with the Thing). The EventInfo::guidThing
contains the GUID and the EventInfo::typeThing
contains the type of Thing involved in the collision.

OMS_EVENT_THING_SET A least one state on a Thing has changed. The


EventInfo::guidThing contains the GUID and the
EventInfo::type contains the type of Thing that has
changed.

OMS_EVENT_THING_DROP A Thing has moved out of the client’s range. The


EventInfo::guidThing contains the GUID and the
EventInfo::typeThing contains the type of Thing that
was dropped.
228 | Chapter 5
Integrating the OMS without the Wrapper

Event Description

OMS_EVENT_THING_GONE A Thing has been removed from the world. The


EventInfo::guidThing contains the GUID and the
EventInfo::typeThing contains the type of Thing that
is gone.

OMS_EVENT_MESSAGE_USER_ The user being searched for by COMS::Mes-


OFFLINE sageFind is offline. The EventInfo::guidThing
contains the user’s public_key. EventInfo::pcMes-
sage and EventInfo::pcUsername contain the
username of client.

OMS_EVENT_MESSAGE_USER_PING The user being searched for by COMS::Mes-


sageFind is online. The EventInfo::guidThing
contains the user’s public_key. EventInfo::pcMes-
sage and EventInfo::pcUsername contain the
username of client.

OMS_EVENT_MESSAGE_RECEIVED An instant chat message was received. The


EventInfo::guidThing contains the user’s public_key.
EventInfo::typeThing contains the message flags.
EventInfo::pcMessage contains the message string
(not necessarily null terminated). EventInfo::
usMessageLength 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). EventInfo::pcUser-
name contains the username of the client that sent
the message.

OMS_EVENT_MESSAGE_RECEIVED_ A secure instant message was received. The


SECURE EventInfo::guidThing contains the user’s public_key.
EventInfo::pcMessage contains the question string.
EventInfo::usMessageLength 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). EventInfo::
pcUsername contains the username of the client
that sent the message. EventInfo::pcAccept contains
the accept prompt. EventInfo::pcReject contains the
rejection prompt. EventInfo::pMsg contains a
pointer to the secure message itself. (Note: Do not
use or modify this pointer or the structure it points
to because it could cause any response to fail.)

OMS_EVENT_SCRIPT_EVENT A script event invocation was received. The


EventInfo::guidThing contains the GUID of the
Thing that is invoking the script and EventInfo::
pvParameters contains a pointer to the vector of
parameters (including the module and function) of
the script invocation.
Integrating the OMS with an Existing Application | 229
Integrating the OMS with the Wrapper

Integrating the OMS with the Wrapper


In the previous example, we looked at implementing the OMS without the help
of the OMSWrapper class. In this section we are going to create the same exam-
ple, but this time we will be implementing the OMSWrapper class to attempt to
simplify the example as much as possible.
For this example, we have only changed the demosky.cpp and demosky.h
files, but there are some drastic changes. So create a new project in Visual Stu-
dio as you have done previously, add the other files from the previous example,
then create a new demosky.cpp and demosky.h file using the following code
listing.
Listing 5-3: DemoSky OMS Example 2
DemoSky.cpp

/*
Copyright (C) 1998-2000 by Jorrit Tyberghein
Copyright (C) 2001 by W.C.A. Wijngaards

This library is free software; you can redistribute it and/or


modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,


but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.

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.

demosky - This application shows a (slow) way to produce a sky using


procedural textures and some fractal algorithms

09/05/2003 - Modified for Butterfly.net Example


*/

#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"

#define X_OFFSET 0.0f


#define Y_OFFSET 0.0f
#define Z_OFFSET 000.0f

#define X_SCALE 1.0f


#define Y_SCALE 1.0f
#define Z_SCALE 1.0f

CS_IMPLEMENT_APPLICATION

// the global system driver variable


DemoSky *System;

void DemoSky::Report (int severity, const char* msg, ...)


{
va_list arg;
va_start (arg, msg);
csRef<iReporter> rep (CS_QUERY_REGISTRY (System->object_reg, iReporter));
if (rep)
rep->ReportV (severity, "crystalspace.application.demosky", msg, arg);
else
{
csPrintfV (msg, arg);
csPrintf ("\n");
}
Integrating the OMS with an Existing Application | 231
Integrating the OMS with the Wrapper

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);
}

void DemoSky::SetTexSpace(csProcSkyTexture *skytex, iPolygon3D *poly,


int size, const csVector3& orig, const csVector3& upt, float ulen,
const csVector3& vpt, float vlen)
{
csVector3 texorig = orig;
csVector3 texu = upt;
float texulen = ulen;
csVector3 texv = vpt;
float texvlen = vlen;
// copied, now adjust
csVector3 uvector = upt - orig;
csVector3 vvector = vpt - orig;
// to have 1 pixel going over the edges.
texorig -= uvector / float(size);
texorig -= vvector / float(size);
texu += uvector / float(size);
texv += vvector / float(size);
texulen += ulen * 2.0f / float(size);
232 | Chapter 5
Integrating the OMS with the Wrapper

texvlen += vlen * 2.0f / float(size);


poly->SetTextureSpace (texorig, texu, texulen, texv, texvlen);
skytex->SetTextureSpace(texorig, texu-texorig, texv-texorig);
}

static bool DemoSkyEventHandler (iEvent& ev)


{
if (ev.Type == csevBroadcast && ev.Command.Code == cscmdProcess)
{
System->SetupFrame ();
return true;
}
else if (ev.Type == csevBroadcast && ev.Command.Code == cscmdFinalProcess)
{
System->FinishFrame ();
return true;
}
else
{
return System ? System->HandleEvent (ev) : false;
}
}

bool DemoSky::Initialize (int argc, const char* const argv[],


const char *iConfigName)
{
object_reg = csInitializer::CreateEnvironment (argc, argv);
if (!object_reg) return false;

if (!csInitializer::SetupConfigManager (object_reg, iConfigName))


{
Report (CS_REPORTER_SEVERITY_ERROR, "Couldn't initialize app!");
return false;
}

if (!csInitializer::RequestPlugins (object_reg,
CS_REQUEST_VFS,
CS_REQUEST_SOFTWARE3D,
CS_REQUEST_ENGINE,
CS_REQUEST_FONTSERVER,
CS_REQUEST_IMAGELOADER,
CS_REQUEST_LEVELLOADER,
CS_REQUEST_CONSOLEOUT,
CS_REQUEST_END))
{
Report (CS_REPORTER_SEVERITY_ERROR, "Couldn't init app!");
return false;
}

if (!csInitializer::SetupEventHandler (object_reg, DemoSkyEventHandler))


{
Report (CS_REPORTER_SEVERITY_ERROR, "Couldn't init app!");
return false;
}
Integrating the OMS with an Existing Application | 233
Integrating the OMS with the Wrapper

// Check for commandline help.


if (csCommandLineHelper::CheckHelp (object_reg))
{
csCommandLineHelper::Help (object_reg);
exit (0);
}

// The virtual clock.


vc = CS_QUERY_REGISTRY (object_reg, iVirtualClock);

// Find the pointer to engine plug-in


engine = CS_QUERY_REGISTRY (object_reg, iEngine);
if (!engine)
{
Report (CS_REPORTER_SEVERITY_ERROR, "No iEngine plugin!");
exit (-1);
}

LevelLoader = CS_QUERY_REGISTRY (object_reg, iLoader);


if (!LevelLoader)
{
Report (CS_REPORTER_SEVERITY_ERROR, "No iLoader plugin!");
exit (-1);
}

myG3D = CS_QUERY_REGISTRY (object_reg, iGraphics3D);


if (!myG3D)
{
Report (CS_REPORTER_SEVERITY_ERROR, "No iGraphics3D plugin!");
exit (-1);
}

myG2D = CS_QUERY_REGISTRY (object_reg, iGraphics2D);


if (!myG2D)
{
Report (CS_REPORTER_SEVERITY_ERROR, "No iGraphics2D plugin!");
exit (-1);
}

kbd = CS_QUERY_REGISTRY (object_reg, iKeyboardDriver);


if (!kbd)
{
Report (CS_REPORTER_SEVERITY_ERROR, "No iKeyboardDriver!");
exit (-1);
}

// 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

// Set up the texture manager


iTextureManager* txtmgr = myG3D->GetTextureManager ();
txtmgr->SetVerbose (true);

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);

// Create our world.


Report (CS_REPORTER_SEVERITY_NOTIFY, "Creating world!...");

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");

room = engine->CreateSector ("room");


csRef<iMeshWrapper> walls (engine->CreateSectorWallsMesh (room, "walls"));
csRef<iThingState> walls_state (SCF_QUERY_INTERFACE (walls->GetMeshObject (),
iThingState));
iPolygon3D* p;
p = walls_state->CreatePolygon ();
p->SetMaterial (imatd);
float size = 500.0; // size of the skybox — around 0, 0, 0 for now.
float simi = size; //*255./256.; // sizeminor
p->CreateVertex (csVector3 (-size, -simi, size));
p->CreateVertex (csVector3 (size, -simi, size));
p->CreateVertex (csVector3 (size, -simi, -size));
p->CreateVertex (csVector3 (-size, -simi, -size));

SetTexSpace (sky_d, p, 256, p->GetVertex (0), p->GetVertex (1),


2.0f * size, p->GetVertex (3), 2.0f * size);
p->GetFlags ().Set(CS_POLY_LIGHTING, 0);
Integrating the OMS with an Existing Application | 235
Integrating the OMS with the Wrapper

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));

SetTexSpace (sky_u, p, 256, p->GetVertex (0), p->GetVertex (1),


2.0f * size, p->GetVertex (3), 2.0f * size);
p->GetFlags ().Set(CS_POLY_LIGHTING, 0);

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));

SetTexSpace (sky_f, p, 256, p->GetVertex (0), p->GetVertex (1),


AM
2.0f * size, p->GetVertex (3), 2.0f * size);
p->GetFlags ().Set(CS_POLY_LIGHTING, 0);

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));

SetTexSpace (sky_r, p, 256, p->GetVertex (0), p->GetVertex (1),


2.0f * size, p->GetVertex (3), 2.0f * size);
p->GetFlags ().Set(CS_POLY_LIGHTING, 0);

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));

SetTexSpace (sky_l, p, 256, p->GetVertex (0), p->GetVertex (1),


2.0f * size, p->GetVertex (3), 2.0f * size);
p->GetFlags ().Set(CS_POLY_LIGHTING, 0);

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));

SetTexSpace (sky_b, p, 256, p->GetVertex (0), p->GetVertex (1),


2.0f * size, p->GetVertex (3), 2.0f * size);
p->GetFlags ().Set(CS_POLY_LIGHTING, 0);

Team-Fly®
236 | Chapter 5
Integrating the OMS with the Wrapper

LevelLoader->LoadTexture ("seagull", "/lib/std/seagull.gif");

// Load a texture for our sprite.


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;
}

// Load a sprite template from disk.


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;
}

// connect to the server...


ConnectUsingServerInfoFile("ServerInfo.cfg", NUM_CLIENT_OBJECTS, CreateArray,
ObjectArray);

// End

engine->Prepare ();

Report (CS_REPORTER_SEVERITY_NOTIFY, "--------------------------------------");

// csView is a view encapsulating both a camera and a clipper.


// You don't have to use csView as you can do the same by manually
// creating a camera and a clipper but it makes things a little easier.
view = csPtr<iView> (new csView (engine, myG3D));
view->GetCamera ()->SetSector (room);
view->GetCamera ()->GetTransform ().SetOrigin (csVector3 (0, 0, 0));
view->SetRectangle (0, 0, myG2D->GetWidth (), myG2D->GetHeight ());

// set the initial position to the origin...


FPOINT3 vPosition;
vPosition.x = 0;
vPosition.y = 0;
vPosition.z = 0;
FPOINT3 vOrientation = {0.0f, 0.0f, 0.0f};
GetOMS()->SetMotionByGUID(GetAvatar(), vPosition, vOrientation);

return true;
}

void DemoSky::SetupFrame ()
{
Integrating the OMS with an Existing Application | 237
Integrating the OMS with the Wrapper

// process OMS events...


Update(100, 10);

csTicks elapsed_time, current_time;


elapsed_time = vc->GetElapsedTicks ();
current_time = vc->GetCurrentTicks ();

// Now rotate the camera according to keyboard state


float speed = (elapsed_time / 1000.0f) * (0.03f * 20.0f);

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);

// Tell 3D driver we're going to display 3D things.


if (!myG3D->BeginDraw (engine->GetBeginDrawFlags () | CSDRAW_3DGRAPHICS))
return;

view->Draw();

// Start drawing 2D graphics.


if (!myG3D->BeginDraw (CSDRAW_2DGRAPHICS)) return;

// 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

vPosition.z = (view->GetCamera()->GetTransform().GetOrigin().y * X_SCALE) +


Z_OFFSET;

FPOINT3 vOrientation = {0.0f, 0.0f, 0.0f};


GetOMS()->SetMotionByGUID(GetAvatar(), vPosition, vOrientation);

bool DemoSky::HandleEvent (iEvent &Event)


{
if ((Event.Type == csevKeyDown) && (Event.Key.Code == 't'))
{
// toggle animation
sky->SetAnimated(object_reg, !sky->GetAnimated(), csGetTicks ());
return true;
}

if ((Event.Type == csevKeyDown) && (Event.Key.Code == CSKEY_ESC))


{
csRef<iEventQueue> q (CS_QUERY_REGISTRY (object_reg, iEventQueue));
if (q)
q->GetEventOutlet()->Broadcast (cscmdQuit);
return true;
}

return false;
}

/*---------------------------------------------------------------------*
* Main function
*---------------------------------------------------------------------*/
int main (int argc, char* argv[])
{
srand (time (NULL));

// Create our main class.


System = new DemoSky ();

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;
}
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);

GetOMS()->ServerLogout(2000); // Wait 2 seconds max


}
}

void DemoSky::EventThingNew(BNGUID guidThing, BNOBJECTTYPE typeThing)


{
CreateThing(guidThing);
}

void DemoSky::EventAvatarNew(BNGUID guidThing, BNOBJECTTYPE typeThing)


{
CreateThing(guidThing);
}

void DemoSky::EventThingHere(BNGUID guidThing, BNOBJECTTYPE typeThing, bool


bClientControlled)
{
UpdateThing(guidThing, bClientControlled);
}

void DemoSky::EventThingSet(BNGUID guidThing, BNOBJECTTYPE typeThing, bool


bClientControlled)
{
UpdateThing(guidThing, bClientControlled);
}

void DemoSky::EventThingDrop(BNGUID guidThing, BNOBJECTTYPE typeThing, bool


bClientControlled)
{
RemoveThing(guidThing);
}

void DemoSky::EventThingGone(BNGUID guidThing, BNOBJECTTYPE typeThing, bool


bClientControlled)
{
RemoveThing(guidThing);
}
240 | Chapter 5
Integrating the OMS with the Wrapper

bool DemoSky::UpdateThing(BNGUID guidThing, bool bClientControlled)


{
bool bRetVal = false;
static std::vector<CThingAttributeValue> vAttributes;
UINT uAttrib;

if (!GetOMS())
return false;

UpdateThingView(guidThing, bClientControlled);

// Update the non-client objects


if ( !bClientControlled )
{
// Try the new GetAttributes function
GetOMS()->GetStatesByGUID(guidThing, vAttributes);

for (uAttrib = 0; uAttrib < vAttributes.size(); uAttrib++)


{
switch ( vAttributes[uAttrib].m_idState )
{
case BUTTERFLY_POSITION:
bRetVal = EventThingSetPosition(guidThing,
vAttributes[
uAttrib].m_Attribute.Value.vVector.x,
vAttributes[
uAttrib].m_Attribute.Value.vVector.y,
vAttributes[
uAttrib].m_Attribute.Value.vVector.z);
break;
}

// Must delete the returned memory


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;
}

// Must delete the returned memory


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;
Integrating the OMS with an Existing Application | 241
Integrating the OMS with the Wrapper

vAttributes[uAttrib].m_Attribute.Value.Blob.iLength = 0;
vAttributes[uAttrib].m_Attribute.Type = PROPERTY_NULL;
}
}
}

return bRetVal;
}

bool DemoSky::CreateThing(BNGUID guidThing)


{
bool bRetVal = false;
char pcTemp[256];
BNOBJECTTYPE typeObject;

if (GetOMS())
{
if (BN_SUCCESS(GetOMS()->GetTypeByGUID(guidThing, typeObject)))
{
printf("Created Thing %ld of type %d\n", guidThing, typeObject);
fflush(stdout);

FPOINT3 vPosition = {0.0f, 0.0f, 0.0f};


FPOINT3 vOrientation = {0.0f, 0.0f, 0.0f};
if (BN_SUCCESS(GetOMS()->GetMotionByGUID(guidThing, vPosition,
vOrientation)))
{
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;

iSector* room = engine->GetSectors ()->FindByName ("room");


if (room)
{
// Add the sprite to the engine.
csRef<iMeshWrapper> sprite
(engine->CreateMeshWrapper (
imeshfact, "MySprite", room,
csVector3 (-3, 5, 3)));
csMatrix3 m; m.Identity ();
switch ( typeObject )
{
case 0:
case 1:
m *= 2.0;
break;
case 2:
case 3:
m *= 1.0;
break;
default:
m *= 0.25;
break;
}
242 | Chapter 5
Integrating the OMS with the Wrapper

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);

// Save the pointer to the thing


AddThingItem(guidThing, sprite);
}
}

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;
}

bool DemoSky::UpdateThingView(BNGUID guidThing, bool bClientControlled)


{
bool bRetVal = false;
char pcTemp[256];

printf("Updated %s Thing %ld\n", bClientControlled ? "Client Controlled" :


"Server", guidThing);
fflush(stdout);

bRetVal = true;

return bRetVal;
}

bool DemoSky::RemoveThing(BNGUID guidThing)


{
bool bRetVal = false;

printf("Removed Thing %ld", guidThing);

csRef<iMeshWrapper> sprite = FindThingItem(guidThing);


engine->RemoveObject(sprite);

// Remove the pointer to the thing


Integrating the OMS with an Existing Application | 243
Integrating the OMS with the Wrapper

RemoveThingItem(guidThing);

bRetVal = true;

return bRetVal;
}

bool DemoSky::EventAvatarSetPosition(BNGUID guidThing, float fX, float fY,


float fZ)
{
EventThingSetPosition(guidThing, fX, fY, fZ);
return true;
}

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);

// Update the position of the thing in the world


csRef<iMeshWrapper> spThing = FindThingItem(guidThing);
if ( spThing )
{
csVector3 pos;
pos.x = (fX - X_OFFSET) / X_SCALE;
pos.y = (fZ - Z_OFFSET) / Z_SCALE;
pos.z = (fY - Y_OFFSET) / Y_SCALE;

spThing->GetMovable()->SetPosition(pos);
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;
}

bool DemoSky::AddThingItem(BNGUID guidThing, iMeshWrapper *pThing)


{
// Make sure that a valid pointer has been passed in
if ( pThing )
{
// Make sure it is not already on the list
if (!FindThingItem(guidThing))
244 | Chapter 5
Integrating the OMS with the Wrapper

{
THINGITEM Temp;

Temp.guidThing = guidThing;
Temp.spThing = pThing;

m_vThingList.push_back(Temp);

}
return true;
}
return false;
}

iMeshWrapper *DemoSky::FindThingItem(BNGUID guidThing)


{
std::list< THINGITEM >::iterator viterThing;

for (viterThing = m_vThingList.begin(); ((viterThing != m_vThingList.end())


&& (viterThing->guidThing != guidThing)); viterThing++);

if ((viterThing != m_vThingList.end()) && (viterThing->guidThing ==


guidThing))
return viterThing->spThing;

return NULL;
}

iMeshWrapper *DemoSky::RemoveThingItem(BNGUID guidThing)


{
csRef<iMeshWrapper> spTemp = 0;
std::list< THINGITEM >::iterator viterThing;

for (viterThing = m_vThingList.begin(); ((viterThing != m_vThingList.end())


&& (viterThing->guidThing != guidThing)); viterThing++);

if ((viterThing != m_vThingList.end()) && (viterThing->guidThing ==


guidThing))
{
spTemp = viterThing->spThing;
m_vThingList.remove(*viterThing);
}

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

This library is free software; you can redistribute it and/or


modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,


but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.

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;

// Required to use the std::list::sort function


bool operator == (const ThingItem &Item1) const
{
return (guidThing == Item1.guidThing);
}
} THINGITEM;
// End

class DemoSky : public COMSWrapper


{
public:
iObjectRegistry* object_reg;
private:
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 flock of birds


// Flock *flock; // Removed

// 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;

/** set texture space of poly, a size x size texture,


* given orig, u, ulen, v, vlen, so that you get no ugly
* edges (connecting to other polygons)
*/
Integrating the OMS with an Existing Application | 247
Integrating the OMS with the Wrapper

void SetTexSpace(csProcSkyTexture *skytex, iPolygon3D *poly, int size,


const csVector3& orig, const csVector3& upt, float ulen,
const csVector3& vpt, float vlen);

void CleanUp();

bool UpdateThing(BNGUID guidThing, bool bClientControlled);

/****************************************************************************\
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);

bool CreateThing(BNGUID guidThing);


bool UpdateThingView(BNGUID guidThing, bool bClientControlled);
bool RemoveThing(BNGUID guidThing);

std::list< THINGITEM > m_vThingList;


bool AddThingItem(BNGUID guidThing, iMeshWrapper *pThing);
iMeshWrapper *FindThingItem(BNGUID guidThing);
iMeshWrapper *RemoveThingItem(BNGUID guidThing);

public:
DemoSky ();
virtual ~DemoSky ();

bool Initialize (int argc, const char* const argv[],


const char *iConfigName);
void SetupFrame ();
void FinishFrame ();
bool HandleEvent (iEvent &Event);

void Report (int severity, const char* msg, ...);


};

#endif // DEMOSKY_H
248 | Chapter 5
Integrating the OMS with the Wrapper

Before we execute this second example, we need to make an addition to the


ServerInfo.cfg file. This addition is an extra token that specifies the name of the
avatar the player should embody upon successful login to the server. The two
avatars we have on our sample server are wordware1 and wordware2, so this
means that we now use the following two configuration files to connect to the
server with either player:
wordware.butterfly.net:9907:wordware1:BocFemp1:7:1:8001:8001:wordware1:
wordware.butterfly.net:9907:wordware2:BocFemp2:7:1:8002:8002:wordware2:

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);

Then we have very similar UpdateThing, UpdateThingView, CreateThing,


RemoveThing, AddThingItem, FindThingItem, and RemoveThingItem methods.
The major difference is overriding the virtual methods within the
COMSWrapper class, which are called when an OMS event occurs. We can
override any of these event methods; however, in this sample we have only
implemented the ones that are essential, i.e., the pure virtual methods.
250 | Chapter 5
Integrating the OMS with the Wrapper

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);

// Update the position of the thing in the world


csRef<iMeshWrapper> spThing = FindThingItem(guidThing);
if ( spThing )
{
csVector3 pos;
pos.x = (fX - X_OFFSET) / X_SCALE;
pos.y = (fZ - Z_OFFSET) / Z_SCALE;
pos.z = (fY - Y_OFFSET) / Y_SCALE;

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;
}

This method is called upon the OMS receiving an OMS_EVENT_THING_SET


event. The method receives the new position of the Thing referenced by
guidThing.
This concludes the wrapper example. As you can see, it really simplifies the
integration while still giving you full control over the OMS by means of the
GetOMS method declared within the COMSWrapper class.

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

Demo Game Part 1 —


Building the GUI

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

Then click on the Proceed to Non-commercial/Private registration form link. Fill


in your personal details and ensure you check the Qt/Windows box, as this is the
version we will be using. You will be asked to confirm your details and then you
will be given two download options. Select the Evaluation for Microsoft Visual
C++ (MSVC 6.0) option.
Once downloaded, run the executable. The following should be visible:

253
254 | Chapter 6
Installing Qt

Figure 6-1: 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.

Figure 6-2: Registration details

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:

Figure 6-4: Folder options

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.

Figure 6-5: Configuring Qt

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.

Designing the GUI


Now that Qt is installed, we are going to use it to create the following dialogs
for use in our application:
n Login dialog
n Signup dialog
n Error dialog
n Chat dialog
To do this, first load Qt Designer. It’s located in the Start menu, under Qt 3.1.2
Evaluation, with an icon called Designer. Once loaded, you should see a screen
similar to the following.
Demo Game Part 1 — Building the GUI | 257
Designing the GUI

Figure 6-6: Qt Designer

Select File 4 New… from the main menu to see the following window:

Figure 6-7: Creating a new dialog


258 | Chapter 6
Designing the GUI

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.

Figure 6-8: A blank dialog (form)

We can now begin designing our first dialog, the login dialog.

The Login Dialog


In the Login dialog, we will only be asking the user for his or her username and
password to access the server. We could also easily add the name of the server
and port it is running on here, but from the game’s point of view, it is cleaner to
simply ask the user for the username and password and hide all the technical
info.
Additionally, we will need a button on the dialog to allow players to create an
account if they do not already have one and a button to allow them to log in
once they have entered their username and password.
So, first, let’s change the name of the dialog from Form1 to LoginDialog. To
do this, ensure the form is selected by clicking on it, then change the Name
property, shown in the Property Editor/Signal Handlers panel at the bottom-
right, to LoginDialog. This change can be seen in the following screen shot:
Demo Game Part 1 — Building the GUI | 259
Designing the GUI

Figure 6-9: Changing the Name property

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.

Figure 6-10: Changing the Name property

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

Figure 6-11: Renamed and resized dialog

Next, we are going to add a logo to the window to make it


look better, and perhaps more like a login screen. To do
this, select the Display tab in the left-hand panel, so that the
options shown in Figure 6-12 are visible.
From this tab, select PixmapLabel by left-clicking on it,
then click within the Login dialog to place the Pixmap-
Label GUI object. The Login dialog should now look
similar to the following.

Figure 6-12: Display


options
Demo Game Part 1 — Building the GUI | 261
Designing the GUI

Figure 6-13: Placing the PixmapLabel

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

Figure 6-14: The correct logo

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

Figure 6-15: The Username text input box

Again, we want to rename this to something more meaningful, so let’s change


the Name property to read Username, as this will be the text input box for the
player’s username.
Next, make a copy (or place a new LineEdit) below the Username box, and
change its name to Password. Also, since this new text input box will be accept-
ing a password, it is better if we make each character “*” rather than the player’s
actual input for security reasons, so select Password in the echoMode property
drop-down menu. This, along with the placement of the new LineEdit, can be
seen in the following screen shot.
264 | Chapter 6
Designing the GUI

Figure 6-16: Setting the password input to echo * characters

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

Figure 6-17: Adding the text labels

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

Figure 6-18: The Create Player and Login buttons

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

The Signup Dialog


If the player does not have a game account, he or she will need some facility to
create one. Feel free to expand upon this; however, the aim of this signup dialog
is to take some basic information from the user and create an account on the
server so he or she can log in using it.
Let’s start by closing any open dialogs (such as the Login one) and creating a
new dialog in the same manner as we created one in the previous section. Once a
new dialog is created, resize it, and change its name to SignupDialog and its
caption to read Signup. Once this is done, it should look something similar to
Figure 6-19.

Figure 6-19: The Signup dialog

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.

Figure 6-20: Adding the text input boxes

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

Figure 6-21: Adding the buttons

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

Then, for the Signup button, we require the following:


c:signalClicked,signupSink::DoSignup

Now that this is done, save it as signupdialog.ui in the same folder as the
logindialog.ui:
c:\crystal\CS\data\temp

The Error Dialog


The next dialog on the “to do” list is the Error dialog. The aim of this dialog is
to provide a simple message box in which we can change the label (message)
displayed from within the code to inform users of errors that occur during the
login and signup process.
270 | Chapter 6
Designing the GUI

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:

Figure 6-22: The Error dialog

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

Figure 6-23: GUI placement

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.

The Chat Dialog


The last of our four dialogs is the chat dialog, which will be used within the
actual game to allow players to chat with each other. Therefore, we will need an
area to display the chat messages, as well as an input box so the players can type
in their messages and a clickable send button to send the messages.
272 | Chapter 6
Designing the GUI

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:

Figure 6-24: The Chat dialog

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

Figure 6-25: Adding the chat message area

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

Figure 6-26: The completed Chat dialog

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.

Converting the Dialogs


In their current form, the dialogs are of no use to Crystal Space. Qt Designer
saves the GUI definitions in a reasonably easy to understand XML format. For
example, here is what errordialog.ui looks like when we open it in Notepad.
Listing 6-1: errordialog.ui
<!DOCTYPE UI><UI version="3.1" stdsetdef="1">
<class>ErrorDialog</class>
<widget class="QDialog">
<property name="name">
<cstring>ErrorDialog</cstring>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>345</width>
Demo Game Part 1 — Building the GUI | 275
Converting the 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

When executed, it should look as follows in the command prompt window:

Figure 6-27: Converting the dialogs

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

component "ErrorLabel" is "Label"


{
Frame: (10,20) - (330,70)
Caption: "ErrorLabel"
Align:
}
}

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

QPushButton Command Button

QRadioButton Radio Button

QSlider Scroll Bar

QButtonGroup Group Frame

QGroupBox Group Frame

QFrame Group Frame

QCheckBox Check Box

QLineEdit Text Box

QTextView Multiline Edit

QLabel Label

QLabel with Pixmap Image View

QListBox List Box

QListView List Box

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

Listing 6-3: logindialog.txt


window "LoginDialog"
{
Style: wfsNormal
Options: wfoGrip+wfoTitle+wfoClose+wfoMin+wfoZoom+wfoControl
Frame: (0,0) - (458,229)
Title: "Login"
component "Logo" is "Image View"
{
Frame: (30,30) - (130,130)
}
component "Username" is "Text Box"
{
Frame: (170,50) - (380,70)
}
component "Password" is "Text Box"
{
Frame: (170,100) - (380,120)
Masked: Yes
MaskChar: "*"
}
component "textLabel1" is "Label"
{
Frame: (170,30) - (270,50)
Caption: "Username..."
}
component "textLabel1_2" is "Label"
{
Frame: (170,80) - (270,100)
Caption: "Password..."
}
component "Login" is "Command Button"
{
Frame: (270,150) - (420,200)
Caption: "Login ->"
connect
{
signalClicked -> loginSink::Login
}
}
component "CreatePlayer" is "Command Button"
{
Frame: (30,150) - (180,200)
Caption: "Create Player"
connect
{
signalClicked -> loginSink::CreatePlayer
}
}
}
Demo Game Part 1 — Building the GUI | 279
Converting the Dialogs

Listing 6-4: signupdialog.txt


window "SignupDialog"
{
Style: wfsNormal
Options: wfoGrip+wfoTitle+wfoClose+wfoMin+wfoZoom+wfoControl
Frame: (0,0) - (369,405)
Title: "Signup"
component "textLabel1" is "Label"
{
Frame: (30,30) - (82,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..."
}
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) - (150,280)
Caption: "Confirm Password..."
}
component "SignupFullname" is "Text Box"
{
Frame: (40,50) - (330,70)
}
component "SignupEmail" is "Text Box"
{
Frame: (40,110) - (330,130)
}
component "SignupUsername" is "Text Box"
{
Frame: (40,170) - (330,190)
}
component "SignupPassword" is "Text Box"
{
Frame: (40,220) - (330,240)
Masked: Yes
MaskChar: "*"
}
component "SignupConfirmPassword" is "Text Box"
{
Frame: (40,280) - (330,300)
Masked: Yes
280 | 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
}
}
}

Listing 6-5: chatdialog.txt


window "ChatDialog"
{
Style: wfsNormal
Options: wfoGrip+wfoTitle+wfoClose+wfoMin+wfoZoom+wfoControl
Frame: (0,0) - (575,160)
Title: "Chat"
component "ChatInput" is "Text Box"
{
Frame: (10,120) - (420,140)
Style: fsSunken
}
component "ChatSend" is "Command Button"
{
Frame: (430,120) - (561,151)
Caption: "Send ->"
connect
{
signalClicked -> chatSink::Send
}
}
}

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

Listing 6-6: “Normal windows” definition (to be placed into


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"
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"
}

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

Instead of the above, it should read:


wfoGrip+wfoTitle+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"
}

component "Username" is "Text Box"


{
Frame: (170,50) - (380,70)
}
component "Username_2" is "Text Box"
{
284 | Chapter 6
Converting the Dialogs

Frame: (170,100) - (380,120)


Masked: Yes
MaskChar: "*"
}
component "textLabel1" is "Label"
{
Frame: (170,30) - (270,50)
Caption: "Username..."
}
component "textLabel1_2" is "Label"
{
Frame: (170,80) - (270,100)
Caption: "Password..."
}
component "Login" is "Command Button"
{
Frame: (270,150) - (420,200)
Caption: "Login ->"
connect
{
signalClicked -> loginSink::Login
}
}
component "CreatePlayer" is "Command Button"
{
Frame: (30,150) - (180,200)
Caption: "Create Player"
connect
{
signalClicked -> loginSink::CreatePlayer
}
}
}

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

component "SignupPassword" is "Text Box"


{
Frame: (40,220) - (330,240)
Masked: Yes
MaskChar: "*"
}
component "SignupConfirmPassword" is "Text Box"
{
Frame: (40,280) - (330,300)
Masked: Yes
MaskChar: "*"
}
component "SignupCancel" is "Command Button"
{
Frame: (30,330) - (141,371)
Caption: "Cancel"
connect
{
signalClicked -> signupSink::SignupCancel
}
}
component "DoSignup" is "Command Button"
{
Frame: (220,330) - (331,371)
Caption: "Signup ->"
connect
{
signalClicked -> signupSink::DoSignup
}
}
}

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
}
}
}

Testing the GUI


Now that we have created our AWS definition file, it would be a good idea to
create a simple application to allow us to test the dialogs before we use them in
our real application.
To do this, first create a new Crystal Space project called appdemogametut1
in the same manner as previous examples, then compile the following source
and header files.
Listing 6-8: demogametut1.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 "iaws/aws.h" // ADDED
#include "iaws/awscnvs.h" // ADDED
#include "demogametut1.h"
#include "iutil/eventq.h"
#include "iutil/event.h"
#include "iutil/objreg.h"
#include "iutil/csinput.h"
#include "iutil/virtclk.h"
Demo Game Part 1 — Building the GUI | 287
Testing the GUI

#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

// The global pointer to our application...


Butterfly *butterfly;

Butterfly::Butterfly (iObjectRegistry* object_reg)


{
Butterfly::object_reg = object_reg;
}

Butterfly::~Butterfly ()
{
}

void Butterfly::SetupFrame ()
{
if(!g3d->BeginDraw (CSDRAW_2DGRAPHICS)) return;

g2d->Clear(0);

// draw text...
288 | Chapter 6
Testing the GUI

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, 64);
}

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

bool Butterfly::HandleEvent(iEvent& ev)


{
if (ev.Type == csevBroadcast && ev.Command.Code == cscmdProcess)
{
butterfly->SetupFrame ();
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 aws->HandleEvent(ev);
}

bool Butterfly::SimpleEventHandler (iEvent& ev)


{
return butterfly->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;
}

if (!csInitializer::SetupEventHandler (object_reg, SimpleEventHandler))


{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Can't initialize event handler!");
return false;
}

// Check for commandline help.


if (csCommandLineHelper::CheckHelp (object_reg))
{
csCommandLineHelper::Help (object_reg);
return false;
}

// The virtual clock.


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

// Find the pointer to engine plug-in


engine = CS_QUERY_REGISTRY (object_reg, iEngine);
if (engine == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iEngine plugin!");
return false;
}

loader = CS_QUERY_REGISTRY (object_reg, iLoader);


if (loader == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iLoader plugin!");
290 | Chapter 6
Testing the GUI

return false;
}

g2d = CS_QUERY_REGISTRY (object_reg, iGraphics2D);


if (!g2d)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iGraphics2D plugin!");
return false;
}

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;
}

kbd = CS_QUERY_REGISTRY (object_reg, iKeyboardDriver);


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

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;
}

// 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;
}

// get the AWS canvas...


Demo Game Part 1 — Building the GUI | 291
Testing the GUI

awsCanvas = csPtr<iAwsCanvas> (aws->CreateCustomCanvas(g2d, g3d));

// prevent window trails...


aws->SetFlag(AWSF_AlwaysRedrawWindows);
aws->SetCanvas(awsCanvas);

// 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!");

aws->GetPrefMgr ()->SelectDefaultSkin ("Normal Windows");

iAwsWindow *test = aws->CreateWindowFrom ("LoginDialog");


if (test) test->Show ();

txtmgr = g3d->GetTextureManager ();

font = g2d->GetFontServer()->LoadFont(CSFONT_LARGE);

return true;
}

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

int main (int argc, char* argv[])


{
iObjectRegistry* object_reg = csInitializer::CreateEnvironment (argc, argv);

butterfly = new Butterfly (object_reg);


if(butterfly->Initialize ())
butterfly->Start ();
delete butterfly;

font = NULL;

csInitializer::DestroyApplication (object_reg);
return 0;
}

Listing 6-9: demogametut1.h


#ifndef __BUTTERFLY_H__
#define __BUTTERFLY_H__

#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]

static bool SimpleEventHandler (iEvent& ev);


bool HandleEvent (iEvent& ev);
void SetupFrame ();
void FinishFrame ();

void DrawFrame2D ();


public:
Butterfly (iObjectRegistry* object_reg);
~Butterfly ();

bool Initialize ();


void Start ();
};

#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.

Figure 6-28: The Login dialog

If we then close the application and change the following line of code in the Ini-
tialize method:
iAwsWindow *test = aws->CreateWindowFrom ("LoginDialog");

to instead read as follows:


iAwsWindow *test = aws->CreateWindowFrom ("SignupDialog");

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

Figure 6-29: The Signup dialog

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

Figure 6-30: The Error dialog

Figure 6-31: The Chat dialog

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

Starting with the 2D example in Chapter 3, “Getting Started with Crystal


Space,” as a base for the code, we have removed the LoadPixMaps method from
both the header and the source file, and also the associated variables for loading
the logo.
The first addition is in the header file, where we have defined the following
two variables as members of our Butterfly class:
csRef<iAws> aws;
csRef<iAwsCanvas> awsCanvas;

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");

After this, we create a canvas by making a call to the CreateCustomCanvas


method of the iAws interface object aws, in which we specify the 2D and 3D
graphics objects that will be used to render onto the canvas with. The line of
code used to create the iAwsCanvas object can be seen here:
awsCanvas = csPtr<iAwsCanvas> (aws->CreateCustomCanvas(g2d, g3d));
Demo Game Part 1 — Building the GUI | 297
Testing the GUI

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.

For our application, we are only going to use the AWSF_AlwaysRedrawWin-


dows flag as we will be re-rendering the entire screen every frame. To set this
flag, we call the SetFlag method of the iAws interface:
aws->SetFlag(AWSF_AlwaysRedrawWindows);

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);

We then acquire a pointer to the AWS preference manager by making a call to


the GetPrefMgr method of the iAws interface. Using this pointer, we can call the
Load method to load in the definition file we created earlier, called
demogame.def. Note that we access this via the VFS, which is mounted under
the VFS path /this/data/temp/demogame.def.
The segment of code used to load in our definition file is shown here:
if(!aws->GetPrefMgr()->Load("/this/data/temp/demogame.def"))
csReport(object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"couldn't load definition file!");

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();

After initialization, the next modification is in the HandleEvent method. Instead


of simply returning false from the method, we pass any unhandled events into
AWS by calling the HandleEvent method of the iAws interface, passing in the
event object ev passed into the application’s HandleEvent method. This can be
seen in the following segment of code:
bool Butterfly::HandleEvent(iEvent& ev)
{
if (ev.Type == csevBroadcast && ev.Command.Code == cscmdProcess)
{
butterfly->SetupFrame ();
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 aws->HandleEvent(ev);
}

Finally, we have to make a modification to the rendering SetupFrame method to


tell AWS to redraw and print to the screen. This can be seen in the following two
lines of code:
Demo Game Part 1 — Building the GUI | 299
Adding the Sinks

aws->Redraw();
aws->Print(g3d, 64);

Adding the Sinks


In this final section of this chapter, we will be making the GUI components
work together. At the moment we have four unrelated dialogs that we can show
independently of one another. What we are going to do now is validate the data
the user enters and react to events such as button clicks and errors the user
makes while using our GUI system.
First let us look at the complete code for this section, and then we will look at
how the GUI was made to all work together correctly in detail.
Listing 6-9: demogametut1.cpp (part 2)
#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" // ADDED
#include "iaws/awscnvs.h" // ADDED
#include "iaws/awsparm.h" // ADDED
#include "demogametut1.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"
#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"
300 | Chapter 6
Adding the Sinks

#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

// The global pointer to our application...


Butterfly *butterfly;

Butterfly::Butterfly (iObjectRegistry* object_reg)


{
Butterfly::object_reg = object_reg;
}

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::HandleEvent(iEvent& ev)


{
Demo Game Part 1 — Building the GUI | 301
Adding the Sinks

if (ev.Type == csevBroadcast && ev.Command.Code == cscmdProcess)


{
butterfly->SetupFrame ();
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 butterfly ? aws->HandleEvent(ev) : false;


}

bool Butterfly::SimpleEventHandler (iEvent& ev)


{
return butterfly->HandleEvent (ev);
}

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;
}

if (!csInitializer::SetupEventHandler (object_reg, SimpleEventHandler))


{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Can't initialize event handler!");
return false;
}
302 | Chapter 6
Adding the Sinks

// Check for commandline help.


if (csCommandLineHelper::CheckHelp (object_reg))
{
csCommandLineHelper::Help (object_reg);
return false;
}

// The virtual clock.


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

// Find the pointer to engine plug-in


engine = CS_QUERY_REGISTRY (object_reg, iEngine);
if (engine == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iEngine plugin!");
return false;
}

loader = CS_QUERY_REGISTRY (object_reg, iLoader);


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

g2d = CS_QUERY_REGISTRY (object_reg, iGraphics2D);


if (!g2d)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iGraphics2D plugin!");
return false;
}

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;
}
Demo Game Part 1 — Building the GUI | 303
Adding the Sinks

kbd = CS_QUERY_REGISTRY (object_reg, iKeyboardDriver);


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

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;
}

// 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;
}

// get the AWS canvas...


awsCanvas = csPtr<iAwsCanvas> (aws->CreateCustomCanvas(g2d, g3d));

// prevent window trails...


aws->SetFlag(AWSF_AlwaysRedrawWindows);
aws->SetCanvas(awsCanvas);

// ADDED part 2

// Register the sinks...


iAwsSink* sink = aws->GetSinkMgr ()->CreateSink ((void*)this);
sink->RegisterTrigger ("Login", &Login);
sink->RegisterTrigger ("CreatePlayer", &CreatePlayer);
aws->GetSinkMgr ()->RegisterSink ("loginSink", sink);

sink = aws->GetSinkMgr ()->CreateSink((void*)this);


sink->RegisterTrigger ("SignupCancel", &SignupCancel);
sink->RegisterTrigger ("DoSignup", &DoSignup);
aws->GetSinkMgr ()->RegisterSink ("signupSink", sink);

sink = aws->GetSinkMgr ()->CreateSink((void*)this);


sink->RegisterTrigger ("ErrorOk", &ErrorOk);
304 | Chapter 6
Adding the Sinks

aws->GetSinkMgr ()->RegisterSink ("errorSink", sink);

sink = aws->GetSinkMgr ()->CreateSink((void*)this);


sink->RegisterTrigger ("Send", &SendChatMessage);
aws->GetSinkMgr ()->RegisterSink ("chatSink", sink);

// [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!");

aws->GetPrefMgr ()->SelectDefaultSkin ("Normal Windows");

csReport (object_reg, CS_REPORTER_SEVERITY_NOTIFY,


"crystalspace.application.butterfly",
"Constructing GUI...");

// ADDED part 2
loginDialog = aws->CreateWindowFrom("LoginDialog");
signupDialog = aws->CreateWindowFrom("SignupDialog");
errorDialog = aws->CreateWindowFrom("ErrorDialog");
chatDialog = aws->CreateWindowFrom("ChatDialog");

// center the AWS components...


double winWidth, winHeight;
csRect r;

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);

// move to bottom (centered)


r = chatDialog->Frame();
winWidth = r.xmax - r.xmin;
winHeight = r.ymax - r.ymin;
chatDialog->MoveTo((g2d->GetWidth()/2) - winWidth/2, g2d->GetHeight() -
Demo Game Part 1 — Building the GUI | 305
Adding the Sinks

(winHeight+5));

// [END]
loginDialog->Show();

txtmgr = g3d->GetTextureManager ();

font = g2d->GetFontServer()->LoadFont(CSFONT_LARGE);

return true;
}

Y
// Login dialog methods...

{ FL
void Butterfly::Login(void* awst, iAwsSource *)

Butterfly* appPtr = (Butterfly *)awst;


AM
printf ("Login Button Pressed\n");

iString *password;
TE

appPtr->loginDialog->FindChild("Username")->GetProperty("Text", (void **)


&appPtr->username);
appPtr->loginDialog->FindChild("Password")->GetProperty("Text", (void **)
&password);

printf("Username was %s\n", appPtr->username->GetData());


printf("Password was %s\n", password->GetData());

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)
{

// show the error and set the error status...


csRef<iString> errStr = csPtr<iString> (new scfString ("Password
invalid"));

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);
}

void Butterfly::CreatePlayer(void* awst, iAwsSource *)


{
Butterfly* appPtr = (Butterfly *)awst;
printf ("Create Player Button Pressed\n");
fflush (stdout);

appPtr->loginDialog->Hide();
appPtr->signupDialog->Show();
}

// Signup dialog methods...


void Butterfly::SignupCancel(void* awst, iAwsSource *)
{
Butterfly* appPtr = (Butterfly *)awst;
printf ("Cancel Signup Button Pressed\n");
fflush (stdout);

appPtr->signupDialog->Hide();
appPtr->loginDialog->Show();
}

void Butterfly::DoSignup(void* awst, iAwsSource *)


{
Butterfly* appPtr = (Butterfly *)awst;
printf ("Do Signup Button Pressed\n");
fflush (stdout);

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);

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();

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)
{

// show the error and set the error status...


csRef<iString> errStr = csPtr<iString> (new scfString ("Email invalid,
please correct this."));

appPtr->errorFromLogin = false;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
else if(!username->GetData() || strlen(username->GetData()) == 0)
{

// show the error and set the error status...


csRef<iString> errStr = csPtr<iString> (new scfString ("Username
invalid, please correct this."));

appPtr->errorFromLogin = false;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
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, please correct this."));

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)
{

// show the error and set the error status...


csRef<iString> errStr = csPtr<iString> (new scfString ("The two
passwords do not match!"));

appPtr->errorFromLogin = false;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
else
{
// Perform the signup here...

// Switch back to the login screen...


appPtr->loginDialog->Show();
}
}

// Error dialog methods...


void Butterfly::ErrorOk(void* awst, iAwsSource *)
{
Butterfly* appPtr = (Butterfly *)awst;
printf ("Error Ok Button Pressed\n"); fflush (stdout);

appPtr->errorDialog->Hide();

if(appPtr->errorFromLogin)
appPtr->loginDialog->Show();
else
appPtr->signupDialog->Show();
}

// Chat dialog methods...


void Butterfly::SendChatMessage(void* awst, iAwsSource *)
{
Butterfly* appPtr = (Butterfly *)awst;
printf ("Chat Send Button Pressed\n"); fflush (stdout);

iString *chatStr;
appPtr->chatDialog->FindChild("ChatInput")->GetProperty("Text", (void **)
&chatStr);

if(chatStr->GetData() && strlen(chatStr->GetData()) > 0)


{
// later this will be sent to all connected players,
// for now it will just be displayed in the chat area...
Demo Game Part 1 — Building the GUI | 309
Adding the Sinks

csRef<iAwsParmList> params = appPtr->aws->CreateParmList();


params->AddInt("row", 0);
params->AddString("string", (*appPtr->username+":
"+*chatStr).GetData());
appPtr->chatDialog->FindChild("ChatArea")->Execute("InsertRow",
params);

// clear the chat string...


csRef<iString> chatStr = csPtr<iString> (new scfString (""));
appPtr->chatDialog->FindChild("ChatInput")->SetProperty("Text",
(void *) chatStr);
}
}

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

int main (int argc, char* argv[])


{
iObjectRegistry* object_reg = csInitializer::CreateEnvironment (argc, argv);

butterfly = new Butterfly (object_reg);


if(butterfly->Initialize ())
butterfly->Start ();
delete butterfly;

font = NULL;

csInitializer::DestroyApplication (object_reg);
return 0;
}

Listing 6-10: demogametut1.h (part 2)


#ifndef __BUTTERFLY_H__
#define __BUTTERFLY_H__

#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;

// Login dialog methods...


static void Login(void* awst, iAwsSource *);
static void CreatePlayer(void* awst, iAwsSource *);

// Signup dialog methods...


static void SignupCancel(void* awst, iAwsSource *);
static void DoSignup(void* awst, iAwsSource *);

// Error dialog methods...


static void ErrorOk(void* awst, iAwsSource *);
bool errorFromLogin;

// Chat dialog methods...


static void SendChatMessage(void* awst, iAwsSource *);
// [END]

static bool SimpleEventHandler (iEvent& ev);


bool HandleEvent (iEvent& ev);
void SetupFrame ();
void FinishFrame ();

public:
Butterfly (iObjectRegistry* object_reg);
~Butterfly ();

bool Initialize ();


void Start ();
};

#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.

Figure 6-32: The Chat dialog in action

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 *);

// Error dialog methods...


static void ErrorOk(void* awst, iAwsSource *);
bool errorFromLogin;

// Chat dialog methods...


static void SendChatMessage(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"

The first, awsparm.h, is used to give us access to the iAwsParmList interface,


which we will be using to supply parameters to some of the GUI objects. The
next two provide the iString functionality. The last three are standard AWS GUI
headers.
Next, moving to the Initialize method, the first addition is the creation of sev-
eral iAwsSink sinks. Basically, using a sink lets us link actions, such as pressing
a button, to a method call. If you remember back to the demogame.def file we
created earlier in this chapter, within the definition for the Login dialog, we had
the following few lines:
component "Login" is "Command Button"
{
Frame: (270,150) - (420,200)
Caption: "Login ->"
connect
{
signalClicked -> loginSink::Login
Demo Game Part 1 — Building the GUI | 313
Adding the Sinks

}
}

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

Therefore, to register these two triggers, we call the RegisterTrigger method of


our sink object, passing in the name of the trigger as a string, i.e., Login and
CreatePlayer, then a function pointer to the method that should be called when
the event occurs. The two lines of code that do this can be seen here.
sink->RegisterTrigger ("Login", &Login);
sink->RegisterTrigger ("CreatePlayer", &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);

sink = aws->GetSinkMgr ()->CreateSink((void*)this);


sink->RegisterTrigger ("ErrorOk", &ErrorOk);
314 | Chapter 6
Adding the Sinks

aws->GetSinkMgr ()->RegisterSink ("errorSink", sink);

sink = aws->GetSinkMgr ()->CreateSink((void*)this);


sink->RegisterTrigger ("Send", &SendChatMessage);
aws->GetSinkMgr ()->RegisterSink ("chatSink", 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

errorDialog->MoveTo((g2d->GetWidth()/2) - winWidth/2, (g2d->GetHeight()/2) -


winHeight/2);

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

The Login Method


The first method we have created is the Login method. Into the method is passed
a pointer to the instance the sink refers to (remember we passed this in when we
created the sinks earlier). Then the source of the iAwsComponent is also passed
in, although we have not assigned this a variable name since we do not require
it.
Within the Login method, the first thing we do is retrieve a pointer to our
Butterfly object from the awst pointer that was passed in, casting it from a void
pointer to be a Butterfly object pointer (remember, these are static methods).
This can be seen in the following line of code:
Butterfly* appPtr = (Butterfly *)awst;

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;

If you remember from before, we also defined an iString pointer as a member of


the Butterfly class, so we can now use these two pointers to retrieve the
username and password entered into the two fields on the Login dialog, before
the button was pressed.
To retrieve this information, we first reference the appPtr, which is the object
of our Butterfly class, to access the loginDialog member. From this, we call the
FindChild method of the iAwsComponent, passing in the name of the

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);

Again, remember how username is a public instance member of our Butterfly


class, so we use appPtr to gain access to it.
Once we have retrieved the data, we can output it to the screen by calling the
GetData method of the iString object, which returns a char array of the string (or
null if no string is contained within it).
printf("Username was %s\n", appPtr->username->GetData());
printf("Password was %s\n", password->GetData());

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.

The CreatePlayer Method


The CreatePlayer method is called when the Create Player button is pressed on
the Login dialog. The idea of this button is to allow the player to proceed to the
Signup dialog to create a new account on the server.
As with the Login method, we first assign the pointer to the Butterfly object
to a local variable called appPtr. Next, we print out that the Create Player button
was pressed using the following line of code:
printf ("Create Player Button Pressed\n");

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();

The SignupCancel Method


In case the user pressed the Create Player button in error or simply changed his
mind about creating an account, the SignupCancel method allows the player to
move from the Signup dialog back to the Login dialog.
This method is linked to the Cancel button on the Signup dialog and simply
displays that it has been pressed to the console, then hides the Signup dialog and
shows the Login one. The complete code for this method can be seen here:
void Butterfly::SignupCancel(void* awst, iAwsSource *)
{
Butterfly* appPtr = (Butterfly *)awst;
printf ("Cancel Signup Button Pressed\n");
fflush (stdout);
318 | Chapter 6
Adding the Sinks

appPtr->signupDialog->Hide();
appPtr->loginDialog->Show();
}

The DoSignup Method


The DoSignup method is what is called when the user accepts his new account
information on the Signup dialog and wishes to proceed with the signup process.
In this method, we assign the Butterfly object and output to the console that
the button has been pressed using the following couple of lines of code:
Butterfly* appPtr = (Butterfly *)awst;
printf ("Do Signup Button Pressed\n");
fflush (stdout);

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

// show the error and set the error status...


csRef<iString> errStr = csPtr<iString> (new scfString ("The two passwords
do not match!"));

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...

// Switch back to the login screen...


appPtr->loginDialog->Show();
}

The ErrorOk Method


The ErrorOk method is called when the OK button on the Error dialog is pressed
by the user. Basically this method notifies the console that the button has been
pressed, hides the dialog, then uses the errorFromLogin Boolean to determine
which dialog to show again, i.e., either the loginDialog or signupDialog,
depending whether it is true or false, respectively. The complete implementation
for this method can be seen here:
void Butterfly::ErrorOk(void* awst, iAwsSource *)
{
Butterfly* appPtr = (Butterfly *)awst;
printf ("Error Ok Button Pressed\n");
fflush (stdout);

appPtr->errorDialog->Hide();

if(appPtr->errorFromLogin)
appPtr->loginDialog->Show();
else
appPtr->signupDialog->Show();
}

The SendChatMessage Method


The final method implemented to handle the buttons being pressed is the
SendChatMessage method.
This method is different from the others we have looked at in that we are
using a multiline edit control to display the chat message input by the user. The
first part of this method is the same, however, in that we store a local copy of the
320 | Chapter 6
Adding the Sinks

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

Demo Game Part 2 —


Signup/Login

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).

Creating a Skeleton CNetworkHandler Class


The aim of the CNetworkHandler is to provide the network functionality for our
demo game, so we want to make it encapsulate as much of the network code as
possible.
To get it started, however, we are going to define the CNetworkHandler class
as follows:
Listing 7-1: CNetworkHandler
class CNetworkHandler : public COMSWrapper
{
private:

public:
CNetworkHandler();
~CNetworkHandler();

bool UpdateThing(BNGUID guidThing, bool bClientControlled);


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);

323
324 | Chapter 7
Creating a Skeleton CNetworkHandler Class

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);

bool CreateThing(BNGUID guidThing);


bool RemoveThing(BNGUID guidThing);

std::list< THINGITEM > m_vThingList;


bool AddThingItem(BNGUID guidThing, iMeshWrapper *pThing);
iMeshWrapper *FindThingItem(BNGUID guidThing);
iMeshWrapper *RemoveThingItem(BNGUID guidThing);
};

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;

// Required to use the std::list::sort function


bool operator == (const ThingItem &Item1) const
{
return (guidThing == Item1.guidThing);
}
} THINGITEM;

So, currently our CNetworkHandler implementation should be the bare bones of


the class, i.e., enough to get it to compile without any functionality. Therefore,
the CNetworkHandler.cpp file should currently look as follows.
Listing 7-2: 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"
Demo Game Part 2 — Signup/Login | 325
Creating a Skeleton CNetworkHandler Class

#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 ()
{
}

void CNetworkHandler::EventThingNew(BNGUID guidThing, BNOBJECTTYPE typeThing)


{
CreateThing(guidThing);
}

void CNetworkHandler::EventAvatarNew(BNGUID guidThing, BNOBJECTTYPE typeThing)


{
CreateThing(guidThing);
}

void CNetworkHandler::EventThingHere(BNGUID guidThing, BNOBJECTTYPE typeThing,


bool bClientControlled)
{
UpdateThing(guidThing, bClientControlled);
}

Team-Fly®
326 | Chapter 7
Creating a Skeleton CNetworkHandler Class

void CNetworkHandler::EventThingSet(BNGUID guidThing, BNOBJECTTYPE typeThing,


bool bClientControlled)
{
UpdateThing(guidThing, bClientControlled);
}

void CNetworkHandler::EventThingDrop(BNGUID guidThing, BNOBJECTTYPE typeThing,


bool bClientControlled)
{
RemoveThing(guidThing);
}

void CNetworkHandler::EventThingGone(BNGUID guidThing, BNOBJECTTYPE typeThing,


bool bClientControlled)
{
RemoveThing(guidThing);
}

bool CNetworkHandler::UpdateThing(BNGUID guidThing, bool bClientControlled)


{
return true;
}

bool CNetworkHandler::CreateThing(BNGUID guidThing)


{
return true;
}

bool CNetworkHandler::RemoveThing(BNGUID guidThing)


{
return true;
}

bool CNetworkHandler::EventAvatarSetPosition(BNGUID guidThing, float fX, float


fY, float fZ)
{
return true;
}

bool CNetworkHandler::EventThingSetPosition(BNGUID guidThing, float fX, float


fY, float fZ)
{
return true;
}

bool CNetworkHandler::AddThingItem(BNGUID guidThing, iMeshWrapper *pThing)


{
return true;
}

iMeshWrapper *CNetworkHandler::FindThingItem(BNGUID guidThing)


{
return NULL;
}
Demo Game Part 2 — Signup/Login | 327
Implementing the Signup

iMeshWrapper *CNetworkHandler::RemoveThingItem(BNGUID guidThing)


{
return NULL;
}

Implementing the Signup


At the time of this writing, there is no standard way to create a signup system for
your game using the Butterfly Grid. So, rather than show some unconventional
way, I am instead going to leave this and post an update for this section on the
Official Butterfly.net Game Developer’s Guide web site.

' http://www.butterflyguide.net

Implementing the Login


The login, however, works perfectly. As we have our GUI in place and also the
basis of our network handling class, we can start looking at how to implement
the login system for the game.
In the previous chapter, instead of attempting any login, we simply showed
the Chat dialog that appears when the user clicks the Login button using the fol-
lowing code segment (within the Butterfly::Login function in demogame.cpp):
else
{
// attempt to log them into the game,
// for now we will just display the chat dialog...
appPtr->chatDialog->Show();
}

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.

Upon successful creation of the OMS, we proceed by calling the SetupCreate-


ThingTable method, passing in the number of client objects, how to create them,
and the array of objects, as we saw in Chapter 5. This call can be seen in the fol-
lowing line of code:
GetOMS()->SetupCreateThingTable(NUM_CLIENT_OBJECTS, CreateArray, ObjectArray);

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);

Then we can proceed by attempting a login. This is done by calling the


ServerLogin method of the OMS class, passing in the username and password
we stored within the OMSWrapper in the previous few lines of code. Note that
we also specify the final parameter as zero to indicate the method should not
timeout.
GetOMS()->ServerLogin(GetUsername(), GetPassword(), 0);

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;

Note that EventLogonPass is simply a virtual method declared within the


OMSWrapper so we can override this method by declaring it in our
CNetworkHandler class.
For the EventLogonPass method, we first output to the console that the login
was successful using the following simple printf statement:
printf("Login Successful!");

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();

Next, we have declared another Boolean and helper method, SetGameActive,


within the Butterfly class to denote if the game is active or not. We will be using
this in the next chapter to determine if the world should be rendered or not. For
now though, we simply set this new active variable to true, using the new
SetGameActive method as shown here:
butterfly->SetGameActive(true);

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);

The complete implementation of the EventLogonPass method can be seen here


for your convenience:
void CNetworkHandler::EventLogonPass()
{
printf("Login Successful!");

butterfly->GetChatDialog()->Show();
butterfly->SetGameActive(true);

csRef<iString> usernameStr = csPtr<iString> (new scfString (m_pcUsername));


csRef<iAwsParmList> params = butterfly->GetAWS()->CreateParmList();
params->AddInt("row", 0);
330 | Chapter 7
Implementing the Login

params->AddString("string", ("Welcome "+*usernameStr+" to the Butterfly Demo


Game!").GetData());
butterfly->GetChatDialog()->FindChild("ChatArea")->Execute("InsertRow",
params);
}

Now let’s run the application using the following information:


username: wordware1
password: BocFemp1
You should see something similar to the following screen shot:

Figure 7-1: Successful login

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;

Therefore, the method we need to override this time is EventLogonFail. In this


method, we can reuse our Error dialog from the previous chapter to display that
the server rejected the login attempt.
Demo Game Part 2 — Signup/Login | 331
Implementing the Login

To do this, we first create our new EventLogonFail method as a member of


our CNetworkHandler class and then print to console that the login has failed, as
shown below:
void CNetworkHandler::EventLogonFail()
{
printf("Login Failed!");

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; }

The complete implementation of the EventLogonFail method can be seen here:


void CNetworkHandler::EventLogonFail()
{
printf("Login Failed!");

csRef<iString> errStr = csPtr<iString> (new scfString ("Server rejected


login!"));

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

Figure 7-2: Failed login

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

Demo Game Part 3 — The


World

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.

Adding Player Communication


In the previous chapter, once a player had logged in, he or she was presented
with the Chat dialog. However, in order to actually enable players to chat with
each other, we first need to do some groundwork.
The first thing we need to do is assign each player a name property that will
be associated with his or her avatar. To do this, we must first modify the XML
Game Configuration Script (GCS), which is used to generate the database
schema on the server.
So, we first need to transfer the XML Game Configuration Script to our local
machine using a Windows utility called PSCP. This is freely available to down-
load at the following URL and is also in the Utilities folder on the companion
CD.

' 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

The underlined sections of the command should, of course, be replaced by your


Butterfly.net account details. Once the command has been executed, the PSCP
utility will ask you for your password. After entering it, you should see some-
thing similar to the following in the console window.

333
334 | Chapter 8
Adding Player Communication

Figure 8-1: Using PSCP to retrieve the XML file

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'?>
<!--

This is the XML Game Configuration Script (GCS)


for the Official Butterfly.net Game Developer's Guide
Andrew Mulholland 2004
Wordware Publishing, Inc.
-->

<Grid organization="Wordware Publishing, Inc">


<!-- Accounts. Note that these sit outside of the world. Three user -->
<!-- accounts are created here, and one daemon account -->
<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>

<!-- 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

<!-- particular example -->

<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

<Property number="258" type="clientset">


<Name value="Name"></Name>
<Type value="String"></Type>
<Allowed type="1"></Allowed>
<Allowed type="2"></Allowed>
</Property>

<!-- 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

<object type="1" GUID="100" Avatar="1">


<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>
</object>
</avatar>
<!-- Begin Identity/Avatar/Object record ========================== -->
<!-- Identity -->
<identity number="2">
<Name value="wordware2"></Name>
<description value="wordware2"></description>
<account value="2"></account>
<avatar value="2"></avatar>
</identity>
<!-- Avatar -->
<avatar number="2">
<object type="1" GUID="101" Avatar="2">
<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="wordware2"></prop>
</properties>
</object>
</avatar>
<!-- Begin Identity/Avatar/Object record ========================== -->
<!-- Identity -->
<identity number="3">
<Name value="butterfly"></Name>
<description value="butterfly"></description>
<account value="3"></account>
<avatar value="3"></avatar>
</identity>
<!-- Avatar -->
<avatar number="3">
<object type="1" GUID="102" Avatar="3">
<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="wordware2"></prop>
</properties>
</object>
</avatar>
<!-- Begin Identity/Avatar/Object record ========================== -->
<!-- Identity -->
Demo Game Part 3 — The World | 337
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>

<!-- Create a few objects for the world -->


<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">
<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>
</world>
</Grid>

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'?>
<!--

This is the XML Game Configuration Script (GCS)


for the Official Butterfly.net Game Developer's Guide
Andrew Mulholland 2004
Wordware Publishing, Inc.
-->
338 | Chapter 8
Adding Player Communication

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>

If the property is assigned to an object of type 2, the value "Random Object"


will be assigned:
<defaults type="2">
<set>
<prop name="Name" value="Random Object"></prop>
</set>
</defaults>
340 | Chapter 8
Adding Player Communication

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

Figure 8-3: The uploaded XML GCS (wordware.xml)

As you can see, there is a command called create-schema. If we execute this


command with the following command:
./create-schema wordware.xml

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.

Figure 8-4: create-schema output


Demo Game Part 3 — The World | 343
Adding Player Communication

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:

Figure 8-5: The /usr/local/bin directory

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:

Figure 8-6: Login rejected

Now execute your equivalent of the wordware-restart command in the


/usr/local/bin directory. After this is executed, you should be able to log in and
the following screen will be visible in the application:
Demo Game Part 3 — The World | 345
Implementing the Chat

Y
FL
AM
TE

Figure 8-7: Login accepted!

Implementing the Chat


Now that we know our changes to the database on the server were successful,
we can look at how to implement the chat system in our game tutorial code.
Firstly, in our ClientObjectDefines.h header file, we have added an additional
attribute to BN_ATTRIB_ENUM, called BN_ATTRIB_NAME. This can be
seen here:
enum BN_ATTRIB_ENUM
{
BN_ATTRIB_ANIMATION = BUTTERFLY_SUBTYPES_MAX + 1,
BN_ATTRIB_IDENTITY,
BN_ATTRIB_NAME,
};

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();

virtual void PureFunction() {};


private:
CEnumAttrib *pIdentity;
CStringAttrib *pName;
};

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);

Then, in the destructor, we add an additional DeleteType call to clean up the


memory used by our new CStringAttrib. This can be seen in the following line
of code:
DeleteType( BN_ATTRIB_NAME, TYPE_CLIENT_SET, pName );

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);

pName = NewStringType(BN_ATTRIB_NAME, TYPE_CLIENT_SET, 32);


if ( pName )
pName->SetAutoProcess(true);
Demo Game Part 3 — The World | 347
Implementing the Chat

CAvatar::~CAvatar()
{
DeleteType( BN_ATTRIB_IDENTITY, BN_ATTRIB_IDENTITY, pIdentity );
DeleteType( BN_ATTRIB_NAME, TYPE_CLIENT_SET, pName );
}

Next, in the CNetworkHandler::UpdateThing method, we need to add in the


handling for receiving updated BN_NAME_ATTRIB messages. In the
non-client controlled section, we need to add the following case statement:
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;

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);

// Update the non-client objects


if ( !bClientControlled )
{
// Try the new GetStatesByGUID function
m_pOMS->GetStatesByGUID(guidThing, vAttributes);

for (uAttrib = 0; uAttrib < vAttributes.size(); uAttrib++)


{
switch ( vAttributes[uAttrib].m_idState )
{
348 | Chapter 8
Implementing the Chat

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;
}

// Must delete the returned memory


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;
}

// Must delete the returned memory


if (( vAttributes[uAttrib].m_Attribute.Type == PROPERTY_BLOB ))
// ( vAttributes[uAttrib].m_Attribute.Type ==
// PROPERTY_LIST_BLOB ))
{
Demo Game Part 3 — The World | 349
Implementing the Chat

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);

for (uAttrib = 0; uAttrib < vAttributes.size(); uAttrib++)


{
switch ( vAttributes[uAttrib].m_idState )
{
case BUTTERFLY_POSITION:
{
}
break;
case BUTTERFLY_VELOCITY:
{
}
break;
case BUTTERFLY_ACCELERATION:
{
}
break;
case BN_ATTRIB_IDENTITY:
break;

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;
}

// Must delete the returned memory


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 )
350 | Chapter 8
Implementing the Chat

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;
}

// Must delete the returned memory


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;
}

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)
{

Recall from ClientObjectDefines.h that BN_THING_AVATAR is enumerated to


the value 1, using the following enumeration:
enum BN_THING_ENUM
{
BN_THING_NULL= 0,
BN_THING_AVATAR,
BN_THING_ANIMAL,
BN_THING_MAX,
};

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);
}

The final addition to the CNetworkHandler class is the addition of an


EventMessageReceived method that overrides the one inherited from the
OMSWrapper class. This method is called upon receipt of a message sent from
the MessageSend method, which we will look at shortly. This EventMessage-
Received method simply outputs to the chat area the username of who sent the
message (which is taken in as a parameter to the method), followed by the mes-
sage that was sent by the user. The complete method can be seen here:
void CNetworkHandler::EventMessageReceived(UWORD uwKey, UWORD uwMessageType,
const char *pcUsername, const char *pcMessage, unsigned short usMessageLength)
{
char tempMessageStr[1024];
sprintf(tempMessageStr, "%s: %s", pcUsername, pcMessage);
csRef<iAwsParmList> params = butterfly->GetAWS()->CreateParmList();
params->AddInt("row", 0);
Demo Game Part 3 — The World | 353
Implementing the Chat

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);

We then create a new vector object of type BNGUIDLIST:


std::vector<BNGUIDLIST> vGUIDList;

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)))
{

Providing this is successfully executed, we then assign the end of our


pcUsername char array with a null value:
pcUsername[uLength] = 0;

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);

After this, we clean up the pcUsername memory as follows:


delete [] 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);

Here is the complete SendChatMessage method for your reference.


void Butterfly::SendChatMessage(void* awst, iAwsSource *)
{
Butterfly* appPtr = (Butterfly *)awst;
printf("Chat message sent");

iString *chatMessage;
appPtr->chatDialog->FindChild("ChatInput")->GetProperty("Text", (void **)
&chatMessage);

std::vector<BNGUIDLIST> vGUIDList;

char *pcUsername = new char[OMS_MAX_STRING_LEN];


unsigned int uLength = 0;

if(pcUsername)
{
networkHandler->GetOMS()->GetGuidList(vGUIDList);

for (unsigned int uIndex = 0; uIndex < vGUIDList.size(); uIndex++)


{
// Get the name
if(BN_SUCCESS(networkHandler->GetOMS()->GetStateByGUID
(vGUIDList[uIndex].guid, BN_ATTRIB_NAME, &pcUsername,
uLength)))
{
pcUsername[uLength] = 0;

// Send the chat message...


Demo Game Part 3 — The World | 355
Adding the World

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

Adding the World


Now that we have the chat up and running, the final part of our demo game is to
TE

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 ();

// Rotate the camera according to keyboard state


float speed = (elapsed_time / 1000.0) * (0.03 * 20);

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'

// Point the camera at the player...


view->GetCamera()->GetTransform().LookAt(player->GetMovable()->
GetPosition()+csVector3(0, 1, 0)-view->GetCamera()->GetTransform
().GetOrigin(), csVector3(0, 1, 0));

// Tell 3D driver we're going to display 3D things...


if (!g3d->BeginDraw(engine->GetBeginDrawFlags() | CSDRAW_3DGRAPHICS))
return;

// Render the world...


view->Draw ();
} // [end of if active]

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

Then after the txtmgr = g3d->GetTextureManager(); line, we need to add the


following code section to initialize the world:
txtmgr->SetVerbose (true);

// Disable the lighting cache.


engine->SetLightingCacheMode (0);

if (!loader->LoadTexture ("butterflytexture", "/lib/butterfly/logo.jpg"))


{
Demo Game Part 3 — The World | 359
Adding the World

csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,


"crystalspace.application.simple",
"Error loading 'butterflytexture' texture!");
return false;
}

iMaterialWrapper* tm =
engine->GetMaterialList ()->FindByName ("butterflytexture");

room = engine->CreateSector ("room");

walls = (engine->CreateSectorWallsMesh (room, "walls")); // MODIFIED


csRef<iThingState> walls_state (SCF_QUERY_INTERFACE (walls->GetMeshObject (),
iThingState));

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->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 ();

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;
}

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;
}

// Create the sprite and add it to the engine.


player = (engine->CreateMeshWrapper(imeshfact, "PlayerSprite", room, csVector3
(-3, 3.1, 3)));

csMatrix3 m;
m.Identity ();
m *= 5.0;
Demo Game Part 3 — The World | 361
Adding the World

csReversibleTransform l_rT=csReversibleTransform ();


l_rT.SetT2O ( m );
l_rT.SetOrigin ( csVector3(0,0,0) );
imeshfact->HardTransform (l_rT);

playerstate = (SCF_QUERY_INTERFACE (player->GetMeshObject(), iSprite3DState));


playerstate->SetAction("stand");
player->DeferUpdateLighting (CS_NLIGHT_STATIC|CS_NLIGHT_DYNAMIC, 10);

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 ();

view = csPtr<iView> (new csView (engine, g3d));


view->GetCamera ()->SetSector (room);
view->GetCamera ()->GetTransform ().SetOrigin (csVector3 (-3, 5, -3));
view->SetRectangle (0, 0, g2d->GetWidth (), g2d->GetHeight ());

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;
}

void Butterfly::TextBoxLostFocus(void* awst, iAwsSource *)


{
// allow movement upon loss of focus...
acceptKeyboardInput = true;
}
// <- ADDED

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

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"
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"
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

Frame: (30,30) - (130,130)


Image: "/lib/butterfly/logo.jpg"
}

component "Username" is "Text Box"


{
Frame: (170,50) - (380,70)
}
component "Password" is "Text Box"
{
Frame: (170,100) - (380,120)
Masked: Yes
MaskChar: "*"
}
component "textLabel1" is "Label"
{
Frame: (170,30) - (270,50)
Caption: "Username..."
}
component "textLabel1_2" is "Label"
{
Frame: (170,80) - (270,100)
Caption: "Password..."
}
component "Login" is "Command Button"
{
Frame: (270,150) - (420,200)
Caption: "Login ->"
connect
{
signalClicked -> loginSink::Login
}
}
component "CreatePlayer" is "Command Button"
{
Frame: (30,150) - (180,200)
Caption: "Create Player"
connect
{
signalClicked -> loginSink::CreatePlayer
}
}
}

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"

Frame: (30,260) - (180,280)


AM
Caption: "Confirm Password..."
}
component "SignupFullname" is "Text Box"
{
TE

Frame: (40,50) - (330,70)


}
component "SignupEmail" is "Text Box"
{
Frame: (40,110) - (330,130)
}
component "SignupUsername" is "Text Box"
{
Frame: (40,170) - (330,190)
}
component "SignupPassword" is "Text Box"
{
Frame: (40,220) - (330,240)
Masked: Yes
MaskChar: "*"
}
component "SignupConfirmPassword" is "Text Box"
{
Frame: (40,280) - (330,300)
Masked: Yes
MaskChar: "*"
}
component "SignupCancel" is "Command Button"
{
Frame: (30,330) - (141,371)
Caption: "Cancel"
connect
{
signalClicked -> signupSink::SignupCancel
}
}
component "DoSignup" is "Command Button"

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
}
}
}

Listing 8-3: demogame.h


#ifndef __BUTTERFLY_H__
#define __BUTTERFLY_H__

#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

csRef<iFont> font; // ADDED


csRef<iMeshWrapper> player; // ADDED
csRef<iSprite3DState> playerstate; // ADDED
bool isWalking;

iCollider* playerCollider; // NEW


iCollider* mapCollider; // NEW
csRef<iMeshWrapper> walls; // NEW

iString *username;

csRef<iAwsWindow> loginDialog;
csRef<iAwsWindow> signupDialog;
csRef<iAwsWindow> errorDialog;
csRef<iAwsWindow> chatDialog;

// Login dialog methods...


static void Login(void* awst, iAwsSource *);
static void CreatePlayer(void* awst, iAwsSource *);

// Signup dialog methods...


static void SignupCancel(void* awst, iAwsSource *);
static void DoSignup(void* awst, iAwsSource *);
368 | Chapter 8
Adding the World

// Error dialog methods...


static void ErrorOk(void* awst, iAwsSource *);
bool errorFromLogin;

// Chat dialog methods...


static void SendChatMessage(void* awst, iAwsSource *);
static void TextBoxClicked(void* awst, iAwsSource *); // ADDED
static void TextBoxLostFocus(void* awst, iAwsSource *); // ADDED

static bool SimpleEventHandler (iEvent& ev);


bool HandleEvent (iEvent& ev);
void SetupFrame ();
void FinishFrame ();

static bool active;


static bool acceptKeyboardInput; // ADDED for movement/chat window

public:
Butterfly (iObjectRegistry* object_reg);
~Butterfly ();

iCollider* CreateCollider (iMeshWrapper* mesh); // ADDED

// New for login...


void SetErrorFromLogin(bool state) { errorFromLogin = state; }
csRef<iAwsWindow> GetErrorDialog(void) { return errorDialog; }
csRef<iAwsWindow> GetChatDialog(void) { return chatDialog; }
void SetGameActive(bool state) { active = state; }
csRef<iAws> GetAWS() { return aws; }

bool Initialize ();


void Start ();
};

extern Butterfly *butterfly;

#endif // __BUTTERFLY_H__

Listing 8-4: demogame.cpp


#include <iostream> // NEW
#include <vector> // NEW

#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 "cstool/collider.h" // ADDED


#include "demogame.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"
#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" // ADDED
#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"
#include "igraphic/image.h"
#include "igraphic/imageio.h"
#include "imap/parser.h"
#include "ivaria/reporter.h"
#include "ivaria/stdrep.h"
#include "csutil/cmdhelp.h"
#include "csutil/csstring.h"
#include "csutil/scfstr.h"
#include "ivaria/collider.h" // ADDED
#include "igeom/polymesh.h" // ADDED

#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

// The global pointer to our application...


Butterfly *butterfly;
CNetworkHandler *networkHandler;

Butterfly::Butterfly (iObjectRegistry* object_reg)


{
Butterfly::object_reg = object_reg;
active = false;
acceptKeyboardInput = true;
}

Butterfly::~Butterfly ()
{
}

void Butterfly::SetupFrame ()
{
networkHandler->Update(100, 10); // NEW

if(active)
{
// ADDED ->

// Store the current transformations (before any movement, etc.)


csReversibleTransform oldPlayerTrans = player->GetMovable()->
GetTransform();

// Check input...
// Get elapsed time from the virtual clock.
csTicks elapsed_time = vc->GetElapsedTicks ();

// Rotate the camera according to keyboard state


float speed = (elapsed_time / 1000.0) * (0.03 * 20);

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 ->

// 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");
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'

// Point the camera at the player...


372 | Chapter 8
Adding the World

view->GetCamera()->GetTransform().LookAt(player->GetMovable()->
GetPosition()+csVector3(0, 1, 0)-view->GetCamera()->
GetTransform ().GetOrigin(), csVector3(0, 1, 0));

// Tell 3D driver we're going to display 3D things...


if (!g3d->BeginDraw(engine->GetBeginDrawFlags() | CSDRAW_3DGRAPHICS))
return;

// Render the world...


view->Draw ();
} // [end of if active]

aws->Redraw();

// Draw the current view of the window system to a


// graphics context with a certain alpha value.
aws->Print (g3d, 64);

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::HandleEvent(iEvent& ev)


{
if (ev.Type == csevBroadcast && ev.Command.Code == cscmdProcess)
{
butterfly->SetupFrame ();
return true;
}
Demo Game Part 3 — The World | 373
Adding the World

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 butterfly ? aws->HandleEvent(ev) : false;


}

bool Butterfly::SimpleEventHandler (iEvent& ev)


{
return butterfly->HandleEvent (ev);
}

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;
}

if (!csInitializer::SetupEventHandler (object_reg, SimpleEventHandler))


{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"Can't initialize event handler!");
return false;
}

// Check for commandline help.


if (csCommandLineHelper::CheckHelp (object_reg))
{
374 | Chapter 8
Adding the World

csCommandLineHelper::Help (object_reg);
return false;
}

// The virtual clock.


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;
}

// Find the pointer to engine plug-in


engine = CS_QUERY_REGISTRY (object_reg, iEngine);
if (engine == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iEngine plugin!");
return false;
}

loader = CS_QUERY_REGISTRY (object_reg, iLoader);


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

g2d = CS_QUERY_REGISTRY (object_reg, iGraphics2D);


if (!g2d)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.butterfly",
"No iGraphics2D plugin!");
return false;
}

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;
}

kbd = CS_QUERY_REGISTRY (object_reg, iKeyboardDriver);


if (kbd == NULL)
{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
Demo Game Part 3 — The World | 375
Adding the World

"crystalspace.application.butterfly",
"No iKeyboardDriver plugin!");
return false;
}

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;
}

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

// Set the window title...


iNativeWindow* nw = g2d->GetNativeWindow ();
if (nw) nw->SetTitle("Demo Game Tutorial 3");

// 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);

// Disable the lighting cache.


engine->SetLightingCacheMode (0);

if (!loader->LoadTexture ("butterflytexture", "/lib/butterfly/logo.jpg"))


{
csReport (object_reg, CS_REPORTER_SEVERITY_ERROR,
"crystalspace.application.simple",
"Error loading 'butterflytexture' texture!");
return false;

Team-Fly®
376 | Chapter 8
Adding the World

iMaterialWrapper* tm =
engine->GetMaterialList ()->FindByName ("butterflytexture");

room = engine->CreateSector ("room");

walls = (engine->CreateSectorWallsMesh (room, "walls")); // MODIFIED


csRef<iThingState> walls_state (SCF_QUERY_INTERFACE (walls->GetMeshObject (),
iThingState));

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

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;
}

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;
}

// Create the sprite and add it to the engine.


player = (engine->CreateMeshWrapper(imeshfact, "PlayerSprite", room,
csVector3 (-3, 3.1, 3)));

csMatrix3 m;
m.Identity ();
m *= 5.0;

csReversibleTransform l_rT=csReversibleTransform ();


l_rT.SetT2O ( m );
l_rT.SetOrigin ( csVector3(0,0,0) );
378 | Chapter 8
Adding the World

imeshfact->HardTransform (l_rT);

playerstate = (SCF_QUERY_INTERFACE (player->GetMeshObject(), iSprite3DState));


playerstate->SetAction("stand");
player->DeferUpdateLighting (CS_NLIGHT_STATIC|CS_NLIGHT_DYNAMIC, 10);

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 ();

view = csPtr<iView> (new csView (engine, g3d));


view->GetCamera ()->SetSector (room);
view->GetCamera ()->GetTransform ().SetOrigin (csVector3 (-3, 5, -3));
view->SetRectangle (0, 0, g2d->GetWidth (), g2d->GetHeight ());

// <- ADDED

// get the AWS canvas...


awsCanvas = csPtr<iAwsCanvas> (aws->CreateCustomCanvas(g2d, g3d));

// prevent window trails...


aws->SetFlag(AWSF_AlwaysRedrawWindows);
aws->SetCanvas(awsCanvas);

// Register the sinks...


iAwsSink* sink = aws->GetSinkMgr ()->CreateSink ((void*)this);
sink->RegisterTrigger ("Login", &Login);
sink->RegisterTrigger ("CreatePlayer", &CreatePlayer);
aws->GetSinkMgr ()->RegisterSink ("loginSink", sink);

sink = aws->GetSinkMgr ()->CreateSink((void*)this);


sink->RegisterTrigger ("SignupCancel", &SignupCancel);
sink->RegisterTrigger ("DoSignup", &DoSignup);
aws->GetSinkMgr ()->RegisterSink ("signupSink", sink);

sink = aws->GetSinkMgr ()->CreateSink((void*)this);


sink->RegisterTrigger ("ErrorOk", &ErrorOk);
Demo Game Part 3 — The World | 379
Adding the World

aws->GetSinkMgr ()->RegisterSink ("errorSink", sink);

sink = aws->GetSinkMgr ()->CreateSink((void*)this);


sink->RegisterTrigger ("Send", &SendChatMessage);
sink->RegisterTrigger ("TextBoxClicked", &TextBoxClicked); // ADDED
sink->RegisterTrigger ("TextBoxLostFocus", &TextBoxLostFocus); // ADDED
aws->GetSinkMgr ()->RegisterSink ("chatSink", sink);

// 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!");

aws->GetPrefMgr ()->SelectDefaultSkin ("Normal Windows");

csReport (object_reg, CS_REPORTER_SEVERITY_NOTIFY,


"crystalspace.application.butterfly", "Constructing GUI...");

loginDialog = aws->CreateWindowFrom("LoginDialog");
signupDialog = aws->CreateWindowFrom("SignupDialog");
errorDialog = aws->CreateWindowFrom("ErrorDialog");
chatDialog = aws->CreateWindowFrom("ChatDialog");

// center the AWS components...


double winWidth, winHeight;
csRect r;

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);

// move the bottom (centered)


r = chatDialog->Frame();
winWidth = r.xmax - r.xmin;
winHeight = r.ymax - r.ymin;
chatDialog->MoveTo((g2d->GetWidth()/2) - winWidth/2,
g2d->GetHeight() - (winHeight+5));
380 | Chapter 8
Adding the World

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

// Login dialog methods...


void Butterfly::Login(void* awst, iAwsSource *)
{
Butterfly* appPtr = (Butterfly *)awst;
printf ("Login Button Pressed\n");

iString *password;

appPtr->loginDialog->FindChild("Username")->GetProperty("Text", (void **)


&appPtr->username);
appPtr->loginDialog->FindChild("Password")->GetProperty("Text", (void **)
&password);

printf("Username was %s\n", appPtr->username->GetData());


printf("Password was %s\n", password->GetData());

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

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)
{

// show the error and set the error status...


csRef<iString> errStr = csPtr<iString> (new scfString ("Password
invalid"));

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);
}

void Butterfly::CreatePlayer(void* awst, iAwsSource *)


{
Butterfly* appPtr = (Butterfly *)awst;
printf ("Create Player Button Pressed\n"); fflush (stdout);

appPtr->loginDialog->Hide();
appPtr->signupDialog->Show();
}

// Signup dialog methods...


void Butterfly::SignupCancel(void* awst, iAwsSource *)
{
Butterfly* appPtr = (Butterfly *)awst;
printf ("Cancel Signup Button Pressed\n"); fflush (stdout);

appPtr->signupDialog->Hide();
appPtr->loginDialog->Show();
}

void Butterfly::DoSignup(void* awst, iAwsSource *)


{
382 | Chapter 8
Adding the World

Butterfly* appPtr = (Butterfly *)awst;


printf ("Do Signup Button Pressed\n"); fflush (stdout);

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);

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();

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)
{

// show the error and set the error status...


csRef<iString> errStr = csPtr<iString> (new scfString ("Email invalid,
please correct this."));

appPtr->errorFromLogin = false;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
else if(!username->GetData() || strlen(username->GetData()) == 0)
{

// show the error and set the error status...


Demo Game Part 3 — The World | 383
Adding the World

csRef<iString> errStr = csPtr<iString> (new scfString ("Username


invalid, please correct this."));

appPtr->errorFromLogin = false;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
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, please correct this."));

appPtr->errorFromLogin = false;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
else if(!confirmPassword->GetData() || strcmp(password->GetData(),
confirmPassword->GetData()) != 0)
{

// show the error and set the error status...


csRef<iString> errStr = csPtr<iString> (new scfString ("The two
passwords do not match!"));

appPtr->errorFromLogin = false;
appPtr->errorDialog->Show();
appPtr->errorDialog->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
}
else
{
// Perform the signup here...

// Switch back to the login screen...


appPtr->loginDialog->Show();
}
}

// Error dialog methods...


void Butterfly::ErrorOk(void* awst, iAwsSource *)
{
Butterfly* appPtr = (Butterfly *)awst;
printf ("Error Ok Button Pressed\n"); fflush (stdout);

appPtr->errorDialog->Hide();

if(appPtr->errorFromLogin)
appPtr->loginDialog->Show();
384 | Chapter 8
Adding the World

else
appPtr->signupDialog->Show();
}

// Chat dialog methods...


void Butterfly::SendChatMessage(void* awst, iAwsSource *)
{
Butterfly* appPtr = (Butterfly *)awst;
printf("Chat message sent");

iString *chatMessage;
appPtr->chatDialog->FindChild("ChatInput")->GetProperty("Text", (void **)
&chatMessage);

std::vector<BNGUIDLIST> vGUIDList;

char *pcUsername = new char[OMS_MAX_STRING_LEN];


unsigned int uLength = 0;

if(pcUsername)
{
networkHandler->GetOMS()->GetGuidList(vGUIDList);

for (unsigned int uIndex = 0; uIndex < vGUIDList.size(); uIndex++)


{
// Get the name
if(BN_SUCCESS(networkHandler->GetOMS()->GetStateByGUID
(vGUIDList[uIndex].guid, BN_ATTRIB_NAME, &pcUsername,
uLength)))
{
pcUsername[uLength] = 0;

// Send the chat message...


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;
}

csRef<iString> emptyStr = csPtr<iString> (new scfString (""));


Demo Game Part 3 — The World | 385
Adding the World

appPtr->chatDialog->FindChild("ChatInput")->SetProperty("Text", emptyStr);
}

// ADDED ->
void Butterfly::TextBoxClicked(void* awst, iAwsSource *)
{
// stop movement while in the chat input
acceptKeyboardInput = false;
}

void Butterfly::TextBoxLostFocus(void* awst, iAwsSource *)


{
// allow movement upon loss of focus...

Y
acceptKeyboardInput = true;
}
// <- ADDED
FL
void Butterfly::Start ()
AM
{
csDefaultRunLoop (object_reg);
}
TE

int main (int argc, char* argv[])


{
iObjectRegistry* object_reg = csInitializer::CreateEnvironment (argc, argv);

networkHandler = new CNetworkHandler();

butterfly = new Butterfly (object_reg);


if(butterfly->Initialize ())
butterfly->Start ();
delete butterfly;
delete networkHandler;

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

Figure 8-8: The merged GUI and 3D 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; }

Next, in the CNetworkHandler.cpp source file, we have made several changes to


the implementations of the methods that are called by the OMS upon the receipt
of events. The first is EventAvatarNew from which we have removed the call to
the CreateThing method at the end of the method. The updated EventAvatarNew
implementation can be seen here:
Demo Game Part 3 — The World | 387
Adding the World

void CNetworkHandler::EventAvatarNew(BNGUID guidThing, BNOBJECTTYPE typeThing)


{
// if this player...
if(guidThing == GetOMS()->GetGUIDAvatar())
{
// set the avatar name (for other players)...
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");

// self ping to retrieve public key...


GetOMS()->MessageFind(GetOMS()->GetGUIDAvatar(), m_pcUsername);
}
}

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 (GetOMS() && FindThingItem(guidThing)==NULL)


{
if (BN_SUCCESS(GetOMS()->GetTypeByGUID(guidThing, typeObject)))
{
sprintf(pcTemp, "Created Thing %ld of type %d", guidThing,
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;

csRef<iMeshWrapper> sprite (butterfly->GetEngine()->


CreateMeshWrapper(butterfly->
388 | Chapter 8
Adding the World

GetSpriteFactory(), "PlayerSprite",
butterfly->GetRoomSector(), csVector3
(pos.x, 3.1, pos.z)));

sprite->GetMovable ()->SetPosition (pos);


sprite->GetMovable ()->UpdateMove ();
csRef<iSprite3DState> spstate (SCF_QUERY_INTERFACE
(sprite->GetMeshObject (), iSprite3DState));
spstate->SetAction ("stand");
sprite->SetZBufMode (CS_ZBUF_USE);
sprite->SetRenderPriority (butterfly->GetEngine()->
GetObjectRenderPriority ());
sprite->DeferUpdateLighting (CS_NLIGHT_STATIC|
CS_NLIGHT_DYNAMIC, 10);

// Save the pointer to the thing


AddThingItem(guidThing, sprite);
}
}
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;
}

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)))
{

If successful, we proceed by checking that the object was an avatar (i.e., a


player) and then we attempt to retrieve the object’s position from the OMS.
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)))
{
Demo Game Part 3 — The World | 389
Adding the World

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)));

sprite->GetMovable ()->SetPosition (pos);


sprite->GetMovable ()->UpdateMove ();
csRef<iSprite3DState> spstate (SCF_QUERY_INTERFACE (sprite->GetMeshObject (),
iSprite3DState));
spstate->SetAction ("stand");
sprite->SetZBufMode (CS_ZBUF_USE);
sprite->SetRenderPriority (butterfly->GetEngine()->GetObjectRenderPriority ());
sprite->DeferUpdateLighting (CS_NLIGHT_STATIC|CS_NLIGHT_DYNAMIC, 10);

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];

printf("Removed Thing %ld", guidThing);

csRef<iMeshWrapper> sprite = FindThingItem(guidThing);


butterfly->GetEngine()->RemoveObject(sprite);

// Remove the pointer to the thing


RemoveThingItem(guidThing);

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

Next, we reimplement the SetThingPosition method as follows:


bool CNetworkHandler::SetThingPosition(BNGUID guidThing, float fX, float fY,
float fZ)
{
bool bRetVal = false;
char pcTemp[256];

// Update the position of the thing in the world


csRef<iMeshWrapper> spThing = FindThingItem(guidThing);
if ( spThing )
{

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; // fixed y pos
pos.z = (vPosition.y - Y_OFFSET) / Y_SCALE;

spThing->GetMovable()->SetPosition (pos);
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;
}

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

#include "csutil/scfstr.h" // NEW


#include "iaws/aws.h" // NEW
#include "iaws/awscnvs.h" // NEW
#include "iaws/awsparm.h" // NEW
#include "imesh/sprite3d.h"
#include "ClientCreates.h"
#include "demogame.h"
#include "CNetworkHandler.h"

CNetworkHandler::CNetworkHandler ()
{
}

CNetworkHandler::~CNetworkHandler ()
{
}

void CNetworkHandler::DoLogin(char *username, char *password)


{
if(CreateOMS(7, 1, "wordware.butterfly.net", "9907", "8002", "8002"))
{
GetOMS()->SetupCreateThingTable(NUM_CLIENT_OBJECTS, CreateArray,
ObjectArray);

SetUsername(username);
SetPassword(password);
SetAvatarName(username);

GetOMS()->ServerLogin(GetUsername(), GetPassword(), 0); // Don't wait


}
else
{
printf("Unable to Create OMS");
}

void CNetworkHandler::EventLogonPass()
{
printf("Login Successful!");

butterfly->GetChatDialog()->Show();
butterfly->SetGameActive(true);

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);

// set the initial position to the origin...


FPOINT3 vPosition;
Demo Game Part 3 — The World | 393
Adding the World

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");

csRef<iString> errStr = csPtr<iString> (new scfString ("Server rejected


login!"));

butterfly->SetErrorFromLogin(true);
butterfly->GetErrorDialog()->FindChild("ErrorLabel")->SetProperty("Caption",
(void *) errStr);
butterfly->GetErrorDialog()->Show();
}

void CNetworkHandler::EventThingNew(BNGUID guidThing, BNOBJECTTYPE typeThing)


{
printf("\n\nNew THING created: GUID = %i\n\n", guidThing);

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);

// Ping this other user...


GetOMS()->MessageFind(GetOMS()->GetGUIDAvatar(), usernameData);

}
else
printf("Failed to get new avatar name!\n");

CreateThing(guidThing);
}
394 | Chapter 8
Adding the World

void CNetworkHandler::EventAvatarNew(BNGUID guidThing, BNOBJECTTYPE typeThing)


{
// if this player...
if(guidThing == GetOMS()->GetGUIDAvatar())
{
// set the avatar name (for other players)...
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");

// self ping to retrieve public key...


GetOMS()->MessageFind(GetOMS()->GetGUIDAvatar(), m_pcUsername);
}
}

void CNetworkHandler::EventThingHere(BNGUID guidThing, BNOBJECTTYPE typeThing,


bool bClientControlled)
{
UpdateThing(guidThing, bClientControlled);
}

void CNetworkHandler::EventThingSet(BNGUID guidThing, BNOBJECTTYPE typeThing,


bool bClientControlled)
{
UpdateThing(guidThing, bClientControlled);
}

void CNetworkHandler::EventThingDrop(BNGUID guidThing, BNOBJECTTYPE typeThing,


bool bClientControlled)
{
}

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
Demo Game Part 3 — The World | 395
Adding the World

(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

static std::vector<CThingAttributeValue> vAttributes;


UINT uAttrib;
char *pcTemp = NULL;

if ( !m_pOMS )
return false;

UpdateThingView(guidThing, bClientControlled);

// Update the non-client objects


if ( !bClientControlled )
{
// Try the new GetStatesByGUID function
m_pOMS->GetStatesByGUID(guidThing, vAttributes);

for (uAttrib = 0; uAttrib < vAttributes.size(); uAttrib++)


{
switch ( vAttributes[uAttrib].m_idState )
{
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.

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;
}

// Must delete the returned memory


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;
}

// Must delete the returned memory


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;
}
}
Demo Game Part 3 — The World | 397
Adding the World

}
else
{
// Try the new GetStatesByGUID function
m_pOMS->GetStatesByGUID(guidThing, vAttributes, false);

for (uAttrib = 0; uAttrib < vAttributes.size(); uAttrib++)


{
switch ( vAttributes[uAttrib].m_idState )
{
case BUTTERFLY_POSITION:
{
}
break;
case BUTTERFLY_VELOCITY:
{
}
break;
case BUTTERFLY_ACCELERATION:
{
}
break;
case BN_ATTRIB_IDENTITY:
break;

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;
}

// Must delete the returned memory


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;
}

// Must delete the returned memory


398 | Chapter 8
Adding the World

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;
}

/*bool CNetworkHandler::CreateThing(BNGUID guidThing)


{
bool bRetVal = false;
char pcTemp[256];
BNOBJECTTYPE typeObject;

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

// FINAL ADDED ->


bool CNetworkHandler::CreateThing(BNGUID guidThing)
{
bool bRetVal = false;
char pcTemp[256];
Demo Game Part 3 — The World | 399
Adding the World

BNOBJECTTYPE typeObject;

if (GetOMS() && FindThingItem(guidThing)==NULL)


{
if (BN_SUCCESS(GetOMS()->GetTypeByGUID(guidThing, typeObject)))
{
sprintf(pcTemp, "Created Thing %ld of type %d", guidThing,
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;

csRef<iMeshWrapper> sprite (butterfly->


GetEngine()->CreateMeshWrapper(butterfly->
GetSpriteFactory(), "PlayerSprite",
butterfly->GetRoomSector(), csVector3
(pos.x, 3.1, pos.z)));

sprite->GetMovable ()->SetPosition (pos);


sprite->GetMovable ()->UpdateMove ();
csRef<iSprite3DState> spstate (SCF_QUERY_INTERFACE
(sprite->GetMeshObject (), iSprite3DState));
spstate->SetAction ("stand");
sprite->SetZBufMode (CS_ZBUF_USE);
sprite->SetRenderPriority (butterfly->GetEngine()->
GetObjectRenderPriority ());
sprite->DeferUpdateLighting (CS_NLIGHT_STATIC|
CS_NLIGHT_DYNAMIC, 10);

// Save the pointer to the thing


AddThingItem(guidThing, sprite);
}
}
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);
400 | Chapter 8
Adding the World

return bRetVal;
}
// <- FINAL ADDED

bool CNetworkHandler::UpdateThingView(BNGUID guidThing, bool bClientControlled)


{
bool bRetVal = false;
char pcTemp[256];

printf("Updated %s Thing %ld\n", bClientControlled ? "Client Controlled" :


"Server", guidThing);

bRetVal = true;

return bRetVal;
}

/*bool CNetworkHandler::RemoveThing(BNGUID guidThing)


{
bool bRetVal = false;
char pcTemp[256];

printf("Removed Thing %ld\n", guidThing);


bRetVal = true;

return bRetVal;
}*/ // REMOVED

// FINAL ADDED ->


bool CNetworkHandler::RemoveThing(BNGUID guidThing)
{
bool bRetVal = false;
char pcTemp[256];

printf("Removed Thing %ld", guidThing);

csRef<iMeshWrapper> sprite = FindThingItem(guidThing);


butterfly->GetEngine()->RemoveObject(sprite);

// Remove the pointer to the thing


RemoveThingItem(guidThing);

bRetVal = true;

return bRetVal;
}
// <- FINAL ADDED

/*bool CNetworkHandler::SetThingPosition(BNGUID guidThing, float fX, float fY,


float fZ)
{
bool bRetVal = false;
Demo Game Part 3 — The World | 401
Adding the World

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

// FINAL ADDED ->


bool CNetworkHandler::SetThingPosition(BNGUID guidThing, float fX, float fY,
float fZ)
{
bool bRetVal = false;
char pcTemp[256];

// Update the position of the thing in the world


csRef<iMeshWrapper> spThing = FindThingItem(guidThing);
if ( spThing )
{

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; // fixed y pos
pos.z = (vPosition.y - Y_OFFSET) / Y_SCALE;

spThing->GetMovable()->SetPosition (pos);
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;
}
// <- FINAL ADDED

bool CNetworkHandler::SetThingOrientation(BNGUID guidThing, float fX, float fY,


float fZ)
{
bool bRetVal = false;
char pcTemp[256];
402 | Chapter 8
Adding the World

printf(" -> Thing %ld rotated to orientation %4.2f, %4.2f, %4.2f\n",


guidThing, fX, fY, fZ);
bRetVal = true;

return bRetVal;
}

bool CNetworkHandler::SetThingVelocity(BNGUID guidThing, float fX, float fY,


float fZ)
{
bool bRetVal = false;
char pcTemp[256];

printf(" -> Thing %ld changed velocity to %4.2f, %4.2f, %4.2f\n",


guidThing, fX, fY, fZ);
bRetVal = true;

return bRetVal;
}

bool CNetworkHandler::SetThingAnimation(BNGUID guidThing, char *pcAnimation)


{
bool bRetVal = false;
char pcTemp[256];

if ( pcAnimation )
{
printf(" -> Thing %ld animation set to %s\n", guidThing, pcAnimation);
bRetVal = true;
}

return bRetVal;
}

bool SetThingIdentity(unsigned int guidThing, long lIdentity)


{
bool bRetVal = false;
char pcTemp[256];

printf(" -> Thing %d identity set to %ld\n", guidThing, lIdentity);


bRetVal = true;

return bRetVal;
}

bool CNetworkHandler::EventAvatarSetPosition(BNGUID guidThing, float fX, float


fY, float fZ)
{
return true;
}

bool CNetworkHandler::EventThingSetPosition(BNGUID guidThing, float fX, float


fY, float fZ)
{
Demo Game Part 3 — The World | 403
Adding the World

return true;
}

bool CNetworkHandler::AddThingItem(BNGUID guidThing, iMeshWrapper *pThing)


{
// return false;

// Make sure that a valid pointer has been passed in


if ( pThing )
{
// Make sure it is not already on the list
if (!FindThingItem(guidThing))
{
THINGITEM Temp;

Temp.guidThing = guidThing;
Temp.spThing = pThing;

m_vThingList.push_back(Temp);

}
return true;
}
return false;
}

iMeshWrapper *CNetworkHandler::FindThingItem(BNGUID guidThing)


{
std::list< THINGITEM >::iterator viterThing;

for (viterThing = m_vThingList.begin(); ((viterThing != m_vThingList.end())


&& (viterThing->guidThing != guidThing)); viterThing++)
;

if ((viterThing != m_vThingList.end()) && (viterThing->guidThing ==


guidThing))
return viterThing->spThing;

return NULL;
}

iMeshWrapper *CNetworkHandler::RemoveThingItem(BNGUID guidThing)


{
csRef<iMeshWrapper> spTemp = 0;
std::list< THINGITEM >::iterator viterThing;

for (viterThing = m_vThingList.begin(); ((viterThing != m_vThingList.end())


&& (viterThing->guidThing != guidThing)); viterThing++)
;

if ((viterThing != m_vThingList.end()) && (viterThing->guidThing ==


guidThing))
{
spTemp = viterThing->spThing;
404 | Chapter 8
Adding the World

m_vThingList.remove(*viterThing);
}

return spTemp;
}

void CNetworkHandler::EventMessageReceived(UWORD uwKey, UWORD uwMessageType,


const char *pcUsername, const char *pcMessage, unsigned short usMessageLength)
{
char tempMessageStr[1024];
sprintf(tempMessageStr, "%s: %s", pcUsername, pcMessage);
csRef<iAwsParmList> params = butterfly->GetAWS()->CreateParmList();
params->AddInt("row", 0);
params->AddString("string", tempMessageStr);
butterfly->GetChatDialog()->FindChild("ChatArea")->Execute("InsertRow",
params);
}

void CNetworkHandler::DisplayOtherState(BNGUID guidThing, CThingAttributeValue


*pAttribute)
{
char pcMessage[1024];
char pcIndex[64];
pcMessage[0] = 0;
pcIndex[0] = 0;

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

csVector3 posVec = player->GetMovable()->GetPosition();


FPOINT3 vPosition;
vPosition.x = (posVec.x * X_SCALE) + X_OFFSET;
vPosition.y = 3.1;
vPosition.z = (posVec.z * X_SCALE) + Z_OFFSET;
FPOINT3 vOrientation = {0.0f,0.0f,0.0f};
networkHandler->GetOMS()->SetMotionByGUID(networkHandler->
GetOMS()->GetGUIDAvatar(), vPosition, vOrientation);
}

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:

Figure 8-9: The final demo

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

Command structure, 64-65 centering on screen, 314-315


Configure Valve Hammer Editor window, changing name of, 258-259
121-124 changing title of, 259-260
ConnectUsingServerInfoFile method, 249 converting, 274-286
CPhysical class, 201-202 directories, setting, 24-25
CreateCollider method, 119-120, 361 DoLogin method, 327-328
CreateCustomCanvas method, 296-297 DoSignup method, 312, 318-319
CreateLight method, 82 DrawScaled method, 59
CreateOMS method, 196-202
E
CreatePlayer method, 312, 317
error checking, 196-198
CreateThing method, 218-220, 387-389
Error dialog, creating, 269-271
CreateVertex method, 78
errordialog.txt file, 276-277
CreateWindowFrom method, 298
errordialog.ui file, 274-275
Crystal Space,
ErrorOk method, 312, 319
collision detection in, 106
event transfer types, 202
compiling, 20-23
event types, 57
example, 36-61, 67-85
EventAvatarNew method, 250, 350-351,
exporting map to, 132-134
386-387
loading map into, 134-148
EventInfo structure, 204-205
obtaining, 13
EventLogonFail method, 330-331
testing installation of, 23
EventLogonPass method, 329-330
updating copy of, 19-20
EventMessageReceived method, 352-353
Crystal Space source code, retrieving, 13-14,
events,
17-19
command, 64-65
CThing derived class attributes, 200-201
joystick, 64
CThingAttribute structure, 213-214
key, 61-63
CThingAttributeUnion, 214
mouse, 63
CThingAttributeValue class, 213
network, 65
CVS client, using, 13-20
OMS, 227-228
CVS server, connecting to, 17-19
EventThingDrop method, 251
D EventThingGone method, 251, 352
Datastore, 4 EventThingHere method, 250
dead reckoning, 5, 191 EventThingNew method, 250
DeleteType method, 346 EventThingSet method, 250
demogame.def file, 282-286 EventThingSetPosition method, 251-252
DemoSky OMS Example 1, 152-190 examples
DemoSky OMS Example 2, 229-247 2D application creation, 36-61
DestroyApplication method, 61 collision detection, 106-121
dialog, Crystal Space, 36-61, 67-85
adding buttons to, 265-266, 268-269, DemoSky OMS, 152-190, 229-247
273-274 dialog creation, 258-274
adding labels to, 264-265, 267-268 Quake 2 model, 85-105
adding logo to, 260
F
adding password box to, 263-264, 268
FindThingItem method, 214-215, 220
adding text boxes to, 262-264, 267-268,
FinishFrame method, 60, 208-209, 406
270-271, 272-274
Index | 409

G LoadMap method, 146


Game Server, 4 LoadMapFile method, 146
game world, defining, 338-339 LoadPixMaps method, 55
Gateway Server, 3 LoadTexture method, 77, 101-102
GetCamera method, 84 Locale, 5
GetChatDialog method, 329 Login dialog, creating, 258-266
GetElapsedTicks method, 84 Login method, 312, 315-317
GetErrorDialog method, 331 login system, implementing, 327-332
GetFirstEvent method, 206 logindialog.txt file, 278
GetGuidList method, 353 logo, adding to dialog, 260-262
GetKeyState method, 58-59, 84
M
GetLights method, 82
map, see also world
GetMotionByGUID method, 215-216,
creating, 121-132
218-219
exporting to Crystal Space format, 132-134
GetNextEvent method, 206-207
loading into Crystal Space, 134-148
GetPrefMgr method, 297
saving, 132
GetProperty method, 318
map editor, see also Valve Hammer Editor
GetSinkMgr method, 313
configuring, 121-124
GetStateByGUID method, 353
downloading, 121
GetStatesByGUID method, 212-213, 217
massively multiplayer, 1
GetTransform method, 84
massively multiplayer online role-playing
GetTypeByGUID method, 218
game, see MMORPG
GUI,
memory, deleting, 209
linking parts of, 299-311
meshes, creating, 192-193
testing, 286-295
MessageFind method, 226, 351-352, 354
H MessageSecureRespond method, 226-227
HandleEvent method, 56-58, 298, 328-329 MessageSend method, 353-354
HandleLogin method, 202 MMORPG, 1-2
HandleSets method, 202 model,
converting to sprite, 99
I
example, 85-105
identities, creating, 340
obtaining, 96
Initialize method, 50, 100-103, 118-120,
scaling, 102-103
146-148
setting action of, 104-105
Internet, using to access Butterfly account, 7-9
mouse events, 63
J Mouse structure, 63
joystick events, 64 Move method, 84
Joystick structure, 64
N
K network events, 65
key events, 61-63 Network Protocol Stack, see NPS
Key structure, 61-62 Network structure, 65
NewStringType method, 346
L
Normal Windows definition, 281
lighting,
NPS, 5
applying, 82
modifying, 101, 147-148
410 | 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

You might also like