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 . . . . . . Introduction . . . . . . . . . . . . What Is Massively Multiplayer? . . Shard Worlds. . . . . . . . . . . . What Is the Butterfly Grid? . . . . Key Aspects . . . . . . . . . . . . Client Libraries. . . . . . . . . Gateway Server . . . . . . . . Datastore/Game Server. . . . . Technology Overview . . . . . . . The Grid . . . . . . . . . . . . Network Protocol Stack (NPS). Locale System . . . . . . . . . Dead Reckoning . . . . . . . . Scripting Game Logic . . . . . Summary. . . . . . . . . . . . . .
. . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1 1 1 2 3 3 3 3 4 4 4 5 5 5 5 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 Introduction. . . . . . . . . . . . . . . . . . Getting Crystal Space . . . . . . . . . . . . Using the CVS Client . . . . . . . . . . . . Getting WinCVS . . . . . . . . . . . . . Configuring WinCVS . . . . . . . . . . . Connecting to the CVS Server . . . . . . Updating the Local Version . . . . . . . . Compiling Crystal Space . . . . . . . . . . . Testing the Installation . . . . . . . . . . Creating Your Own Project. . . . . . . . . . Setting the Directories . . . . . . . . . . Creating and Setting Up the Project . . . Creating the Project Resource File . . . . Creating a Simple 2D Application . . . . Summary . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13 13 13 13 13 14 17 19 20 23 24 24 25 33 36 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 . . Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Obtaining the Latest OMS Client Libraries . . . . . . . . . . . . . Integrating the OMS without the Wrapper . . . . . . . . . . . . . Events . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . OMS_EVENT_LOGON_PASS . . . . . . . . . . . . . . . OMS_EVENT_LOGON_FAIL . . . . . . . . . . . . . . . OMS_EVENT_IDENT_LIST_CHANGE . . . . . . . . . . OMS_EVENT_EMBODY_DONE . . . . . . . . . . . . . OMS_EVENT_EMBODY_FAIL . . . . . . . . . . . . . . OMS_EVENT_THING_NEW . . . . . . . . . . . . . . . OMS_EVENT_THING_HERE . . . . . . . . . . . . . . . OMS_EVENT_THING_SET . . . . . . . . . . . . . . . . OMS_EVENT_THING_DROP . . . . . . . . . . . . . . . OMS_EVENT_THING_GONE . . . . . . . . . . . . . . . OMS_EVENT_MESSAGE_USER_OFFLINE . . . . . . . OMS_EVENT_MESSAGE_USER_PING . . . . . . . . . OMS_EVENT_MESSAGE_RECEIVED . . . . . . . . . . OMS_EVENT_MESSAGE_RECEIVED_SECURE . . . . Integrating the OMS with the Wrapper . . . . . . . . . . . . . . . Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EventThingNew . . . . . . . . . . . . . . . . . . . . . . . EventAvatarNew . . . . . . . . . . . . . . . . . . . . . . . EventThingHere . . . . . . . . . . . . . . . . . . . . . . . EventThingSet . . . . . . . . . . . . . . . . . . . . . . . . EventThingDrop . . . . . . . . . . . . . . . . . . . . . . . EventThingGone . . . . . . . . . . . . . . . . . . . . . . . EventThingSetPosition. . . . . . . . . . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. 149 . 149 . 149 . 151 . 209 . 210 . 210 . 210 . 211 . 218 . 218 . 221 . 221 . 221 . 222 . 222 . 223 . 223 . 226 . 229 . 250 . 250 . 250 . 250 . 250 . 251 . 251 . 251 . 252
Chapter 6 Demo Game Part 1 — Building the GUI Introduction . . . . . . . . . . . . . . . . . . . . . Installing Qt . . . . . . . . . . . . . . . . . . . . . Designing the GUI . . . . . . . . . . . . . . . . . The Login Dialog . . . . . . . . . . . . . . . . The Signup Dialog. . . . . . . . . . . . . . . . The Error Dialog. . . . . . . . . . . . . . . . . The Chat Dialog . . . . . . . . . . . . . . . . . Converting the Dialogs . . . . . . . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . .
. . . . . . . . . . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . .
. 253 . 253 . 253 . 256 . 258 . 267 . 269 . 271 . 274
. . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . .
| v
Contents
Testing the GUI . . . . . . . . . . Adding the Sinks . . . . . . . . . The Login Method. . . . . . . The CreatePlayer Method . . . The SignupCancel Method . . The DoSignup Method . . . . The ErrorOk Method . . . . . The SendChatMessage Method Summary . . . . . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
Chapter 7 Demo Game Part 2 — Signup/Login . . . Introduction . . . . . . . . . . . . . . . . . . . . . . . Creating a Skeleton CNetworkHandler Class . . . . . . Implementing the Signup . . . . . . . . . . . . . . . . Implementing the Login. . . . . . . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . .
. . . . . .
. . . . . . . . . . . . . . . . . . . . . . .
. . . . . .
. . . . . .
. . . . .
. . . . . . . . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . .
. 323 . 323 . 323 . 327 . 327 . 332
Chapter 8 Demo Game Part 3 — The World . Introduction . . . . . . . . . . . . . . . . . . Adding Player Communication . . . . . . . . Implementing the Chat . . . . . . . . . . . . Adding the World . . . . . . . . . . . . . . . Summary . . . . . . . . . . . . . . . . . . .
. . . . . .
. . . . . .
. . . . . . . . . . . . . . . . . . . . . . .
. . . . . .
. . . . . .
. . . . .
. . . . . . . . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . .
. 333 . 333 . 333 . 345 . 355 . 406
. . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
286 299 315 317 317 318 319 319 321
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 persistent 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 basically 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 character 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 platform-independent window into a massively multiplayer world. To the gamer, this technology gives a fresh approach to massively multiplayer 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 translate 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 contribute 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 communication between all players on all the different servers. However, the fact that four servers are used is transparent to the end user.
Figure 1-4: Four shards to one world
Introduction
| 5
Technology Overview
So what this means is that sections of the game world are handled by different 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)
AM FL Y
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 retransmission. Packets can also be flagged as reliable to ensure delivery to the receiver.
Locale System
TE
A Locale within the Butterfly Grid is a convex area within the game world, 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 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’ perception 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 others 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 integrating 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
Important When signing up, be sure to select “Other” in the “How did you Ü hear 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 maintain 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, utilities, 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 circulate 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 mechanism 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 placing 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 discussion 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 features 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 downloaded, 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 Butterfly 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 create 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 database 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 starting 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/
NOTE There are other tools available from the above link. While it is up to Ü you 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 administrative 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:
[email protected]:/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
AM FL Y
Using the CVS Client
Figure 3-2: The WinCvs Preferences window – updated General tab
TE
Next, click on the Globals tab. This will display the following options by default.
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 output 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 snapshot 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
[email protected]) *****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. However, as Crystal Space is continually being updated, the remote repository is also being updated with newer code, such as bug fixes and extra functionality. Therefore, 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 repository” 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 platform 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 following 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_snapshot.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 suggested names in the book. c:\crystal\CS
You will also need to install the DirectX 9 SDK, which is available from the following 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 Configuration… 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 example 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 examples 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 Studio 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
AM FL Y
After we have added this, simply click OK. Note that this step to set the directories will only ever need to be done once, as the settings are global to all projects created in the Visual Studio IDE.
Creating and Setting Up the Project
TE
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 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…. 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 little 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 existing 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 Studio 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 deleting 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 copy echo copy
Moving output to "$(TargetPath)" Moving output to "$(TargetPath)"
CS root. ..\.. MSVC Debug Bin. 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 conformity 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
AM FL Y
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.
Figure 3-34: Adding a “Version” resource
TE
You will then be presented with the following screen:
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 information; 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include
"cssysdef.h" "cssys/sysfunc.h" "iutil/vfs.h" "csutil/cscolor.h" "cstool/csview.h" "cstool/initapp.h" "cstool/cspixmap.h" "butterfly.h" "iutil/eventq.h" "iutil/event.h" "iutil/objreg.h" "iutil/csinput.h" "iutil/virtclk.h" "iengine/sector.h" "iengine/engine.h" "iengine/camera.h" "iengine/light.h" "iengine/statlght.h" "iengine/texture.h" "iengine/mesh.h"
Getting Started with Crystal Space
| 39
Creating Your Own Project #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include
"iengine/movable.h" "iengine/material.h" "imesh/thing/polygon.h" "imesh/thing/thing.h" "imesh/object.h" "ivideo/graph3d.h" "ivideo/graph2d.h" "ivideo/txtmgr.h" "ivideo/texture.h" "ivideo/material.h" "ivideo/fontserv.h" "igraphic/image.h" "igraphic/imageio.h" "imap/parser.h" "ivaria/reporter.h" "ivaria/stdrep.h" "csutil/cmdhelp.h"
CS_IMPLEMENT_APPLICATION // Application Specific... csPixmap* logoImg; int logo_x, logo_y; csRef
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 ifile = loader->LoadImage ("/lib/butterfly/logo.jpg"); csRef 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 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 #include "csutil/ref.h" struct struct struct struct struct struct struct struct struct struct
iObjectRegistry; iEngine; iLoader; iGraphics2D; iGraphics3D; iKeyboardDriver; iVirtualClock; iEvent; iView; iTextureManager;
class Butterfly { private: iObjectRegistry* object_reg; csRef engine; csRef loader; csRef g2d; csRef g3d; csRef kbd; csRef vc; csRef view; csRef 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__
c:\crystal\CS\data
AM FL Y
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): 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). ; Added for Butterfly VFS.Mount.lib/butterfly
= $@data$/butterfly.zip
TE
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 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 #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 methods 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 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 automatically 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 structures we will be using in our class and main code using the following lines: struct struct struct struct struct struct struct struct struct struct
iObjectRegistry; iEngine; iLoader; iGraphics2D; iGraphics3D; iKeyboardDriver; iVirtualClock; iEvent; iView; 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 accurately, 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 definition, 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 engine; csRef loader; csRef g2d; csRef g3d; csRef kbd; csRef vc; csRef view; csRef 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 methods. 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 application. 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 application 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 easiest 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_APPLICATION 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 declaration of the font variable: csRef 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 CreateEnvironment 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 factory 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 documentation, 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 rendering 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_REQUEST_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 messages 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_SEVERITY_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 ();
AM FL Y
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 pointer to an iImage interface that we create a local smart pointer reference to called ifile. This can be seen here: bool Butterfly::LoadPixMaps () {
csRef ifile = loader->LoadImage ("/lib/butterfly/logo.jpg");
TE
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/ 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 second 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 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 interface 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 handle 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, accessing 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 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 interface, 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 calling the FindRGB method of the iGraphics2D interface, which returns an integer representation of the color we requested. In our example we are retrieving a yellow 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 created, 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 reference 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; int Char; int Modifiers; } Key;
// Key code // Character code // Control key state
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
NOTE Although the CSKEY_FIRST and CSKEY_LAST macros are not actually Ü related 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 example, 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 combination 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, csevMouseDown, 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; int x, y; int Button; int Modifiers; } Joystick;
// // // //
Joystick number (1, 2, ...) Joystick x, y Joystick button number Control key state
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 Command structure looks as follows: struct { Code; void *Info; } Command;
// Command code // Command info
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 commands, 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
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 frame of rendering.
cscmdFinalProcess
This command is broadcast right after cscmdPostProcess on each frame of rendering.
cscmdCanvasHidden
This command lets the application know that the application (canvas) is not currently visible to the users (for example, it may have been iconified).
cscmdCanvasExposed
This command lets the application know the application is now visible (when it was previously hidden).
TE
AM FL Y
Command Code Macro
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 interface. 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 chapter 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 functionality 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 (formerly 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include
"cssysdef.h" "cssys/sysfunc.h" "iutil/vfs.h" "csutil/cscolor.h" "cstool/csview.h" "cstool/initapp.h" "cstool/cspixmap.h" "butterfly3d1.h" "iutil/eventq.h" "iutil/event.h" "iutil/objreg.h" "iutil/csinput.h" "iutil/virtclk.h" "iengine/sector.h" "iengine/engine.h"
// MODIFIED
67
68 |
Chapter 4 Basics of 3D in Crystal Space #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include
"iengine/camera.h" "iengine/light.h" "iengine/statlght.h" "iengine/texture.h" "iengine/mesh.h" "iengine/movable.h" "iengine/material.h" "imesh/thing/polygon.h" "imesh/thing/thing.h" "imesh/object.h" "ivideo/graph3d.h" "ivideo/graph2d.h" "ivideo/txtmgr.h" "ivideo/texture.h" "ivideo/material.h" "ivideo/fontserv.h" "igraphic/image.h" "igraphic/imageio.h" "imap/parser.h" "ivaria/reporter.h" "ivaria/stdrep.h" "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 ifile = loader->LoadImage ("/lib/butterfly/logo.jpg"); csRef txt = txtmgr->RegisterTexture (ifile, CS_TEXTURE_2D); txt->Prepare (); logoImg = new csSimplePixmap (txt); return true; }
void Butterfly::SetupFrame ()
Moving into 3D 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());
| 69
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 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 walls(engine->CreateSectorWallsMesh (room, "walls")); csRef 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 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 (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 Basics of 3D in Crystal Space
Listing 4-2: butterfly3d1.h #ifndef __BUTTERFLY_H__ #define __BUTTERFLY_H__ #include #include "csutil/ref.h" iObjectRegistry; iEngine; iLoader; iGraphics2D; iGraphics3D; iKeyboardDriver; iVirtualClock; iEvent; iView; iTextureManager; iFont; iSector; // NEW
AM FL Y
struct struct struct struct struct struct struct struct struct struct struct struct
TE
class Butterfly { private: iObjectRegistry* object_reg; csRef engine; csRef loader; csRef g2d; csRef g3d; csRef kbd; csRef vc; csRef view; csRef txtmgr; csRef room; // NEW csPixmap* logoImg; csRef 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®
| 75
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. Note You may not have noticed but up until now we have been using software Ü rendering. 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 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 Initialize 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 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 walls_state (SCF_QUERY_INTERFACE (walls->GetMeshObject (), iThingState));
Next, we declare a pointer to an iPolygon3D interface, which we can use to create 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 p->CreateVertex p->CreateVertex p->CreateVertex
(csVector3 (csVector3 (csVector3 (csVector3
(-5, 0, 5)); (5, 0, 5)); (5, 0, -5)); (-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 structure 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 represents 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 diagram. 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 polygon (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 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 referenced 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 clipping 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 (new csView(engine, g3d));
Once we have our view object, we can retrieve a reference to the camera associated 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 following 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 determines 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 reference 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 following 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 forward 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 little room.
AM FL Y
Quake 2 Model Example
TE
Now that we know how to make a simple room and render it, in this example we 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 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 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 configuration 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include
"cssysdef.h" "cssys/sysfunc.h" "iutil/vfs.h" "csutil/cscolor.h" "cstool/csview.h" "cstool/initapp.h" "cstool/cspixmap.h" "butterfly3d2.h" // MODIFIED "iutil/eventq.h" "iutil/event.h" "iutil/objreg.h" "iutil/csinput.h" "iutil/virtclk.h" "iengine/sector.h" "iengine/engine.h" "iengine/camera.h" "iengine/light.h" "iengine/statlght.h" "iengine/texture.h" "iengine/mesh.h"
Team-Fly®
86 |
Chapter 4 Quake 2 Model Example #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include
"iengine/movable.h" "iengine/material.h" "imesh/thing/polygon.h" "imesh/thing/thing.h" "imesh/object.h" "imesh/sprite3d.h" // NEW "ivideo/graph3d.h" "ivideo/graph2d.h" "ivideo/txtmgr.h" "ivideo/texture.h" "ivideo/material.h" "ivideo/fontserv.h" "igraphic/image.h" "igraphic/imageio.h" "imap/parser.h" "ivaria/reporter.h" "ivaria/stdrep.h" "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 ifile = loader->LoadImage ("/lib/butterfly/logo.jpg"); csRef 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 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 walls(engine->CreateSectorWallsMesh (room, "walls")); csRef 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 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 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 (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 #include "csutil/ref.h" struct struct struct struct struct struct struct struct struct struct struct struct struct struct
iObjectRegistry; iEngine; iLoader; iGraphics2D; iGraphics3D; iKeyboardDriver; iVirtualClock; iEvent; iView; iTextureManager; iFont; iSector; iMeshWrapper; iSprite3DState;
// NEW // NEW
class Butterfly { private: iObjectRegistry* object_reg; csRef engine; csRef loader; csRef g2d; csRef g3d; csRef kbd; csRef vc; csRef view; csRef txtmgr;
Moving into 3D
| 95
Quake 2 Model Example csRef room; csPixmap* logoImg; csRef font; csRef player; csRef playerstate; bool isWalking;
// NEW // NEW // NEW
static bool SimpleEventHandler (iEvent& ev); bool HandleEvent (iEvent& ev); void SetupFrame (); void FinishFrame ();
AM FL Y
bool LoadPixMaps (); void DrawFrame2D (); public: Butterfly(iObjectRegistry* object_reg); ~Butterfly(); 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 correctly, 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:\crystal\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 following 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 Crystal 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 application, we need to change the fourth line down to read as follows: skin
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 player; csRef playerstate; bool isWalking = false;
The first is an iMeshWrapper, which we will use to store the model transformation 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 covers. 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: skin
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 associate 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 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 segment 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:
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 following 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(); }
AM FL Y
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, followed 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 below. if(kbd->GetKeyState ('d')) { player->GetMovable()->GetTransform().RotateThis(csVector3(0, 1, 0), speed * 4); player->GetMovable()->UpdateMove(); }
TE
Next we add the following line of code to make the camera always face the model. 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 collision 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 handle 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include
"cssysdef.h" "cssys/sysfunc.h" "iutil/vfs.h" "csutil/cscolor.h" "cstool/csview.h" "cstool/initapp.h" "cstool/cspixmap.h" "cstool/collider.h" "butterfly3d3.h" "iutil/eventq.h" "iutil/event.h" "iutil/objreg.h" "iutil/csinput.h" "iutil/virtclk.h" "iengine/sector.h" "iengine/engine.h" "iengine/camera.h" "iengine/light.h" "iengine/statlght.h" "iengine/texture.h" "iengine/mesh.h" "iengine/movable.h" "iengine/material.h" "imesh/thing/polygon.h" "imesh/thing/thing.h" "imesh/object.h" "imesh/sprite3d.h" "ivideo/graph3d.h" "ivideo/graph2d.h" "ivideo/txtmgr.h" "ivideo/texture.h" "ivideo/material.h" "ivideo/fontserv.h" "igraphic/image.h" "igraphic/imageio.h"
// NEW // MODIFIED
Moving into 3D
| 107
Adding Simple Collision Detection #include #include #include #include #include #include
"imap/parser.h" "ivaria/reporter.h" "ivaria/stdrep.h" "ivaria/collider.h" "igeom/polymesh.h" "csutil/cmdhelp.h"
// NEW // NEW
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; mapCollider = NULL;
// NEW // NEW
}
Butterfly::~Butterfly () { }
bool Butterfly::LoadPixMaps () { csRef ifile = loader->LoadImage ("/lib/butterfly/logo.jpg"); csRef 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 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 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 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 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);
TE
AM FL Y
// NEW -> 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; } // <- NEW
engine->Prepare (); view = csPtr (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 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 #include "csutil/ref.h" struct struct struct struct struct struct struct struct
iObjectRegistry; iEngine; iLoader; iGraphics2D; iGraphics3D; iKeyboardDriver; iVirtualClock; iEvent;
Moving into 3D Adding Simple Collision Detection struct struct struct struct struct struct
iView; iTextureManager; iFont; iSector; iSprite3DState; iMeshWrapper;
class Butterfly { private: iObjectRegistry* object_reg; csRef engine; csRef loader; csRef g2d; csRef g3d; csRef kbd; csRef vc; csRef view; csRef txtmgr; csRef room; csRef cdsys; // NEW csPixmap* logoImg; csRef font; csRef player; csRef playerstate; bool isWalking; iCollider* playerCollider; iCollider* mapCollider; csRef walls;
// NEW // NEW // 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__
| 117
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 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 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 concludes 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 forward, 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 collision 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 detection, but I’ll leave you to expand upon it. Note A more advanced method of collision detection is to use the CollidePath Ü method 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 Hammer 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 following 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
TE
AM FL Y
Creating and Loading a Map
Figure 4-22: The main interface
Like me, you’re probably not an artist, but we can create a simple map to demonstrate 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 rectangular 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 creating 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 displays “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 display 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 following 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 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include
"cssysdef.h" "cssys/sysfunc.h" "iutil/vfs.h" "csutil/cscolor.h" "cstool/csview.h" "cstool/initapp.h" "cstool/cspixmap.h" "cstool/collider.h" "csutil/ptrarr.h" // NEW "butterfly3d4.h" // MODIFIED "iutil/eventq.h" "iutil/event.h" "iutil/objreg.h" "iutil/csinput.h" "iutil/virtclk.h" "iengine/sector.h" "iengine/engine.h" "iengine/camera.h" "iengine/light.h" "iengine/statlght.h" "iengine/texture.h" "iengine/mesh.h" "iengine/movable.h" "iengine/material.h" "imesh/thing/polygon.h" "imesh/thing/thing.h" "imesh/object.h" "imesh/sprite3d.h" "ivideo/graph3d.h" "ivideo/graph2d.h" "ivideo/txtmgr.h" "ivideo/texture.h"
Moving into 3D Creating and Loading a Map "ivideo/material.h" "ivideo/fontserv.h" "igraphic/image.h" "igraphic/imageio.h" "imap/parser.h" "ivaria/reporter.h" "ivaria/stdrep.h" "ivaria/collider.h" "igeom/polymesh.h" "csutil/cmdhelp.h"
AM FL Y
#include #include #include #include #include #include #include #include #include #include
CS_IMPLEMENT_APPLICATION
// The global pointer to our application... Butterfly *butterfly;
TE
Butterfly::Butterfly (iObjectRegistry* object_reg) { 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 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®
| 135
136 |
Chapter 4 Creating and Loading a Map return true; } // <- NEW
bool Butterfly::LoadPixMaps () { csRef ifile = loader->LoadImage ("/lib/butterfly/logo.jpg"); csRef 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 ml (engine->GetMeshes()); for(int i=0; iResetCollisionPairs (); 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 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 (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 meshlist (engine->GetMeshes()); for(int i=0; i < meshlist->GetCount(); i++) { csRef 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 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 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 #include "csutil/ref.h" struct struct struct struct struct struct struct struct struct struct struct struct struct struct
iObjectRegistry; iEngine; iLoader; iGraphics2D; iGraphics3D; iKeyboardDriver; iVirtualClock; iEvent; iView; iTextureManager; iFont; iSector; iSprite3DState; iMeshWrapper;
class Butterfly { private: iObjectRegistry* object_reg; csRef engine; csRef loader; csRef g2d; csRef g3d; csRef kbd; csRef vc; csRef view; csRef txtmgr; csRef room; csRef cdsys; csPixmap* logoImg; csRef font; csRef player; csRef playerstate; bool isWalking; iCollider* playerCollider;
Moving into 3D
| 145
Creating and Loading a Map csPArray mapColliderArray; // NEW
static bool SimpleEventHandler (iEvent& ev); bool HandleEvent (iEvent& ev); void SetupFrame (); void FinishFrame (); bool LoadPixMaps (); void DrawFrame2D ();
AM FL Y
iCollider* CreateCollider (iMeshWrapper* mesh); bool Butterfly::LoadMap(); // NEW public: Butterfly(iObjectRegistry* object_reg); ~Butterfly(); bool Initialize(); void Start(); };
TE
#endif // __BUTTERFLY_H__
If we run this in a project that has been set up correctly, we should see something 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 mapColliderArray;
This creates an expandable pointer array for holding iCollider objects. The csPArray is somewhat similar to the C++ STL (standard template libraries) vector 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 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 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 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 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
For each of these, we first reset the collision detection system using the following 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 current 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 following 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 experimenting with code and different ideas. In the next chapter, we are going to get down and dirty with the Object Management 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 distributed 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 chapter, 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 example, 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 chapters, 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 following 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 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include
"cssysdef.h" "cssys/sysfunc.h" "demosky.h" "cstool/proctex.h" "cstool/prsky.h" "cstool/csview.h" "cstool/initapp.h" "csutil/cmdhelp.h" "ivideo/graph3d.h" "ivideo/graph2d.h" "ivideo/natwin.h" "ivideo/txtmgr.h" "ivideo/fontserv.h" "ivaria/conout.h" "imesh/sprite2d.h" "imesh/object.h" "imap/parser.h" "iengine/mesh.h" "iengine/engine.h" "iengine/sector.h" "iengine/camera.h" "iengine/movable.h" "iengine/material.h" "imesh/thing/polygon.h" "imesh/thing/thing.h" "ivaria/reporter.h" "igraphic/imageio.h" "iutil/comp.h" "iutil/eventh.h" "iutil/eventq.h" "iutil/event.h" "iutil/objreg.h" "iutil/csinput.h" "iutil/virtclk.h" "iutil/vfs.h"
// Added #include "imesh/sprite3d.h" #include "grid-oms/oms.h" #include "ClientCreates.h"
| 153
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 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 Integrating the OMS without the Wrapper
// Modified //delete flock; // End delete sky; delete sky_f; delete sky_b; delete sky_l; delete sky_r; delete sky_u; delete sky_d; }
AM FL Y
DemoSky::~DemoSky () { // Added CleanUp(); // End
TE
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); 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®
| 155
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, "sky_f"); sky_b = new csProcSkyTexture(sky); iMaterialWrapper* imatb = sky_b->Initialize(object_reg, "sky_b"); sky_l = new csProcSkyTexture(sky); iMaterialWrapper* imatl = sky_l->Initialize(object_reg, "sky_l"); sky_r = new csProcSkyTexture(sky); iMaterialWrapper* imatr = sky_r->Initialize(object_reg, "sky_r"); sky_u = new csProcSkyTexture(sky); iMaterialWrapper* imatu = sky_u->Initialize(object_reg, "sky_u"); sky_d = new csProcSkyTexture(sky); iMaterialWrapper* imatd = sky_d->Initialize(object_reg, "sky_d");
engine, txtmgr,
engine, txtmgr,
engine, txtmgr,
engine, txtmgr,
engine, txtmgr,
engine, txtmgr,
room = engine->CreateSector ("room"); csRef walls (engine->CreateSectorWallsMesh (room, "walls")); csRef 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, p->CreateVertex (csVector3 (-simi, p->CreateVertex (csVector3 (-simi, p->CreateVertex (csVector3 (-simi,
size, -size)); size, size)); -size, size)); -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 char char char char char
*pcServerIP = "Temporary ServerIP"; *pcServerPort = "Temporary ServerPort"; *pcUsername = "Temporary Username"; *pcPassword = "Temporary Password"; *pcNPSInPort = "8002"; *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 (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 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; }
AM FL Y
void DemoSky::SetUsername(const char *pcUsername) { if ( pcUsername ) { m_pcUsername = new char[strlen(pcUsername) + 1]; if ( m_pcUsername ) strcpy(m_pcUsername, pcUsername); } }
TE
void DemoSky::SetPassword(const char *pcPassword) { 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 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 static std::vector UINT char
bRetVal = false; vAttributes; uAttrib; *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 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 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 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; }
AM FL Y
bool DemoSky::ReceiveProjectile(char *pcData, UINT usDataLength) { bool bRetVal = false; char pcTemp[1024]; char *pcStartPos = pcData; ULONG ulLong;
TE
if ((usDataLength > sizeof(BNGUID) + (sizeof(float) * 4) + 2)) { // Retrieve the data from the data string 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 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 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 but WITHOUT ANY MERCHANTABILITY Library General
distributed in the hope that it will be useful, WARRANTY; without even the implied warranty of or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 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 #include "csgeom/math2d.h" #include "csgeom/math3d.h" // Added #include //** 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 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 view; csRef engine; iMaterialWrapper* matPlasma; csRef font; csRef LevelLoader; csRef myG2D; csRef myG3D; csRef kbd; csRef 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 imeshfact; //** OMS Variables COMS char char char char int int BNGUID // End
*m_pOMS; *m_pcServerIP; *m_pcServerPort; *m_pcUsername; *m_pcPassword; m_iGame; m_iVersion; m_guidAvatar;
/** 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 COMS void BNGUID bool const char void const char void bool void bool
CleanUp(); *GetOMS() { return m_pOMS; } SetAvatar(BNGUID guidThing) {m_guidAvatar = guidThing;} GetAvatar() {return m_guidAvatar;} UpdateThing(BNGUID guidThing, bool bClientControlled); *GetUsername() {return m_pcUsername;} SetUsername(const char *pcUsername); *GetPassword() {return m_pcPassword;} SetPassword(const char *pcPassword); CreateOMS(char *pcIP, char *pcPort, char *pcNPSInPort, char *pcNPSOutPort); Update(); CreateThing(BNGUID guidThing);
182 |
Chapter 5 Integrating the OMS without the Wrapper bool bool bool bool bool bool bool bool bool bool bool
UpdateThingView(BNGUID guidThing, bool bClientControlled); RemoveThing(BNGUID guidThing); ReceiveOffline(char *pcID, ULONG ulKey); ReceivePing(char *pcID, ULONG ulKey); ReceiveMessage(char *pcUsername, char *pcMessage, ULONG ulKey); ReceiveProjectile(char *pcData, UINT usDataLength); ReceiveSecure(EventInfo *pData, char *pcQuestion); SetThingPosition(BNGUID guidThing, float fX, float fY, float fZ); SetThingOrientation(BNGUID guidThing, float fX, float fY, float fZ); SetThingAnimation(BNGUID guidThing, char *pcAnimation); 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 void void bool
Initialize (int argc, const char* const argv[], const char *iConfigName); SetupFrame (); FinishFrame (); 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 extern BNOBJECTTYPE
CreateArray[]; 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 }
AM FL Y
CAvatar::CAvatar(BNGUID guidObject, bool bIsClientControlled, BNGUID guidParentObject /*= GUID_INVALID*/, bool bPreventClobber /*= false*/) : CLiving(BN_THING_AVATAR, guidObject, bIsClientControlled, guidParentObject, bPreventClobber) { pIdentity = NewEnumType(BN_ATTRIB_IDENTITY, BN_ATTRIB_IDENTITY, BNAttribIdentityVALUES, sizeof(BNAttribIdentityVALUES) / sizeof(INT)); if ( pIdentity ) pIdentity->SetAutoProcess(true); } CAvatar::~CAvatar() { DeleteType( BN_ATTRIB_IDENTITY, BN_ATTRIB_IDENTITY, pIdentity ); }
TE
CAnimal::CAnimal(BNGUID guidObject, bool bIsClientControlled, BNGUID guidParentObject /*= GUID_INVALID*/, bool bPreventClobber /*= false*/) : CLiving(BN_THING_ANIMAL, guidObject, bIsClientControlled, guidParentObject, bPreventClobber) { } 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 private: CStringAttrib
PureFunction() = 0; *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 private: };
PureFunction() = 0;
class CAvatar : public CLiving { public: CAvatar(BNGUID guidObject, bool bIsClientControlled, BNGUID guidParentObject = GUID_INVALID, bool bPreventClobber = false); virtual ~CAvatar(); virtual void private: CEnumAttrib };
PureFunction() {}; *pIdentity;
class CAnimal : public CLiving { public: CAnimal(BNGUID guidObject, bool bIsClientControlled, BNGUID guidParentObject = GUID_INVALID, bool bPreventClobber = false); virtual ~CAnimal(); virtual void private: };
PureFunction() {};
#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 different 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 contains 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 forward 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 representation 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. Note Dead reckoning is a term used to denote prediction of what the player is Ü going 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 trajectory 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, "sky_f"); sky_b = new csProcSkyTexture(sky); iMaterialWrapper* imatb = sky_b->Initialize(object_reg, "sky_b"); sky_l = new csProcSkyTexture(sky); iMaterialWrapper* imatl = sky_l->Initialize(object_reg, "sky_l"); sky_r = new csProcSkyTexture(sky); iMaterialWrapper* imatr = sky_r->Initialize(object_reg, "sky_r"); sky_u = new csProcSkyTexture(sky); iMaterialWrapper* imatu = sky_u->Initialize(object_reg, "sky_u"); sky_d = new csProcSkyTexture(sky); iMaterialWrapper* imatd = sky_d->Initialize(object_reg, "sky_d");
engine, txtmgr,
engine, txtmgr,
engine, txtmgr,
engine, txtmgr,
engine, txtmgr,
engine, txtmgr,
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 created 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 constructor, 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 declarations are as follows: char char char char char char
*pcServerIP = "Temporary ServerIP"; *pcServerPort = "Temporary ServerPort"; *pcUsername = "Temporary Username"; *pcPassword = "Temporary Password"; *pcNPSInPort = "8002"; *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 therefore 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 conversion 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; } }
AM FL Y
If there is no delimiter found, we assume that the local ports have not been specified and we use the default ones that were assigned just before attempting to read in the config file. Next, we call two methods called SetUsername and SetPassword, passing in the username and password information we just retrieved from the configuration file. These can be seen here: SetUsername(pcUsername); SetPassword(pcPassword);
TE
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 appropriate 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 initializes 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_INVALID_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 purpose 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 butterfly_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 database. 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 represent 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 private: CEnumAttrib }; class CAnimal : public CLiving
PureFunction() {}; *pIdentity;
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 private: };
PureFunction() {};
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 attributes 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_SUBTYPES_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 private: };
PureFunction() = 0;
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 private: CStringAttrib };
PureFunction() = 0; *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_TRANSFER_TYPE_EVENT.
Integrating the OMS with an Existing Application
| 203
Integrating the OMS without the Wrapper
Note Within the COMS class, there is also a method called SetEventMesÜ sageHandle, which can be used to give the OMS the handle to the window that will receive the event messages through window notification messages. The message 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 contains 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 simplicity. 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. Setting 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 (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 position 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 long bool int
*pEventInfo = NULL; lNumEventsLeft = 1; bStillMore = true; 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; BNOBJECTTYPE typeThing; BNGUID guidTo;
// // // // // // //
This is the GUID of the Thing that the event is about. This is the type of Thing that the event is about. 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 // 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 // only for instant message events // and it is self deleting. char *pcAccept; // This is the prompt to display to // the user for accepting a secure // message. It is used only for secure // instant message events and it is // self deleting. char *pcReject; // This is the prompt to display to // the user for rejecting a secure // 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 *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.
TE
AM FL Y
BNOBJECTTYPE typeTo;
/* * 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 process, 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 client 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 (provided 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 initialize 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 rendering 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 disconnecting 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 milliseconds (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 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 possible 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 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 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 controlled. 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 userdefined 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; BNOBJECTTYPE m_typeObject;
bool m_bDirty; CTHINGATTRIBUTE m_Attribute; public: //! This is the constructor. CThingAttributeValue() {}
// // // // // // // //
This is the state ID. 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. This is true if the state is dirty. This is the value for the state.
//! 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; FLOAT fFloat; FPOINT3 vVector; TOKEN
Token;
CTHINGBLOBATTRIBUTESTRUCT CTHINGSTRINGATTRIBUTESTRUCT } CTHINGATTRIBUTEUNION;
// // // // // // // Blob; String;
This is the long/int value. This is the float value. This is the vector value containing three floats (X, Y, and Z). This is the token value, i.e., a 64-bit value containing the GUID, spec type, and type. // This is the BLOB value. // This is the string value.
As you can see, this CTHINGATTRIBUTEUNION union contains all the possible 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 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 spThing; // Required to use the std::list::sort function bool operator == (const ThingItem &Item1) const { return (guidThing == Item1.guidThing); } } THINGITEM;
AM FL Y
As you can see, this structure basically contains the unique ID of the Thing and also a smart pointer to the mesh that is used to represent the Thing within the 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: std::list< THINGITEM > m_vThingList;
TE
Therefore, what our FindThingItem method does is search through this list, comparing the guidThing passed in with the one contained within each of the 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 variables 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 avatar 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 application 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 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 different 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 examples. The code for this can be seen here for reference: csRef 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 vector, 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 following 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 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 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 contains 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, BN_MESSAGE_TYPE_TEXT_MAX = 0x7F, // All binary data comes after this BN_MESSAGE_TYPE_BINARY_PROJECTILE,
BN_MESSAGE_TYPE_BINARY_MAX, };
// Text-based messages
// // // //
Origin (guid), Target Position (float) X, Y, Z, Velocity (float), Name (string), [Explosion (string)]
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);
AM FL Y
After this, we repeat the process to retrieve the four floating-point values, which are the x, y, z, and velocity of the projectile.
TE
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);
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 additional 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 following 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. Remember 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_ OFFLINE
The user being searched for by COMS::MessageFind is offline. The EventInfo::guidThing contains the user’s public_key. EventInfo::pcMessage and EventInfo::pcUsername contain the username of client.
OMS_EVENT_MESSAGE_USER_PING
The user being searched for by COMS::MessageFind is online. The EventInfo::guidThing contains the user’s public_key. EventInfo::pcMessage 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::pcUsername contains the username of the client that sent the message.
OMS_EVENT_MESSAGE_RECEIVED_ SECURE
A secure instant message was received. The 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 example, 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 Studio 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 but WITHOUT ANY MERCHANTABILITY Library General
distributed in the hope that it will be useful, WARRANTY; without even the implied warranty of or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 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 #include #include #include #include #include #include #include #include #include #include
"cssysdef.h" "cssys/sysfunc.h" "grid-oms/OMSWrapper.h" "demosky.h" "cstool/proctex.h" "cstool/prsky.h" "cstool/csview.h" "cstool/initapp.h" "csutil/cmdhelp.h" "ivideo/graph3d.h" "ivideo/graph2d.h"
230 |
Chapter 5 Integrating the OMS with the Wrapper #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include
"ivideo/natwin.h" "ivideo/txtmgr.h" "ivideo/fontserv.h" "ivaria/conout.h" "imesh/sprite2d.h" "imesh/object.h" "imap/parser.h" "iengine/mesh.h" "iengine/engine.h" "iengine/sector.h" "iengine/camera.h" "iengine/movable.h" "iengine/material.h" "imesh/thing/polygon.h" "imesh/thing/thing.h" "ivaria/reporter.h" "igraphic/imageio.h" "iutil/comp.h" "iutil/eventh.h" "iutil/eventq.h" "iutil/event.h" "iutil/objreg.h" "iutil/csinput.h" "iutil/virtclk.h" "iutil/vfs.h" "imesh/sprite3d.h" "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 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 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 delete delete delete delete delete delete
sky; sky_f; sky_b; sky_l; sky_r; sky_u; 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);
| 231
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, "sky_f"); sky_b = new csProcSkyTexture(sky); iMaterialWrapper* imatb = sky_b->Initialize(object_reg, "sky_b"); sky_l = new csProcSkyTexture(sky); iMaterialWrapper* imatl = sky_l->Initialize(object_reg, "sky_l"); sky_r = new csProcSkyTexture(sky); iMaterialWrapper* imatr = sky_r->Initialize(object_reg, "sky_r"); sky_u = new csProcSkyTexture(sky); iMaterialWrapper* imatu = sky_u->Initialize(object_reg, "sky_u"); sky_d = new csProcSkyTexture(sky); iMaterialWrapper* imatd = sky_d->Initialize(object_reg, "sky_d");
engine, txtmgr,
engine, txtmgr,
engine, txtmgr,
engine, txtmgr,
engine, txtmgr,
engine, txtmgr,
room = engine->CreateSector ("room"); csRef walls (engine->CreateSectorWallsMesh (room, "walls")); csRef 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 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);
AM FL Y
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);
TE
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, p->CreateVertex (csVector3 (-simi, p->CreateVertex (csVector3 (-simi, p->CreateVertex (csVector3 (-simi,
size, -size)); size, size)); -size, size)); -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®
| 235
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 (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 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 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 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 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 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 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 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 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. distributed in the hope that it will be useful, WARRANTY; without even the implied warranty of or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Public License for more details.
AM FL Y
This library is but WITHOUT ANY MERCHANTABILITY Library General
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 */
TE
#ifndef DEMOSKY_H #define DEMOSKY_H
#include #include "csgeom/math2d.h" #include "csgeom/math3d.h"
// Added #include //** 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®
| 245
246 |
Chapter 5 Integrating the OMS with the Wrapper struct struct struct struct struct
iGraphics3D; iGraphics2D; iVirtualClock; iObjectRegistry; iEvent;
// Added typedef struct ThingItem { BNGUID guidThing; csRef 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 view; csRef engine; iMaterialWrapper* matPlasma; csRef font; csRef LevelLoader; csRef myG2D; csRef myG3D; csRef kbd; csRef 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 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 bool
bool bool bool
EventThingSetPosition(BNGUID guidThing, float fX, float fY, float fZ); EventAvatarSetPosition(BNGUID guidThing, float fX, float fY, float fZ); CreateThing(BNGUID guidThing); UpdateThingView(BNGUID guidThing, bool bClientControlled); 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 second 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 wrapper’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 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
AM FL Y
Installing Qt
Figure 6-3: Installation options
TE
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 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 bottomright, 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 PixmapLabel 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 accepting 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 adding 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
TE
AM FL Y
Designing the GUI
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 appropriately 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 property 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 second 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 appropriately, 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 ErrorDialog ErrorDialog 0 0 345
Demo Game Part 1 — Building the GUI
| 275
Converting the Dialogs
TE
AM FL Y
148 Error ErrorOk 120 90 90 40 Ok ErrorLabel 10 20 320 50 ErrorLabel AutoText AlignTop
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 construct 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 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
| 279
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: HighlightColor: ShadowColor: FillColor: TextDisabledColor: TextForeColor: TextBackColor: ButtonTextColor: OverlayTextureAlpha: ScrollBarHeight: ScrollBarWidth: WindowMin: WindowZoom: WindowClose: WindowMinAt: WindowZoomAt: WindowCloseAt: CheckBoxUp: CheckBoxDn: CheckBoxOn: CheckBoxOff: RadioButtonUp: RadioButtonDn: RadioButtonOn: RadioButtonOff: TreeCollapsed: TreeExpanded: TreeVertLine: TreeHorzLine: TreeChkUnmarked: TreeChkMarked: TreeGrpUnmarked: TreeGrpMarked: ScrollBarUp: ScrollBarDn: ScrollBarRt: ScrollBarLt: }
"/aws/texture.png" 230,230,230 60,60,60 200,200,200 128,128,0 0,0,0 255,255,255 0,0,192 128 16 16 "/aws/minimize.png" "/aws/zoom.png" "/aws/close.png" (46, 6) - (46-11, 6+10) (34, 6) - (34-11, 6+10) (19, 6) - (19-11, 6+10) "/aws/chkup.png" "/aws/chkdn.png" "/aws/chkon.png" "/aws/chkoff.png" "/aws/radup.png" "/aws/raddn.png" "/aws/radon.png" "/aws/radoff.png" "/aws/treecol.png" "/aws/treeexp.png" "/aws/treevl.png" "/aws/treehl.png" "/aws/treechke.png" "/aws/treechkf.png" "/aws/treegrpe.png" "/aws/treegrpf.png" "/aws/sbup.png" "/aws/sbdn.png" "/aws/sbrt.png" "/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 configuration (vfs.cfg): VFS.Mount.aws
= $@data$/awsdef.zip
If you look within the awsdef.zip file, you will find the PNG images the previous 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 following 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 definition in our demogame.def file, giving us the following complete Windows definition file: Listing 6-7: demogame.def skin "Normal Windows" { Texture: HighlightColor: ShadowColor: FillColor: TextDisabledColor: TextForeColor: TextBackColor: ButtonTextColor: OverlayTextureAlpha: ScrollBarHeight: ScrollBarWidth: WindowMin: WindowZoom: WindowClose: WindowMinAt: WindowZoomAt: WindowCloseAt: CheckBoxUp: CheckBoxDn: CheckBoxOn: CheckBoxOff: RadioButtonUp:
"/aws/texture.png" 230,230,230 60,60,60 200,200,200 128,128,0 0,0,0 255,255,255 0,0,192 128 16 16 "/aws/minimize.png" "/aws/zoom.png" "/aws/close.png" (46, 6) - (46-11, 6+10) (34, 6) - (34-11, 6+10) (19, 6) - (19-11, 6+10) "/aws/chkup.png" "/aws/chkdn.png" "/aws/chkon.png" "/aws/chkoff.png" "/aws/radup.png"
Demo Game Part 1 — Building the GUI Converting the Dialogs RadioButtonDn: RadioButtonOn: RadioButtonOff: TreeCollapsed: TreeExpanded: TreeVertLine: TreeHorzLine: TreeChkUnmarked: TreeChkMarked: TreeGrpUnmarked: TreeGrpMarked: ScrollBarUp: ScrollBarDn: ScrollBarRt: ScrollBarLt:
"/aws/raddn.png" "/aws/radon.png" "/aws/radoff.png" "/aws/treecol.png" "/aws/treeexp.png" "/aws/treevl.png" "/aws/treehl.png" "/aws/treechke.png" "/aws/treechkf.png" "/aws/treegrpe.png" "/aws/treegrpf.png" "/aws/sbup.png" "/aws/sbdn.png" "/aws/sbrt.png" "/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" {
| 283
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 Converting the Dialogs
TE
AM FL Y
} 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) } 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" { Frame: (220,330) - (331,371) Caption: "Signup ->" connect { signalClicked -> signupSink::DoSignup } } }
Team-Fly®
| 285
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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include
"cssysdef.h" "cssys/sysfunc.h" "iutil/vfs.h" "csutil/cscolor.h" "cstool/csview.h" "cstool/initapp.h" "cstool/cspixmap.h" "iaws/aws.h" "iaws/awscnvs.h" "demogametut1.h" "iutil/eventq.h" "iutil/event.h" "iutil/objreg.h" "iutil/csinput.h" "iutil/virtclk.h"
// ADDED // ADDED
Demo Game Part 1 — Building the GUI Testing the GUI #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include
"iengine/sector.h" "iengine/engine.h" "iengine/camera.h" "iengine/light.h" "iengine/statlght.h" "iengine/texture.h" "iengine/mesh.h" "iengine/movable.h" "iengine/material.h" "imesh/thing/polygon.h" "imesh/thing/thing.h" "imesh/object.h" "ivideo/graph3d.h" "ivideo/graph2d.h" "ivideo/txtmgr.h" "ivideo/texture.h" "ivideo/material.h" "ivideo/fontserv.h" "ivideo/natwin.h" // ADDED "igraphic/image.h" "igraphic/imageio.h" "imap/parser.h" "ivaria/reporter.h" "ivaria/stdrep.h" "csutil/cmdhelp.h"
CS_IMPLEMENT_APPLICATION // Application Specific... csRef 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...
| 287
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 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 (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 #include "csutil/ref.h"
292 |
Chapter 6 Testing the GUI struct struct struct struct struct struct struct struct struct struct
iObjectRegistry; iEngine; iLoader; iGraphics2D; iGraphics3D; iKeyboardDriver; iVirtualClock; iEvent; iView; iTextureManager;
class Butterfly { private: iObjectRegistry* object_reg; csRef engine; csRef loader; csRef g2d; csRef g3d; csRef kbd; csRef vc; csRef view; csRef txtmgr; // ADDED csRef aws; iAwsPrefManager* awsprefs; csRef 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 Initialize 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 application 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
TE
AM FL Y
Here is how the other two dialogs look within our Crystal Space application.
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 aws; csRef 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 (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_AlwaysRedrawWindows 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: HighlightColor: ShadowColor: FillColor: TextDisabledColor: .......[file cut here]
"/aws/texture.png" 230,230,230 60,60,60 200,200,200 128,128,0
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 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include
"cssysdef.h" "cssys/sysfunc.h" "iutil/vfs.h" "csutil/cscolor.h" "cstool/csview.h" "cstool/initapp.h" "cstool/cspixmap.h" "iaws/aws.h" // "iaws/awscnvs.h" // "iaws/awsparm.h" // "demogametut1.h" "iutil/eventq.h" "iutil/event.h" "iutil/objreg.h" "iutil/csinput.h" "iutil/virtclk.h" "iengine/sector.h" "iengine/engine.h" "iengine/camera.h" "iengine/light.h" "iengine/statlght.h" "iengine/texture.h" "iengine/mesh.h" "iengine/movable.h" "iengine/material.h" "imesh/thing/polygon.h" "imesh/thing/thing.h" "imesh/object.h" "ivideo/graph3d.h" "ivideo/graph2d.h" "ivideo/txtmgr.h" "ivideo/texture.h" "ivideo/material.h" "ivideo/fontserv.h" "ivideo/natwin.h" // "igraphic/image.h" "igraphic/imageio.h" "imap/parser.h" "ivaria/reporter.h"
ADDED ADDED ADDED
ADDED
300 |
Chapter 6 Adding the Sinks #include #include #include #include
"ivaria/stdrep.h" "csutil/cmdhelp.h" "csutil/csstring.h" "csutil/scfstr.h"
// ADDED // ADDED
CS_IMPLEMENT_APPLICATION // Application Specific... csRef 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 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 (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;
AM FL Y
}
// Login dialog methods... void Butterfly::Login(void* awst, iAwsSource *) { Butterfly* appPtr = (Butterfly *)awst; printf ("Login Button Pressed\n");
TE
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... csRef errStr = csPtr (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 errStr = csPtr (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 iString iString iString iString
*name; *email; *username; *password; *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 errStr = csPtr (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 errStr = csPtr (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 errStr = csPtr (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 errStr = csPtr (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 errStr = csPtr (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 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 chatStr = csPtr (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 #include "csutil/ref.h" struct struct struct struct struct struct struct struct struct struct struct
iObjectRegistry; iEngine; iLoader; iGraphics2D; iGraphics3D; iKeyboardDriver; iVirtualClock; iEvent; iView; iTextureManager; iString;
class Butterfly {
310 |
Chapter 6 Adding the Sinks private: iObjectRegistry* object_reg; csRef engine; csRef loader; csRef g2d; csRef g3d; csRef kbd; csRef vc; csRef view; csRef txtmgr; // ADDED csRef aws; csRef awsCanvas; // [END] // ADDED 2 iString *username; csRef csRef csRef csRef
loginDialog; signupDialog; errorDialog; 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 creating, 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 csRef csRef csRef