Flash 10 Multiplayer Game Essentials
Create exciting real-time multiplayer games using Flash
Prashanth Hirematada
BIRMINGHAM - MUMBAI
Flash 10 Multiplayer Game Essentials Copyright © 2010 Packt Publishing
All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, without the prior written permission of the publisher, except in the case of brief quotations embedded in critical articles or reviews. Every effort has been made in the preparation of this book to ensure the accuracy of the information presented. However, the information contained in this book is sold without warranty, either express or implied. Neither the author, Packt Publishing, nor its dealers or distributors will be held liable for any damages caused or alleged to be caused directly or indirectly by this book. Packt Publishing has endeavored to provide trademark information about all the companies and products mentioned in this book by the appropriate use of capitals. However, Packt Publishing cannot guarantee the accuracy of this information.
First published: July 2010
Production Reference: 1020710
Published by Packt Publishing Ltd. 32 Lincoln Road Olton Birmingham, B27 6PA, UK. ISBN 978-1-847196-60-6 www.packtpub.com
Cover Image by Vinayak Chittar (
[email protected])
Credits Author Prashanth Hirematada Reviewers Ali Raza
Editorial Team Leader Akshara Aware Project Team Leader Lata Basantani
Bruce Wade Project Coordinator Acquisition Editor
Jovita Pinto
David Barnes Proofreader Development Editor
Aaron Nash
Dhwani Devater Production Coordinator Technical Editor
Alwin Roy
Pallavi Kachare Cover Work Copy Editor Lakshmi Menon Indexer Rekha Nair
Alwin Roy
About the author Prashanth Hirematada, ia the founder of Gamantra, a game technology company
focused on Network engines and server platforms. Prior to founding Gamantra in 2006, he was a Chief Architect at Shanda Interactive Entertainment Ltd., where he was responsible for creating a common game development platform for all MMOG initiatives at Shanda. He joined Shanda in 2004 through Shanda's acquisition of Zona, Inc., a MMOG game technology company, headquartered in Santa Clara, USA. At Zona, as a Technical Chief Architect, he was responsible for server-side architecture and implementation of MMOG framework. Prior to joining Zona in 2001, Prashanth worked in various Silicon Valley based technology start-up companies developing software at various levels for well over seven years. His master's thesis was a distributed implementation of the Message Passing Library (MPI) on a heterogeneous network of workstations including Solaris, HP-UX, OpenStep, and Windows-NT. He received his MS in Computer Science from the California State University, Sacramento, California, in 1994 and his BS in Computer Science from Bangalore University, Bangalore, India in 1992. You can contact him at
[email protected]. This book would not have been possible without the constant blessings, encouragement, and belief received from my parents throughout my entire life. I would like to acknowledge the constant support of my loving wife, Jessie who puts up with all my long hours in front of the computer. I also thank Tony Chen and Wilson Wu for their dedicated support with the implementation of GNet and Pulse SDK.
About the reviewers Ali Raza is a fresh and invigorated aspirant in the field of design, development,
and authoring. He became part of the IT field from quite an early age and worked up from designing business cards, flyers, books, websites, digital maps, software interfaces, and almost all design-related things to audio and video editing, animation, and even minor 3D modeling in Autodesk Maya. Later, playing with code became his passion, which compelled him to work in various programming languages including C++, Java SE, Actionscript 3, and PHP. Ali is pursuing a Master of Science degree in Computer. He is also an Adobe Certified Instructor, Adobe Certified Expert, and Sun Certified Java Programmer. He is currently a senior developer at 5amily Ltd, a London-based, forthcoming genealogy-related social networking rich Internet application. Previously, he has worked with different national and international advertising, telecommunication, and IT firms. Ali is authoring Adobe Flex 3 with AIR exam guide from the platform of ExamAids. He is also a regular author in Flash and Flex developer magazine and writes project-based articles, predominantly on Data Visualization, and also loves writing book reviews.
In his spare time, you will find him either engulfed in design and development related books or envisaging the accomplishment of a series of certifications in ACE Flash and ACE Dreamweaver after his masters. You can contact him at
[email protected]. I would like to express my gratitude to the packet publishing for bringing unique titles. I would also like to thank to Dhwani Devater and Jovita Pinto.
Bruce Wade got started in software development using Flash early on in his web
development career. After going to school for game programming he quickly found himself leveraging Flash for online game development. He works around the clock implementing new Flash games and tools for his website Warply Designed, which is dedicated to independent game developers. I would like to thank all the authors for the sleepless nights I had reading their books to help improve on my skill sets.
Table of Contents Preface Chapter 1: Getting Started
1 7
Development environment Pulse SDK Installing Flash Builder 4 Downloading the Pulse SDK Installing Pulse SDK Post-installation checks Running the samples
8 8 9 9 10 12 13
A high-level architecture of multiplayer game server Simple deployment architecture Enterprise deployment architecture
14 14 16
Network programming paradigm Client-to-client interaction The server modules Game server modules
18 19 19 20
Starting the server Starting the client
The session server process The balancer process Server game programming Zero server-side programming
Persistence Session and session manager Avatar manager Friends Chat Object synchronization Object serialization Security Connection management and message dispatcher
13 13
17 17 17 17
20 20 21 21 21 21 22 22 22
Table of Contents Message
The game client The overall structure of a multiplayer game The main game loop Processing messages from the server The programming API Summary
Chapter 2: Game Interface Design
Overview of Pulse library components The Pulse API design Creating the Hello World sample Setting up the project The Hello World specification The schema file Code generator Project directory structure Introduction to PulseUI Screen management in PulseUI The PulseGame class Exploring the Hello World sample HelloGame.as The login screen The screen class The skinner class The outline class Player registration Exploring the game server deployment Registration and login Registration The login Dealing with multiple logins Guest logins
Summary
23
24 25 25 27 27 28
29 29 30 31 31 35 36 37 38 39 39 40 43 43 45 46 47 49 50 50 50
51 52 53 54
54
Chapter 3: Avatar and Chat
55
Introduction to Pulse modeler Example schema Design of a game avatar Modeling the avatar Avatar display in Hello World Customizing Player Display
55 56 59 60 60 61
[ ii ]
Table of Contents
Avatar-related APIs PulseGame client APIs Pulse game client callbacks Friends management Friends in Hello World The friends API Customizing friends display The chat feature The chat API Chatting in Hello World Customizing chat display Implementing high scores High scores in Hello World Skinning the user interface Skinning for Hello World Summary
Chapter 4: Lobby and Room Management Introduction to lobby and room management Modeling game room
66 66 67 67 68 68 69 73 73 74 74 76 77 78 78 79
81 81 84
Game room management Seating order Room states Player states Kicking out a player Room types Audience Room properties
The lobby screen implementation Lobby screen in Hello World Customizing lobby screen Customizing game room display Filtering rooms to display Lobby and room-related API New game screen implementation New game screen in Hello World Customizing the new game screen New game room API Designing the game screen Implementing the game screen Customizing the game screen Summary [ iii ]
85 85 85 86 87 88 89 89
89 89 90 90 91 92 93 93 94 95 96 97 102 102
Table of Contents
Chapter 5: Game Logic
103
Gameplay implementation Modeling game states Game states types Game states in Hello World Code walk-through
103 104 105 107 107
GameStateSprite class General flow of events Game state schema Adding a new game state Updating game state Removing a game state
108 108 109 110 111 113
Game state API Miscellaneous classes The Button Effect class The Slider class The ShakeEffect class Summary
Chapter 6: Multiplayer Game Example: Tic-tac-toe Running the game from sample directory The Pulse UI framework Setting up the project Getting started: Modeling the game Project directory structure Code walk-through TictactoeGame Overriding the constructor Overriding the initNetClient method Implementing a turn-based game Sending and receiving player actions
114 115 116 117 121 123
125 126 126 127 128 129 130 130
130 131 132 132
TictactoeSkinner TictactoeNewGameScreen TictactoeGameScreen
133 133 134
Other screens and features Lobby screen Chat
145 145 146
Initializing the game screen Displaying player turn Letting the player make the move Who won? Finding the winner
135 138 139 140 143
[ iv ]
Table of Contents
TopTen Registration screen Summary
Chapter 7: Multiplayer Game Example: Jigsaw Setting up the project Files in the project The game graphics DisplayManager Managing pieces—Group The PieceSprite class Creating a piece Dragging of pieces Checking for matches Multiplayer and networking Code generation Screen classes The JigsawGame class
Overriding the constructor Overriding the initNetClient method Server communication
The JigsawSkinner class The NewJigsawGameScreen class The JigsawGameScreen class Summary
Chapter 8: Card-based Racing Game Tutorial Implementation Graphics The map and frog movement The step class The frog class
147 148 148
149 150 150 151 151 152 152 153 161 163 166 169 169 170
170 170 171
172 173 174 176
177 178 178 179
181 181
Card management Screen management Class Skin Class JJF Class NewGame Multiplayer design Card distribution Frog position Assigning player color Schema
183 190 190 191 192 192 193 194 194 194 [v]
Table of Contents
Gameplay implementation Assigning colors Determining the initial frog positions Getting the initial three cards Playing the game Summary
Chapter 9: Real-time Racing Game Tutorial Game design The game client The main game loop The spaceship class Controlling movement Skinning the ship
196 197 200 201 202 204
205 206 206 207 207
207 210
The racetrack module Mapping coordinates Loading quadrants The mini-map class The Radar class Implementing items
212 212 212 215 216 218
Detecting collisions
221
Implementing the shield Finishing the race Multiplayer implementation Designing the schema
222 223 224 225
Assigning ship color Putting items on the map Ship prediction and interpolation Winning the race Summary
226 229 230 233 235
The ShipMask class The ShipPos class The item class The ShipWin class
226 226 226 226
Appendix A: Introduction to FlashBuilder and AS3 Installing Flash Builder 4 AS2 versus AS3 Exploring Flash Builder 4 Hello World! Defining a class Classes—defining game objects Creating game objects
[ vi ]
237 237 237 238 240 242 244 244
Table of Contents
Variables and properties Magic numbers and constants Methods Property and method access Taming the inheritance monster Interface class Static properties and methods
Appendix B: Graphics Programming in AS3 Flash object hierarchy Object EventDispatcher DisplayObject and DisplayObjectContainer InteractiveObject Sprite, in detail Which way is up? Let the fun begin Events Timers Trace Embedding pictures Mouse events What do we need to handle mouse events for? How to register for a mouse event What are the events we can handle in Flash? Handling mouse events in many objects Where is the Mouse? Drag-and-drop Keyboard events Arrow key handling: The basics Arrow key handling: The professional way Labels, text fields, and sprite buttons Filters: Adding effects to sprites Transparency: Playing with the alpha channel Cool fading screens Cutting up assets
Index
[ vii ]
246 249 250 251 252 264 265
267 267 268 268 268 268 269 269 271 273 274 275 276 279 279 280 281 282 284 284 287 288 290 294 296 299 300 306
309
Preface There are plenty of Flash games on the web, but what about multiplayer Flash games and moreover real-time multiplayer games? Not that many! The Pulse SDK presented in this book abstracts the standard set of features required for any multiplayer game, which is leveraged throughout the book. As you will see, writing multiplayer becomes quite straightforward with all the standard set of features such as room and lobby management, friends, top ten, registration, among others, all taken care of, allowing the developer to focus only on the game.
What this book covers
The reason that one does not see that many real-time multiplayer games is due to the fact that development and deployment requires a great deal of knowledge about networking and server programming. The developer must also be well aware of the performance bottlenecks along the way when the game is played by several thousands of players at the same time. There is also the issue of dealing with the database in order to save the scores, achievements, friends, and other attributes of a player. All of these present a high barrier for the developer attempting to write a multiplayer game. This book presents all the features required for any multiplayer game, their design and implementation. All of the implementation presented in this book is based on the Pulse SDK framework.
Preface
Chapter 1, Getting Started, sets up all the required software, namely, Flash Builder 4 and Pulse SDK. We will also fire up the game server and test drive a few multiplayer game samples. The server and the client may be run all on one machine and on separate physical machines. We will go through the high-level design and architecture of a game server and its modules that must be implemented in order to support any typical multiplayer game. We will also see an enterprise architecture that is capable of serving thousands of concurrent players. We will also touch upon the communication paradigms between game clients, namely peer-to-peer and the client-server architectures. The book follows the client server exclusively. Throughout the book, we will work at a higher level of abstraction in developing our games. To make things even simpler, we will use a paradigm and a set of APIs provided by Pulse SDK that does not require us to write a single line of code on the server and yet we will implement, turn-based and non turn-based games, puzzle games as well as fast action racing games. Chapter 2, Game Interface Design, mainly deals with the game UI required by a typical multiplayer game. The UI is leveraged by the Pulse UI framework bundled along with Pulse SDK. From Login screen, lobby screen for creating a new game room, game screen itself. The UI also involves friends display, players display within the game room, chat, and more. The chapter is presented with the Hello World sample in the backdrop. The complete source code for Hello World sample comes with the Pulse package, so you can modify the sample and experiment as you progress through the chapter. Chapter 3, Avatar and Chat, starts to model our game objects starting with avatar for the game. We will explore what it means to model the game entities. We will learn how to design the avatar in this chapter. We will explore the different kinds of objects (entities) that may be modelled within the Pulse Modeller, also the property types that may be defined within each class (entity). We will also explore the game UI in greater detail as related to the friends display and player display. We will also see how we can customize them from the default behaviour as offered by the Pulse UI framework. You will learn the Pulse APIs that deal with customizing the avatar for the player, making friends, and chat. Chapter 4, Lobby and Room Management, discusses the central feature to any multiplayer games, lobby and room management. You will continue to use the Pulse Modeller to design the rooms that is required for the game. We will learn about the different room properties and their status during its existence. We will also see how players may join, leave, or kick a player out of a room. Again we will leverage the Pulse API to manage all these features within our game.
[2]
Preface
Chapter 5, Game Logic, is the heart of the game—the game itself! We will review all the different Pulse game state APIs that deal with implementing the game logic. We will learn how we can design the game states required for the game via the Pulse Modeller and use them through an innovative set of Pulse APIs. We will see how we can use the unique set of APIs that makes any game logic implementation possible without writing a single line of server code. Chapter 6, Multiplayer Game Example: Tic-Tac-Toe, has the complete source for Tic-Tac-Toe included in the Pulse package; in this chapter, we will do a complete walk-through of the code. Tic-tac-toe is of course a real-time multiplayer game. Multiple clients may connect to the server, create a room or join a room, and play the game. We will go through the different screens such as login screen, lobby, top-ten, and registration. The game is implemented in completeness, and includes friend-making and chat. Chapter 7, Multiplayer Game Example: Jigsaw walks through a non-turn-based game, a multiplayer jigsaw (unlike tic-tac-toe, which is a turn-based game), where multiple players collaborate to solve the same jigsaw puzzle. The player matching the most pieces wins. The chapter will walk you through in detail to first make the client part work, leaving the multiplayer implementation to the latter part of the chapter. The walk-through includes cutting any given picture into jigsaw pieces and then managing them during game play. We will learn how to detect the correct matching pieces when they are next to each other. We will also see how all the matched pieces are kept synchronized among all the players that are currently playing the game. Chapter 8, Card-based Racing Game Tutorial: Jump Jump Frog, is a fun card-based racing game. The chapter will teach you a commonly occurring game implementation, namely card distribution. Here we make heavy use of unique game state concept available in Pulse SDK. Similar to other game implementation tutorials in this book, we will first start with the design of the schema for the game and then we will learn the graphics part of the game client and finally explore the multiplayer implementation. Chapter 9, Real-time Racing Game Tutorial: AstroRace, teaches to implement the most exciting game genre, a racing game. We will see what we need to design in the schema for the game. The exciting game to play has its challenges set for the developers. In this game, the players will race against one another in a spaceship. Along the racetrack the players will have avoid items that may work against them or pickup items that will help them get faster to the finish line.
[3]
Preface
Appendix A: This part of the book introduces Flash Builder and coding in AS3. It is a great start for those programmers who are novice AS3 programmers. Here the basic AS3 syntax is presented in a relatively few number of pages, letting starters get proficient in flash programming in a short amount of time. For those of you getting into object oriented programming for the first time, this appendix is a great start. Appendix B: Having a good handle on the AS3 syntax, this appendix presents the basics of flash graphics programming. It starts with the basic building block, sprite. Readers will also learn to draw and move them around on stage. In depth discussions of essential techniques to any flash game development such as events, timers, event listeners, mouse and keyboard handling are also presented. Interesting things like transparency, added cool effects to sprites are also part of this exciting appendix. Plenty of examples with complete code that will jump start you into your next game development.
Who this book is for
This book is written for game developers that are either starting out with game development in Flash or professional game developers wanting to write the next hot real-time multiplayer game. If you are starting out new in Flash, you will find the appendix very useful as they teach working with Flash Builder 4, learning to program in AS3, and lots of programming examples required for game programming, including sprite basics, keyboard and mouse handling, and a lot more. Professional game developers, who are comfortable writing single player games, find it challenging when it comes to multiplayer games, a lot of distracting tasks to take care of before a line of code for the game is written. Armature multiplayer game developers would appreciate the challenges that befall when writing their first game. The book discusses each of the challenges.
Conventions
In this book, you will find a number of styles of text that distinguish between different kinds of information. Here are some examples of these styles, and an explanation of their meaning. Code words in text are shown as follows: The two environment variables that all of Pulse SDK depends on are GAMANTRA and GNET_JAVA.
[4]
Preface
A block of code is set as follows:
<property index=”0” name=”x” count=”1” type=”int”/> <property index=”1” name=”y” count=”1” type=”int”/> <property index=”2” name=”color” count=”1” type=”int”/>
When we wish to draw your attention to a particular part of a code block, the relevant lines or items are set in bold: public function MyGame() { s_instance = this; new MySkinner(); super(); }
New terms and important words are shown in bold. Words that you see on the screen, in menus or dialog boxes for example, appear in the text like this: Clicking Next will take you to a standard UCLA screen.
Warnings or important notes appear in a box like this.
Tips and tricks appear like this.
Reader feedback
Feedback from our readers is always welcome. Let us know what you think about this book—what you liked or may have disliked. Reader feedback is important for us to develop titles that you really get the most out of. To send us general feedback, simply send an e-mail to
[email protected], and mention the book title via the subject of your message. If there is a book that you need and would like to see us publish, please send us a note in the SUGGEST A TITLE form on www.packtpub.com or e-mail
[email protected]. If there is a topic that you have expertise in and you are interested in either writing or contributing to a book on, see our author guide on www.packtpub.com/authors. [5]
Preface
Customer support
Now that you are the proud owner of a Packt book, we have a number of things to help you to get the most from your purchase. Downloading the example code for this book You can download the example code files for all Packt books you have purchased from your account at http://www.PacktPub.com. If you purchased this book elsewhere, you can visit http://www.PacktPub. com/support and register to have the files e-mailed directly to you.
Errata
Although we have taken every care to ensure the accuracy of our content, mistakes do happen. If you find a mistake in one of our books—maybe a mistake in the text or the code—we would be grateful if you would report this to us. By doing so, you can save other readers from frustration and help us improve subsequent versions of this book. If you find any errata, please report them by visiting http://www. packtpub.com/support, selecting your book, clicking on the let us know link, and entering the details of your errata. Once your errata are verified, your submission will be accepted and the errata will be uploaded on our website, or added to any list of existing errata, under the Errata section of that title. Any existing errata can be viewed by selecting your title from http://www.packtpub.com/support.
Piracy
Piracy of copyright material on the Internet is an ongoing problem across all media. At Packt, we take the protection of our copyright and licenses very seriously. If you come across any illegal copies of our works, in any form, on the Internet, please provide us with the location address or website name immediately so that we can pursue a remedy. Please contact us at
[email protected] with a link to the suspected pirated material. We appreciate your help in protecting our authors and our ability to bring you valuable content.
Questions
You can contact us at
[email protected] if you are having a problem with any aspect of the book, and we will do our best to address it. [6]
Getting Started This book is all about creating synchronous multiplayer flash games! Multiplayer games are different from single-player games in that every multiplayer game must have a certain feature set, such as lobby and room management, chat, friend making, etc., not to mention the game itself. This book explores the features and challenges that lie in developing a typical multiplayer game and its deployment. It also presents an object-oriented framework that makes it easy to manage the various user interface screens as well as exchange game state information among players while playing a game. We begin our journey by downloading and installing the required software: Flash Builder 4 and Pulse SDK. The reader is expected to have basic AS3 programming skills and a working knowledge of either Flex Builder 3 or Flash Builder 4. Those readers who have recently entered into the world of Flash may find the two appendices useful. Appendix A provides a quick overview of developing AS3-based code in Flash Builder 4 IDE as well as giving a brief overview of AS3 language itself, along with the object-oriented concepts. Appendix B covers Flash graphics programming in AS3 required for gaming in general. The graphics programming discussions are purely AS3 based, as it does not involve the use of creative series (CS) tools. This is excellent for traditional programmers who like to do everything in code. In this chapter, we will cover the following topics: •
Downloading and installing Flash Builder 4
•
Downloading and installing Pulse SDK
•
Running the multiplayer game sample HelloWorld
•
High-level architecture of a multiplayer game deployment
•
Network programming and simple client-server implementation
Getting Started
Development environment
To develop a flash-based multiplayer game as discussed in this book, the following are necessary: •
Flash AS3 development Environment, such as Eclipse, Flash Builder 4, or Flash Develop
•
Pulse SDK
In this book, we will use Adobe Flash Developer 4 to manage our game project and to write all the code. However, you may very well use any other IDEs that you are familiar with. In order to aid us in learning to develop multiplayer games in a relatively short amount of time, we will make use of Pulse SDK throughout this book.
Pulse SDK
Pulse SDK is a commercial middleware product developed by Gamantra that helps speed up the development of a multiplayer game. It offers an innovative API that does not require any server-side programming and offers, among other features, room and lobby management, high scores, in-game registration, chat, and persistence. It allows game developers to work at higher levels, focusing on game implementation rather than working at lower-level networking and server implementation. Pulse includes a generic server on which you can write a wide variety of multiplayer games. It is the aim of Pulse to simplify the multiplayer game development, and for this reason, we don't deal with the server-side programming. However, for the development of more sophisticated games such as MMORPG and virtual world implementation, which require server-side logic, the versions of Pulse that offer server-side SDK would be more suited. These are not discussed in this book. Pulse SDK does not offer any additional low-level graphics libraries on top of Flash. However, it does offer a high-level networking API and a generic server. It includes a simple game screen management component, which is ideal for rapid prototyping of Flash-based multiplayer game. The Pulse SDK is a simple yet powerful developer API that integrates with a powerful Pulse deployment platform.
[8]
Chapter 1
Installing Flash Builder 4
A trial version of Flash Builder can be downloaded from the Adobe site for free at http://www.adobe.com/products/flashbuilder/. Unlike traditional tools from Adobe, such as the CS series, Flash Builder is aimed at traditional programmers who love to write code instead of using designer-oriented tools. The IDE includes a powerful editor with auto-completion features for faster coding, a debugger, and a profiler. The integrated compiler allows developers to produce a compact swf file for immediate deployment.
Downloading the Pulse SDK
To download the SDK, browse to http://forum.gamantra.com and register. Once you register, you will see the download discussion topic on the front page of the forum. Click on it and go to the latest post, which contains the newest version of the SDK. The SDK is a free trial version that you may use to create a complete multiplayer game. The main difference between a trial version and a paid version is that in a trial version the persistence is disabled and the number of concurrent players is limited to four. The downloaded file is a self-extracting executable. Once you start the setup file, it will first extract a .jar file within it, which performs the actual installation. For this reason, you need to have Java Runtime installed on your computer prior to firing up the installation. If you have trouble starting up the installation executable, make sure that you have the JRE installed on your computer and that .jar is associated with java.exe.
[9]
Getting Started
Installing Pulse SDK Installation of Pulse SDK is as follows:
1. On starting the installation program, you will see the following welcome screen:
2. Clicking Next will take you to a standard UCLA screen. 3. Select the choice I accept the terms of this license agreement and click on Next:
4. In the installation path, which is the next screen, specify where all the contents of the package should be installed on newer Windows systems, such as Vista or Windows 7. Unless you are the administrator, it is best to install on a simpler path, as shown in the next screenshot, and avoid installing it among Program Files or even on the C drive. After setting the installation path, click on Next.
[ 10 ]
Chapter 1
5. On the confirmation window for the installation path, click on OK and this will start installing the files and set the environment variables.
6. The next screen will confirm the shortcuts to add. You may leave them as they are or change them to suite your needs. Clicking on Next will take you to the final screen.
[ 11 ]
Getting Started
Post-installation checks
To make sure that the installation was successful, you need to verify the directory structure where you installed the package. The root-level folders must look similar to the following:
The bin folder contains several batch files, including one to start the server and another for generating class code files for your game, which we will see in greater detail in this chapter. The doc folder contains the development guide and API guides for Pulse. The frameworks folder contains the PulseUI package that helps you jumpstart your
multiplayer game development by providing you all the standard game screens and their flow management. If you want to quickly build a game prototype, PulseUI will greatly reduce the development time and help you focus solely on your game implementation. The complete source for PulseUI is also available, so you can tweak the parts you want to suite your needs.
The lib folder contains all the swc files that you will need to include in your game project, which we will soon discuss in this chapter. Finally, the sample folder contains the following three samples: •
HelloWorld
•
Tic-tac-toe
•
Jigsaw
For each sample, the compiled swf and source files are available. We will do a walkthrough for each of these samples later in the book. The two environment variables that all of Pulse SDK depends on are GAMANTRA and GNET_JAVA. Upon installation, GAMANTRA should point to the root installation folder and GNET_JAVA must point to the JRE folder, such as C:\Program Files\Java\ jre6. If you don't see them set properly for any reason, you may manually set them appropriately.
[ 12 ]
Chapter 1
From here on we will use $ to mean the pulse root installation folder; for example, the tic-tac-toe sample is found in $\samples\tictactoe.
Running the samples
Once you have verified the installation, we are now ready to fire up the server and take the samples for a test drive.
Starting the server
Starting the server is quite easy and there are two ways to fire it up. One way is from the Windows start button; navigate to All Programs | Gamantra | Pulse and select Start Pulse Server. Another way to fire up the server is by launching the batch file $\ bin\start_pulsar.bat. This will launch the server in a console window. If you don't see the window, it may be because it is minimized. Make sure you check the taskbar for the new console icon. The console should look similar to the following screenshot:
Starting the client
The free trial version allows you to start one, and only one, sample at a time. If you want to try out another sample, you need to restart the server and then fire up another sample. For example, if you want to try out HelloWorld, you may start multiple client instances of HelloWorld, and next if you want to fire up tic-tac-toe, you need to restart the server and then fire up the tic-tac-toe instances. [ 13 ]
Getting Started
To start the HelloWorld client instances, start the server and then open the HelloGame.swf found in the folder $\samples\HelloWorld\bin\hw. You may open it with either an Internet browser or Flash Player. You may also open several instances of them. To fire up the jigsaw sample, you need to open the file JigsawGame.swf found in $\ samples\jigsaw\bin\jig, and the tic-tac-toe sample may be started by opening the file TictactoeGame.swf in $\samples\tictactoe\bin\tictactoe.
A high-level architecture of multiplayer game server For the curious minds who like to know what happens behind the scenes of a multiplayer game, the following illustration shows how individual Flash-based game clients communicate with one another via the game server.
Simple deployment architecture
The architecture is a simplified one that does not show a typical industry strength deployment with load balancers and separate login server, and others. However, the architecture shown here is still capable of serving thousands of concurrent players: Database
Game Server (Example Java based)
Client Flash Game
Client Flash Game
Client Flash Game
Client Flash Game
Depending on the type of game, the performance of a single-server architecture can support anywhere from a few hundred to a few thousand concurrent players. Notice the word concurrent, which means playing at the same time as opposed to the number of registered accounts or some other metrics. [ 14 ]
Chapter 1
What largely affects and limits the performance is the number of messages that the server has to relay to all the players within the same game room. What constitutes a message could be anything that others need to know when a player does some action. For example, a player picks up an item off the ground, a player chooses a card, the player hits the gas driving a car, etc. A server that's hosting a card game would serve more concurrent players than, for example, a racing game. As you will see in the very last example presented in this book, in a racing game the rest of the players must know at all times where you are, by the second or in a lesser interval. This means that messages must be sent up to the server at rapid intervals and the game server must reply to all other players within the room as quickly as possible. What was just described is only the gameplay part of messaging. But there are others; for example, when someone created a room, joined a room, chats, etc. These messages also become significant when there are more and more players actively connected to the game server and not necessarily playing the game. Besides game message communication, there is a lot that the game server must do as a commercial deployment, such as storing the account information of players in a persistent database, managing rooms in a lobby, maybe managing multiple lobbies, monitoring chatting activity, keeping track of high scores, sometimes hosting AI bots to play with players, and validating players' moves and making sure no one is cheating with a hacked client, and so on. If you are planning to write a multiplayer game, you have to deal with all the previous items and much more as opposed to a single player game, where much of the work is actually dealing with writing the game and nothing else. This point is exactly what discourages many game developers from ever writing a multiplayer game. Fortunately, the situation is not that bad, as there are several commercial APIs or SDKs that take care of all these issues and let you focus on the game. Some wellknown SDKs are Pulse, which we will use in this book, ElectroServer, and SmartFox. There are also some open source ones such as Red5. Adobe also has a solution in the market—the Flash Media Server— which you could use to implement games on, but it is mostly designed and suited for hosting video or image-sharing services. If you were to write a game server on your own, you would need to be very familiar with the network protocols such as TCP or UDP. You also need to know how best to write all the bookkeeping and services on the server side as well as deal with the database.
[ 15 ]
Getting Started
Enterprise deployment architecture
The simple architecture works well if you are writing one game and your traffic is around a few thousand players. But what if you are planning to write and deploy tens or even hundreds of games? Then how would the server deployment architecture look like that can sustain it? Referring to the following figure you will notice a few things. There is more than one game server; here each game server is responsible for handling all the events for rooms belonging to a set of lobbies. You will also notice there are a few other processes such as the session server and the balancer. These processes assist game servers in keeping the load balanced among the game servers, or in Session Server's case, take some of the load off the game server. You will also notice that there is an additional DB called Auth DB so that the authentication traffic can be separated out from the game-related persistence traffic hitting the database. The database itself may be scaled using clustering techniques available in MySQL and other commercial products.
[ 16 ]
Chapter 1
The session server process
These processes serve as the connection point to the player clients. Once the connection is made to the session server, the player will remain connected to the session server until the player logs off. All data packets sent by game clients are received by the session server and are then routed to the appropriate game server. In addition, the session server does all the final data broadcast to the clients. The data broadcast is a fairly heavy duty for a large number of players and increases exponentially with the increase in the number of players. The rationale for splitting this functionality away from the game server is to free up the CPU cycles on the game server for the game logic. The session server is also the point where the players get authenticated. There can be as many session servers as the deployment requires. If a session server goes down, all the players connected to the session server will need to log in again.
The balancer process
The balancer is the first point of contact for all player clients trying to log in, the balancer simply responds to the clients with the IP address of the least loaded session server. There can be as many balancers as required. If a balancer process goes down, a backup balancer could take over without affecting any players.
Server game programming
The multiplayer game programming usually involves writing code at two places, one of them being obvious, the client, where all the action happens for a player and where the player interacts with the game client. The other is the server, where all the validation and synchronization of game states sent up by the clients takes place. At the heart of game development and deployment, the game logic is what differs from game-to-game; the rest of the functionalities discussed in the next few chapters, such as lobby and room management, registration, friends, and so on, remain constant.
Zero server-side programming
Although game server-side programming makes it easy to write complicated game logic and offers greater security, for simpler and casual games, with Pulse SDK, complete game implementation may be achieved without writing a single line of server-side code specific to the game. [ 17 ]
Getting Started
The server in this case is a generic server implementation that provides the basic interaction among player clients. For simple games such as chess, tic-tac-toe, multiplayer jigsaw, and even racing games, the generic server works well. The downside of it is security, because there is no special logic specific to the game to detect cheats by a modified or a hacked client, making it harder but not impossible.
Network programming paradigm
The most basic implementation to communicate between two computers is sending messages between the computers. The networking standard was set in place in the late 70s by the International Organization for Standardization (ISO). Things have not changed significantly since then in terms of the low-level communication protocol. However, the speeds of the hardware that enable such communication have risen significantly, and it has been adopted across the globe. There are several ways a programmer can tap into these lower-level software layers, the most basic one being sockets. They are the portal through which a computer sends and receives data from another. A distributed memory is an alternative architecture, but is typically suited for computing nodes that are physically close to each other, such as within one computer system. The distributed memory architecture best suited for solving one single problem by massively parallelizing the solution is not suited for long-distance communication to implement a multiplayer gaming. Remote Procedure Call (RPC) is another programming model that may be adopted to implement multiple client interaction; in fact, Adobe's Media server solutions provides this model. Underneath it, however, the actual data communication happens via the socket implementation. RPC provides an easy-to-use programming model for programmers, but lacks the speed of using direct sockets. The socket communication itself has two distinct protocols: one is called the TCP and the other is the UDP. The main difference between the two is the reliability. The TCP protocol offers the best reliability against the hardware and software failures in the way that it connects the two end points of communicating computers. UDP on the other hand does not; the application layer must add additional programming logic to overcome the packet loss or packets arriving out of order at the receiving end. For the sake of reliability, TCP protocol sacrifices performance, meaning two computers communicating in TCP are generally slower compared to UDP. UDP protocol is great if the underlying network hardware is highly reliable. In cases where speed is highly desirable and quality may be compromised, UDP makes for a better choice. Numerous well-known games are in fact implemented on top of this protocol, with the reliability built in the game client layers. Another important fact to consider is that UDP may work well in some countries but not in all. [ 18 ]
Chapter 1
Client-to-client interaction
Given that multiple clients must communicate among each other to play a game together, traditionally there are two models for achieving network communication between them: •
Client server
•
Peer-to-peer
In a client-server model, all data communication among all clients goes through a server. The server is publicly accessible to all the players' computers and has a significantly high bandwidth allocation. The advantage of this model is the accessibility of the game server to anyone who is connected to the internet. Also, the programming model for the game developer is straight forward. The server is also a place where the user's actions may be monitored to ensure fairness to all the players within the community. The biggest drawback of this model is the amount of time taken for data to be sent from one player's computer to another. All data packets must travel to the server from the client and then back to the receiving client. In a peer-to-peer model, two computers create a direct socket link among the players' computers. In this model, the time spent in sending data from one player to another is cut in half compared to the client-server model. But this model is less secure compared to the client-server architecture. The model has other limitations as well: the number of players each player may communicate with is severely restricted, as each player's computer must open socket connections to each of the other computers that it wants to interact with. Most often, games implemented in the P2P model are limited to 32 or at most 64 players within a game room. Accessibility is also an issue with P2P because of the firewalls and multiple computers connected to the router inside a home, office, or Internet game rooms, though the games that use this model are as famous as Warcraft, CounterStrike, and the like.
The server modules
For any multiplayer programming, you must either know how to deal with sending and receiving data from the server, or use a third-party SDK/API that does all the low-level work for you and presents the developer with a high-level API.
[ 19 ]
Getting Started
Game server modules
In this section, we will briefly explore the different modules that a typical game server must implement in order to provide all the necessary features for the multiplayer game client. The description provided is a simplistic view of an actual implementation. The implementation of a Pulse server, for example, is much more sophisticated than presented here. However, it gives the reader a general picture of a typical game server. Database
Persistence
Session Manager
Avatar Manager
Lobby & Room Manager
Friends
Chat
Object Sync
Security
Object Serialization
Message Dispatcher
Client Connection Manager
Persistence
The persistence module takes care of all the reads and writes to a relational database, such as MySQL. All other modules requiring reading or writing to the database will request this module to do the actual work.
Session and session manager
A session is maintained for each player that is connected to the server. Various bookkeeping information keeps track of each session, including things such as hours played, login time, client IP and port, etc. The session is cleaned after the player has logged out. Usually, there is one session for each client connection. [ 20 ]
Chapter 1
The server may hold connections to several players at the same time, so we need something called a session object to keep track of the data transfer from different clients so that we don't mix up the data from other players. A session is created for every client that connects to the server. The session is where all the messages are processed. Each session holds its own temporary buffer to read the data coming in from the socket. Remember, data is received from a socket in small chunks. So until the whole message is received, the partial data received is stored in a holding buffer. Once the complete message is received, the message is then processed. The messages could be login, chat, or any game state sent by the client.
Avatar manager
The avatar manager is responsible for keeping track of the properties specific to the player in the game. The avatar keeps track of virtual cash, hit points, life, etc. Usually, for each session, there exists only one active avatar.
Lobby and room manager
Lobby contains many rooms and rooms contain many players. This module keeps track of where each player is and lets players know when a new room is created, updated or purged.
Friends
Typically, friendship relations between avatars are persisted to DB. The relationships are established upon player requests, also keeping track of any pending friendship requests.
Chat
A chat module typically routes the messages from one player to many others or privately to one other player.
Object synchronization
This module keeps track of all the current states of a game within a room. Any object modified by the client must be updated to all other clients within the same game instance within a room.
[ 21 ]
Getting Started
Object serialization
Any objects needing synchronization across players are transmitted as messages over the network. This module is responsible for converting a given object to a compact binary format, sometimes encrypting prior to sending over the network. The module also performs the complimentary function where the received binary messages are de-serialized when a message arrives at the destination over the network. A typical message format is sent between client and server. These message exchanges are the basic building blocks of any networked client-server implementation.
Security
Flash runtime has a built-in security, so flash clients cannot simply open socket connections to any server it wants. The server the client wants to connect to must grant explicit permission to the client requesting it. This module just does that. When the client creates a socket object and wants to connect to the server, the flash runtime first requests security policy, the request is received and sends back a policy data specifying that it is okay for the client to open a socket connection to the server.
Connection management and message dispatcher
This module is the workhouse for receiving messages from clients and routing them to different modules or services. The module is also responsible for dispatching messages generated from the server to the clients. Upon server start up, this service could spawn multiple threads that loop infinitely until the server process is terminated. This object does the following in a loop: 1. Checks for any new client connections. 2. If a new client connection is detected, a session object is created for the connection and adds the new session to the active list. 3. For each session in the active list, calls read and write methods. 4. Also detects any client termination and cleans up the corresponding session object.
[ 22 ]
Chapter 1
Initialize Create new Session
Session 0
Check for new connections Read from socket
Session 1
Read Write to socket Session N Write
Message
The message contains a series of bytes starting with a header followed by the message itself. The message implementation in this example is simple. Body
Header
Header
Type (int)
Body Length (int)
The message header typically consists of the description of the message body, and the previous screenshot shows that the there are two fields: •
Message type
•
Message body length
The message type determines how the message is encoded and what it represents. In the example, we have two messages: login and chat. Each message has its set of data that is stored in the message body, thus the client and server must encode and decode in the same way.
[ 23 ]
Getting Started
A simplistic login message body consists of: •
Username length (int)
•
Username (bytes)
•
Password length (int)
•
Password (bytes)
A simplistic chat message body consists of: •
Sender name's length (int)
•
Sender's name (bytes)
•
Chat message length (int)
•
Chat message (bytes)
The game client
Similar to the server, the client also contains several modules that are responsible for performing various functions: Message Handler
Game Code
Object Serialization Messages
Game State Manager
Game Server
Friends
•
Message Handler: Any messages coming from the server or any messages that the client sends either as a request or are intended for other clients are handled by this module.
•
Object Serialization: Similar to the module found in the game server; all messages that are received are converted from objects to binary messages and vice versa by this module.
•
Game State Manager: The game state manager handles all the caching of state information when the player is playing the game.
[ 24 ]
Chapter 1
•
Friends and Chat: Along with managing the players that the client may be interacting with in a lobby or a game room, various other functions such as keeping track of friends or handling chat messages are also part of a typical game client. With the use of a networked game client SDK such as Pulse, the services are offered to the game developer as a set of APIs. This approach makes it easy for game developers to focus on the game itself, rather than having to deal with all the low-level implementation of the required services.
The overall structure of a multiplayer game
Now that we have a general idea of what comprises a server and a client, let's shift our focus on to the part where the game is actually implemented. Like all software, games must be structured in order to handle their complexity. This statement is especially true for an online game. A single player that gets shipped on a CD or gets published on a website does not usually undergo subsequent revisions once it's out. But in the case of an online game, the game code is very much alive as long as there are players playing the game. For this reason, a well-structured game implementation becomes even more important so when new features may be added and bugs be found and fixed with ease without introducing new ones.
The main game loop
Almost all game implementation has a top-level game loop that starts at the beginning of the game and breaks out when the game ends.
[ 25 ]
Getting Started
The following illustration is of a typical game loop. It's true for single player games as well as multiplayer games, except that multiplayer games must need to log in to a game server, send messages to the server, and process the messages received from the server. Initialize
Login
EnterAvatar
Process User Input Render Screen Publish Updates to Server Receive & process Network Msgs
We will learn a great deal about each of them in the coming chapters, but at this point, it should give you a fairly good idea of how things look from the top. Initialize is where the program loads the resources it needs, for example, loading art assets from the disk. Upon a successful login to the server, it registers the avatar the player has entered. Typically, others in the game lobby would see a name representing the avatar either when the player chats or enters the game room waiting to be played. Also, the avatar's buddies or friends are notified whenever the avatar enters the game. The main loop constantly reads the user input, be it a mouse, keyboard, or any other form of input device. The various update methods are also called on the game. For example, the input processor may detect that a left arrow is held down, resulting in the avatar's spaceship steering to the left. The graphics display is constantly redrawn or refreshed each time in the loop. Any user actions that need to be seen by others in the game or in the vicinity are sent up to the server as messages. The server then broadcasts these messages to the players clients, and when received, triggers further visual updates and so on.
[ 26 ]
Chapter 1
Processing messages from the server
Once the login is successful, in order to process the notifications received from the server, the game code must repeatedly call handleMsg method. Usually in AS3 programming, events generated in one module can be handled and processed by subscribing to the events via the event dispatch and listener model in another parts of the software. In case of Pulse SDK, as you will learn throughout this book, the number of server notifications are one too many and becomes cumbersome to program using the events model. For this reason, Pulse adopts an interface model usually found in Java and C++ programming. In order to process the notifications from the server, the developer must provide an implementation for the Pulse callback interface, set up a timer and have it call handleMsg. For most games, calling it every 100 ms would suffice. You could call it more often for racing types of games where you would want to process any incoming messages as quickly as you can. However, for turn-based games such as chess, calling it every second, or even two, would be acceptable. public function handleMsg():Boolean
The return value indicates if there are still pending messages in the queue. There may be more than one message pending that needs to be processed. For each call to the method, one message will be processed. The order of the message processed is in the same order they were received from the server. Based on the message type, the corresponding GameClientCallback interface methods will be invoked on the object instance that you provided in the constructor. Without calling this method, there will be no callback methods fired.
The programming API
For the rest of the chapter will go over some key APIs offered by the pulse package and leave the discussion of PulseUI until the next chapter. A complete reference of the API is provided in Appendix B. The game implementation to communicate with the server and receive notification from other player's action is via the following two classes: •
PulseClient
•
PulseCallback
[ 27 ]
Getting Started
The PulseClient is what you would use to send updates from the player to the server, which will eventually be sent to other players. The PulseCallback is an interface that your game must implement to receive notification from the server. The following screenshot shows the interaction in a graphical way: implements GameClientCallback interface MyPulseGame Calls
GameClient
MyGame
Pulse Framework
PulseClient is one which your game code calls to log in: inform the server of the player actions via add, remove, or update game state, request to make a friend, create a game room; etc. PulseCallback is where you get notifications back from the server, such as whether an action by the player was successful or caused an error. The interface also notifies when a new room was created, a friend request was made, a player entered the room, and so on. The APIs can be broadly classified based on features as follows: •
System, such as login etc.
•
Game room and lobby management, such as creating or joining game room.
•
Game state management, such as those used during game play.
•
Chat, for private and public conversations among players.
•
Friends, for making new friends.
Summary
In this chapter, we downloaded and installed all the required IDE (FlashBuilder 4) and the SDK (Pulse) to help us start creating multiplayer games. We also test ran the Pulse server and fired up some samples. We also discussed the high-level architecture of a typical multiplayer game deployment. An implementation of a simple client-server communication protocol was also presented, providing an insight into how the messages can be sent and received between a Flash AS3-based client and a Java-based server. In the next chapter, we will start to explore the HelloWorld sample in greater detail. [ 28 ]
Game Interface Design In this chapter, we will explore the Hello World sample included in the Pulse package. We will focus on how things get initialized and the login process. We will also cover the Pulse framework functionalities. We will explore how the different screens for the game are managed by the PulseUI framework. Finally, we will explore some of the server-side challenges regarding player login and registration during a commercial game deployment. Specifically, we will learn the following: •
Programming structure of a multiplayer game
•
Game screen management with the PulseUI framework
•
Detailed walk-through of the login process
•
Login and registration in a typical multiplayer game deployment
Overview of Pulse library components
The Pulse package includes two components Pulse.swc and PulseUI.swc. The Pulse.swc offers the API required for you to build a multiplayer game. While PulseUI offers the game screen management, both aid in the rapid development of your game. The Pulse.swc is required in order to communicate with the server and with other clients. The usage of PulseUI, on the other hand, is optional. It is recommended to use the PulseUI since it allows you to focus only on the game implementation and leaves the standard feature set of a multiplayer game to be taken care of by the PulseUI package. Once you have the implementation of your game done and working well, you can then replace the PulseUI package with something of your own that is more suited to your game.
Game Interface Design
The following is a block diagram that shows the dependencies among different components:
The Pulse API design
The interaction of the game client with the server and other clients happens primarily via two classes that expose the Pulse features: •
GameClient
•
GameClientCallback
The GameClient is primarily used to send request to the server while creating a room, joining a room, or sending a message to other players such as chat or game state updates during game play. The GameClientCallback is an AS3 interface class for which one of the classes within the GameClient must implement. All notifications from the server are processed by the Pulse layer and corresponding notifications are called on the implementation of the callback class—for example, when a create room request was successful or when a chat message was received, etc.
[ 30 ]
Chapter 2
Creating the Hello World sample
Let us now explore the Hello World sample that is included in the Pulse package. The Hello World sample and the rest of the samples rely heavily on the game screen management framework package, PulseUI, which is also included in the Pulse package along with the source code. In this chapter, we will focus on the code contained in the Hello World sample and how the sample makes use of the PulseUI. At this point, we will assume that you have the SDK downloaded, have successfully fired up the servers, and test drove the samples as described in Chapter 1. In order to explore the Hello World sample, we first need to create a project in Flash Builder—all the required source files already exists in the sample folders. The Hello World sample does the following: create a room or join an existing game room, then add, remove, or modify a game state—the changes done on one client instance are then reflected on all other clients that are in the same room. Think it is too much for a Hello World sample? It is not! These are just the basic functionalities for any multiplayer game. Moreover, we don't need to write the code for every bit of functionality because we heavily rely on Pulse SDK to do all the dirty work.
Setting up the project
Fire up the Flash Builder 4 IDE and let us start by creating an ActionScript project called Hello World: 1. From the main menu, navigate to File | New | ActionScript Project. You will see the following screenshot. Enter a project name HelloWorld or any other name of your choice.
[ 31 ]
Game Interface Design
2. Since we already have the entire source required for Hello World from the Pulse package, click on Next to specify that the source folder is already on the disk. This will bring up the following screen where we choose the Hello World src folder as shown. Note that the screenshot shows that Pulse was installed under F:\Gamantra. This path may be different on your computer.
3. Once we have chosen the source folder, we still need to choose the main source folder and main application file. Unfortunately, in order to do this, we need to navigate a bug in Flash Builder 4. You need to click on the Back button and then again on the Next button, bringing us back to where we were. 4. We now click on the Browse button, as shown in the screenshot, and choose the [source path] src and click on OK.
[ 32 ]
Chapter 2
5. Next we choose the main application file—this determines the main class file that the execution will start with.
[ 33 ]
Game Interface Design
6. We need to tell the Flash Builder to use the Pulse libraries for this project. In the Flash world, the library files come with an extension .swc, which stands for shockwave component. Once you make it available to your project, you can start using the classes and functions that are exposed from within the library. In order to do so, choose the Library Path tab view and click on the Add SWC… button; navigate to the lib folder within the Pulse installation folder and choose Pulse.swc and once again do the same procedure for PulseUI.swc. Click on the Finish button.
As a final step, before attempting to run the sample, we also need to set the stage size to 800 (width) by 600 (height). The PulseUI requires the stage size to be exactly this size. We may also set the background color of our choice as shown in the following screenshot:
[ 34 ]
Chapter 2
After this step, Flash Builder 4 should be able to crunch all the code in folders and report no problems. This will also create the swf files under the project folder within the workspace ready for you to take it for a spin. At this point, you may also use the debugger to step through the code. But make sure the Pulse server is running so that you may login and explore all the screens.
The Hello World specification
The Hello World client will be able to create a new HelloGameState and share it with other players, and any player may change the x and y and have that change reflected in every player's screen. Here is the final screen that we will end up with:
[ 35 ]
Game Interface Design
The screenshot is that of the game screen. The circles are a visual representation of the game states, the position of the circle comes from the corresponding game states x and y values and so does the color from the color property. We will have two buttons: one to add new game states and another to remove them. To add a new circle (a game state), we click on the Add button. To remove an existing game state, we click on any of the circles and click on the Remove button. The selected circle appears to be raised like the one on the far right-hand side of the screenshot. We may also modify an existing game state by moving the circles by clicking and dragging them to a different position—doing that on one client, we can observe the change in every other player's screen as well.
The schema file
For any Pulse-based game development, we first start out with an XML-formatted schema file. Let's now explore the schema file for the Hello World sample. The game developer must create a schema file that specifies all the needed game states, avatars, and game room objects. After you have created the schema file, we then use a Pulse modeler tool to create the class files based on the schema to be used within the game. So first let's examine the schema file for the Hello World project:
<property index="0" name="x" count="1" type="int"/> <property index="1" name="y" count="1" type="int"/> <property index="2" name="color" count="1" type="int"/>
Navigate to the project folder where you have created the project and create a file called GameSchema.xml with the above content.
[ 36 ]
Chapter 2
We will go through the details of the XML file in greater detail later in the next chapter. For the Hello World sample, we will define a game state object that we can use to share game states among all the players within a game room. We will name the class as HelloGameState, but you are welcome to call it by any name or something that makes sense to your game. You may also define as many game state classes as you like. For the HelloGameState in the schema file, each game state instance will define three properties, namely, x, y, and color.
Code generator
In order to create the AS3 class files from the schema file, you need to run the batch file called PulseCodeGen.bat found in the $\bin folder. It takes the following three parameters: 1. Path to schema file. 2. Namespace. 3. Output directory. In order to make our life easier, let us create a batch file that will call the PulseCodeGen and pass all the required parameters. The reason for creating the batch file is that you have to code generate every time you modify the schema file. As you progress through your game implementation, it is normal to add a new class or modify an existing one. The convenience batch file may look like what's shown next. Let's call it init.bat and save it in the same root folder for the sample along with the schema file. @ECHO OFF IF EXIST .\src\hw\gsrc\client del .\src\hw\gsrc\client\*.as CALL "%GAMANTRA%"\bin\PulseCodeGen.bat .\GameSchema.xml hw.gsrc .\src\ hw\gsrc IF NOT %ERRORLEVEL% == 0 GOTO ERROR ECHO Success! GOTO END :ERROR ECHO oops! :END pause
The schema file parameter to the Pulse code generator is specified as .\GameSchema. xml because the schema file and the batch file are in the same folder. The second parameter is the package name for the generated classes—in this example, it is specified to be hw.gsrc. You specify the directory that the generated classes will be saved to as the last parameter. Note that the code generator appends client to both the package and directory into which it will be saved. It is also important to match the package name and directory structure as required by the AS3 compiler. [ 37 ]
Game Interface Design
Upon running the code gen successfully, there is one AS3 class generated for each class in the schema file and two additional supporting class files. One is a factory class called GNetClientObjectFactory, which is responsible for creating new instances of generated classes, and the other is GNetMetaDataMgr, which aids in serializing and de-serializing the transmitting data over the network. The data carried is what resides in the instances of generated classes. You don't need to deal with these two extra classes first hand; it is mostly used by the underlying Pulse runtime system. As for the generated classes for what is defined in the schema file, the name of the class would be identical to what is specified in the schema file plus the suffix Client. In this example, there would be a class generated with the name HelloGameStateClient.as. Go ahead and try running the batch file called init.bat under $\samples\ HelloWorld. If you have trouble running the batch file, double-check the post installation steps in Chapter 1.
Project directory structure
The Hello World that is part of the Pulse package is organized into the following directory structure: •
hw °°
gsrc °°
°°
rsrc
°°
ui
client
The package hw being the root package contains the main class HelloWorld.as, and the gsrc as you see contains the generated class. The rsrc folder contains the skin files, which we will discuss in more detail later in this chapter. The skin files in Pulse consist of three PNG files that provide all the basic buttons and background for the game. You can simply replace these PNG files with your own set, provided the individual elements with the file are of the exact dimensions and are placed at the exact same positions.
[ 38 ]
Chapter 2
Introduction to PulseUI
Before we start exploring the code within Hello World, let's briefly check out the PulseUI framework. It contains numerous classes that implement the screen management for a multiplayer game. For us to leverage the PulseUI framework, we simply need to subclass and override the classes defined in the framework. The bare minimum classes to subclass from PulseUI are: •
PulseGame
•
Skinner
•
NewGameScreen
•
GameScreen
We will explore each of these classes as we progress through the book.
Screen management in PulseUI
The following figure shows you the various screens that are managed and the arrowhead shows how different screens could be navigated:
The PulseUI starts off with the login screen as one would expect for any multiplayer game. Upon successful login, we enter the lobby screen, where the player can browse through all the available rooms and join one of them. From the lobby screen, the player may also visit the registration screen or the top ten screen. The registration screen allows players to quickly register, which then provides them with a login username, password, and an avatar name. The top ten screen shows off the top ten players as well as the player's own ranking. [ 39 ]
Game Interface Design
The PulseGame class
This class is where all the action starts for our multiplayer games. The game code must subclass the PulseGame class in order to leverage the initial set of required boot strapping. It also makes sense for our subclass to be the main class for the project, although it does not have to be. PulseGame instantiates and holds a pointer to the GameClient to publish any action from the game client to the server. It also implements the GameClientCallback
interface that implements the logic when the notifications from the server are received.
Let us now look at the methods that must be overridden. Starting with the constructor of the subclass (MyGame), you should instantiate your subclass of the skinner. The first thing that the constructor of PulseGame does is to create and init the login screen, as it will most probably be displayed right after the splash screen has done its thing. It is important to have the static protected property defined in PulseGame s_ instance set to our subclass instance of PulseGame as shown below: public function MyGame() { s_instance = this; new MySkinner(); super(); }
Note that we also want to call the super class's (PulseGame) constructor only after we are done with our initialization. The reason is that PulseGame is a singleton, and there are numerous places within the framework that require access to this singleton. The previous code makes sure that the instance of our subclass is returned in all cases.
[ 40 ]
Chapter 2
The following figure shows the methods calls stack. Important to note here is that all the methods are protected, meaning that we can customize default behavior of the class at any step on the way. In order to provide our own customized login screen, we simply override the initLoginScreen. If we wanted something fancy during the splash, we could override the splash method. On the other hand, if we simply wanted to provide a different sprite for the splash instead of the default, we would override the getSplash method. We could also entirely skip the splash screen by overriding the splash method and calling onSplashDone, as shown below: protected override function splash():void { onSplashDone(); }
If the splash method is completely overridden, it is the subclass's responsibility to call the onSplashDone method after the splash screen has done its thing. The default behavior of onSplashDone is simply to call the start method.
[ 41 ]
Game Interface Design
The start method then initiates the following series of method calls. First it instantiates the GameClient, the main API class of Pulse, which enables all communication with the server. This method must be overridden if the game defines a schema, which is true in most cases. The login screen is then initialized so that it can create any sprites that are needed for display, and finally makes a call to display the login screen to the player.
In the case of advanced implementation of a game, the showLogin method may be overridden to read parameters passed from the HTML embedded code into the Flash game swf. In some cases, the game may not show the login screen, but retrieve the username and session from the flash parameters and directly invoke the server login. Upon the server's response to the login request, the Pulse SDK serves up a GameLoginEvent defined in the Pulse layer. The login event callback, onLogin method, is called. The login event passed in may be examined to determine if the login was successful or returned an error. The following is the default implementation that you may find in PulseGame: protected function onLogin(event:GameLoginEvent):void { if ( event.isSuccess() ) { init(); // init the game client screen m_timer.addEventListener(TimerEvent.TIMER, processMsg); m_timer.start(); } else { var loginError:int = event.getErrorCode(); if ( loginError == GameErrors.ERR_LOGIN_FAIL ) { // try again } else { // Server un-available!! } } } [ 42 ]
Chapter 2
We see that if the login was successful, we can continue with the initialization for the rest of the game screens. This happens in the init method. The init method first draws the outline, which serves the background for the entire game and never changes; other game screens are laid on top of the outline. The init method after drawing the outline then initializes all the other screens. The following is how the init method looks in the PulseGame class: protected function init():void { // Show the outline drawOutline(); // init other screens initLobbyScreen(); initGameScreen(); initNewGameScreen(); initHiScoreScreen(); initRegisterScreen(); postInit(); }
Each of the init methods may be overridden to create our versions of the screen. For example, a custom lobby screen may be created by overriding the initLobbyScreen. The outline, in addition to drawing the background, also shows the player's own avatar representation. We may provide our own implementation of the outline by simply overriding the initOutline method. The final step of the init method is the postInit, which the subclass may override to perform any additional tasks or drawing required for the game. The default implementation of the postInit requests the server to take the player to the lobby.
Exploring the Hello World sample
In the following sections, we will explore parts of the Hello World sample, specifically dealing with the initial start-up phase.
HelloGame.as
The main class called HelloGame inherits from PulseGame, which is defined in PulseUI. In the constructor of the class, we initialize the Skinner instance that provides buttons and background for the game sample. public function HelloGame() { s_instance = this; initSkinner(); [ 43 ]
Game Interface Design super(); } protected function initSkinner():void { new HelloSkinner(); }
Since the Pulse server is capable of serving multiple games, we need to override the getGameId method and provide a string that must be unique among all the games hosted by the server. public override function getGameId():String { return "HelloWorld"; }
In order to connect to the server for sending and receiving messages, we need to create the Pulse level class instance called GameClient. To instantiate the network communication client, it also needs the instance of the generated factory class. protected override function initNetClient():void { var factory:GNetClientObjectFactory; factory = new GNetClientObjectFactory(); m_netClient = new GameClient(factory, this); }
In the Hello World example, we will modify only two screens—one screen where a new room is created and another the game screen. We will leave all the other screens such as lobby, high scores, and register as they are. protected override function initNewGameScreen():void { m_newGameScreen = new NewGameRoomScreen(); m_newGameScreen.init(); } protected override function initGameScreen():void { m_gameScreen = new HelloGameScreen(); m_gameScreen.init(); }
Whenever a new game state arrives, an existing one is removed or modified, the corresponding methods on the PulseGame are invoked. Remember that this HelloGame class inherits from PulseGame, so we can simply override these methods and pass it to the game screen for update. public override function onNewGameState(gameState:GameStateClient):void { (m_gameScreen as HelloGameScreen).onAddGS(gameState); } public override function onUpdateGameState(gameState:GameStateClient):void { [ 44 ]
Chapter 2 (m_gameScreen as HelloGameScreen).onUpdateGS(gameState); } public override function onRemoveGameState(gameState:GameStateClient):void { (m_gameScreen as HelloGameScreen).onRemoveGS(gameState); }
That's it for the main class! The reason why it seems so simple to write games based on PulseUI is that all the game mechanics are handled by PulseUI framework. What we are doing is simply modifying the default behavior.
The login screen
The login screen is the first screen to be shown for any multiplayer game:
It offers two buttons, one being the Guest button for which the player need not type any username or password. In this case, the player will be logged in as a guest. If the player does have a username and password, the player needs to click on the OK button after filling in the fields. The login class allows one to customize the login screen, so the server IP does not show up in the login screen. After creating the login screen, we may simply call the showIP method and pass false. The best place to call the method is by overriding the initLoginScreen in your subclass of PulseGame class as follows: protected override function initLoginScreen():void { super.initLoginScreen(); m_login.setIP("127.0.0.1"); m_login.showIP(false); }
[ 45 ]
Game Interface Design
We also need to remember to set the IP of the Pulse server. During the development phase, you may want to show the IP field, but during deployment, you may want to hide it and set the IP value programmatically. The login method takes in the IP of the server and port. By default the IP is the localhost, but could be any IP on the Internet. The Pulse server always listens to port 2021, so the client must pass the same value. For the enterprise edition of Pulse, this port may be configured. public function login(ip:String="127.0.0.1", port:int=2021, username:String=null, password:String=null, sessionId:int=0)
The user name and password may be passed from user input or could be null in case the player wishes to log in as a guest. The session ID in most cases can be passed a value of zero. The session ID is provided for advanced game portal implementation where a user gets assigned a session ID from the web portal implementation. From then on, the player may jump from one page to another, containing different games. In this case, for providing the user name and the session ID, the user need not log in every time a new game is attempted to be played.
The screen class
This is the base class for all the screen classes. It defines the bare minimum methods such as init, show, and hide. Each subclass then overrides these methods for their specific sprite management. public class Screen extends Sprite { protected var m_showing:Boolean; public function Screen() { } public function init():void { } public function show():void { if ( m_showing ) { trace("Already showing!"); return; } m_showing = true; } public function hide():void { [ 46 ]
Chapter 2 if ( !m_showing ) { trace("Already hidden!"); return; } m_showing = false; } public function isShowing():Boolean { return m_showing; }
For each screen, the init method is called once, where the subclass may initialize any sprites it needs, ready to be displayed. The show and hide methods can be called several times during the life of the game. The show method is called when the screen is to be displayed and hide to remove it from display. All screen-swapping is done by the showScreen method in PulseGame. If there is any custom screen class (for example, a credits screen) other than those subclassed from the PulseUI framework, it is recommended that it is inherited from the screen class so as to leverage the existing screen management code.
The skinner class
The skinner class provides a simplistic way to change the UI specific to your game. It is meant to provide a quick way to an alternative set of UI theme. The skinner class provides the art asset for all the UI in the game except for the art required for the game itself. It provides the background and buttons sprites for the general UI. The skinner is designed to take in three PNG files, namely online.png, ui.png, and frame.png. Outline.png provides the art that is behind everything, which means the outline is
drawn first and everything else is laid on top of it. From start to finish, the outline does not change and is always seen if nothing draws to cover it.
[ 47 ]
Game Interface Design
Frame is a small section to hold various UI elements in login and new game screen. Both outline and frame are simply copied from the file and displayed as they are onto the screen.
UI.png supplies all the required buttons for all the screens except for those required
specifically for the game. Once the file content is copied into memory, it cuts it up into various pieces. The position and size of each UI element is hardcoded, which means that when you want to provide your own UI, the elements must be in the same position and size as the original.
If you needed something more sophisticated, you would need to replace the skinner with your own implementation as well as with various parts of PulseUI code where this is accessed.
[ 48 ]
Chapter 2
The outline class
As you saw in the walk-through of the PulseGame object, the outline object is the first one to be painted onto the screen. Let us now dissect the outline class in detail. The following screenshot shows the lobby screen that lays on top of the outline. The outline class draws the following: •
Background
•
Top-left avatar
•
Friends bar
•
Chat history
•
Chat input
All the other UI elements are laid by the lobby screen, which we will discuss shortly. Also note that the outline class is not a subclass of screen object, although it could be. The reason it does not have to be is because the outline is always present in the background as opposed to other screens, which they may show and hide many times over.
[ 49 ]
Game Interface Design
Player registration
A player may register and create an avatar right within the game. A guest player may register via the RegisterScreen. The basic information for a player to register is: • • •
Username Password Avatar name
All functionality to register a guest is already provided by the PulseUI framework. Similar to the HiScore screen, you may customize the font and color by overriding the getFormat method.
Exploring the game server deployment Let us explore in detail some of the functionality of the game server.
Registration and login
As simple as it may sound, the login is a complicated beast. From a user experience stand point, login must be simple no matter what happens behind the scenes. In order for new users to get into your game, there are the three essential steps: 1. Registration 2. Authentication 3. Login [ 50 ]
Chapter 2
Registration
Registration is the process where new users create their account name and set their password usually through a website. Although it is a common practice to implement this functionality as a web-based application, players are often allowed to register within the game. This has the advantage of players not having to go to another screen, typing in the URL, and so on. Casual players just don't have the patience.
When a new user successfully creates an account, the backend web server creates the database entry with the user name, password, and other collected information. This database is called the authentication database or Auth DB for short. The authentication DB is then used to authenticate users during the time when players are attempting to log in to the game. Other important information that the authentication database may store is the user's place of residence, e-mail, date of birth, etc. This data often comes from the user entry during the registration process. The other kinds of information that could be automatically collected are data such as date and time of registration, the IP from where the person is registering, etc. These data are valuable to collect in order establish trends as the game progresses in its deployment stages. Further, the authentication DB could also keep track of the login activity. Every time a player logs in or out, a DB log is recorded along with other information such as the IP. This kind of data is helpful in discovering the user trends of your community. For example, this data can help you find out the last players activity in the week and be designated as the server update period, or if the user activity was high at certain periods of a week or day, it would allow yourself to be more cautious about server stability during these times.
[ 51 ]
Game Interface Design
The login
A login may be as simple as the client logging into the game server. It opens a network connection to the server to send the username and password. The server then verifies the username and password, and allows or denies the client to log in. This may be true during the development of the game, but when the game is deployed publicly and when a large number of people are on the game cluster, the login process soon gets complicated during a public deployment. The client first contacts the balancer, which keeps track of all the load statistics of all the processes within the game cluster. The balancer first determines if the client is the latest release that is compatible with the server. If so, then the login process continues further.
The balancer then determines the least loaded session server. The determination of least load is a combination of the number of client connections the server is maintaining and the CPU usage of the machine. The balancer then sends the session server's IP and port to the client for it to connect. The client then proceeds to connect to the assigned session server, which then must authenticate the username and password. There are two common schemes during the login process that the authentication could be implemented.
[ 52 ]
Chapter 2
In the early authentication scheme, the authentication is verified before the connection to the session server is established. If the authentication passes, the login server will inform the appropriate session server to expect a login from the client. The session server will receive the username, and for additional security, a session key is also passed. This session key is also sent to the client, so when the client connects to the session server, the client sends the username and the session key for the session server to validate the connection. Notice that this scheme further splits the responsibility to another process, the login server. In lazy authentication, the session server is responsible for initiating the authentication and either accept or deny the request. The lazy authentication is much simpler than the early authentication mechanism.
Dealing with multiple logins
The one common issue that must be dealt with in a public deployment is the issue of multiple logins. There are two options that could be implemented when a duplicate login is detected: 1. Disallow the second login. 2. Disconnect the first login.
[ 53 ]
Game Interface Design
Even though the first option seems simpler, it is not commonly adopted. There are two reasons why the second option is more practical and preferred. One technical reason is that sometimes there is a chance that there is a stale server session after a client crash, so the second login forces a cleanup of these stale sessions. If such a problem is present in a deployment, it should be fixed by the developer. But a more legitimate reason for opting for option two is the user behavior—a player may leave the computer without logging out of the game and may then choose to log in to the game from another place. When the player attempts to log in, the first loggedin client will be disconnected and the player can play from the other computer. A similar situation may arise even within the same computer when the user opens another browser window or tab and initiates a fresh login.
Guest logins
Guest logins are great for new players to try out without having to go through the extensive registration process. It provides an opportunity for them to explore your game very quickly. Guests are special; they are let into the game without a login. But ultimately, and depending on the game, you would want them to register, especially for a multiplayer game. What sets multiplayer games apart from single player games is that the players are part of a community, which means that identity becomes an important aspect to players. Players who log in as guests are restricted players in many ways; for one thing, they are seen as guests to other players, meaning they don't have a unique identity. Most often their data is not saved to the database, meaning that their high score or experience are not kept track by the server. These restrictions encourages a guest player to sign up and be an active member of your community while giving them an opportunity to experience your online game.
Summary
In this chapter, we got our feet wet exploring the Hello World sample. We learned how to set up the project in Flash Builder 4 and the project directory structure. We also learned how to create a game schema and how we can set up the code generator. Now we also have a handle on all the screens required for the game and how PulseUI manages it for us. Finally, we have made the first contact with the server and logged in the client.
[ 54 ]
Avatar and Chat In this chapter, we will learn how to go about designing the avatar for a multiplayer game. We will explore the Hello World sample related to avatar and the essential features related to an avatar, such as chat and making friends. We will start by learning about Pulse Modeler, a tool that will help us design all the objects required for a multiplayer game. Learning about the modeler is essential because that is where we begin designing our game objects that will be shared across the network with other players. In the following chapters, we will use the modeler to design rooms as well as the objects to implement the game itself. Specifically, we will learn the following: •
The Pulse modeler
•
Modeling the avatar in the game
•
Displaying players in the game
•
Managing friends
•
Chat
•
Keeping track of high scores
Introduction to Pulse modeler
The Pulse game development begins with a game schema file. Pulse modeler, among other things, generates code based on your schema definition and helps us create an object-oriented environment for ease of programming. Behind the scenes, during runtime, it also helps reduce bandwidth during client-server communication.
Avatar and Chat
You will find PulseCodeGen.bat under $\bin. It requires three arguments: 1. Schema File: the game schema file. 2. Package name: a suitable package name according to your game code structure. 3. Output Directory: the directory where the modeler should put your source files into. If your game does not need any custom objects defined, you still need to create an empty schema file as shown below and have it run through the modeler:
For each class defined in the schema file, you may specify the parent class, be it one of GameAvatar, GameRoom, or GameState. The following are the only three classes from which the developer should inherit from and add properties that are required for the game: •
GameAvatar
•
GameRoom
•
GameState
Example schema
The following schema is borrowed from the multiplayer jigsaw game example that comes bundled with the Pulse SDK, which we will explore in great detail in Chapter 7:
<property name="pieceId" type="int"/> <property name="dir" count="1" type="int"/>
[ 56 ]
Chapter 3 <property name="x" type="float"/> <property name="y" count="1" type="float"/>
The entire schema should be defined within the
tag. The import section remains pretty much static and must be included as part of your schema. Failing to do so would get you compiler errors. The import section tells the code generator to include the parent class files so that it can resolve the parent class references in the generated class files. Each class is defined within the tag; the name attribute is the name of the class that gets generated; and the parent must be one of GameAvatar, GameRoom, or GameState. You would normally want to define just zero or one GameAvatar and GameRoom entities as required. Depending on the game, you may want to define zero or as many GameState entities. It is important to note that the class ID must be unique and must start from 600 or above. The generated code for PieceMatch is PieceMatchClient—the suffix indicates that the generated class is a client-side entity. All properties defined must be within the tag. The private and system tags are used only in the GNet product family. The name of the property must be unique within the class definition. The following table describes data types allowed in the schema. The count by default is 1 if specified, and if the count is greater than 1, then the property generated will be of AS3 type Array.
[ 57 ]
Avatar and Chat
It is quite hard to get the schema right the very first time as you progress through the development of the game, and when adding those last minute features, one may discover that either new classes are needed or new properties are needed for an existing class or that even some classes or properties are defunct and are no longer needed.
Whenever such a need arises, any schema update should be followed with a code generation and then the game code may be able to access and use the new classes and properties. The following table enumerates the various data types available for the properties within a class: Type name
Length (bytes)
AS3 Type
Notes
chars
1 * count
String
Converts to UTF bytes. The count specified in the XML schema file must be large enough to hold the encoded bytes.
byte
1 * count
int
Only one byte times count is transferred over the network.
Array (count > 1) short
2 * count
int Array (count > 1)
[ 58 ]
Only two bytes times count is transferred over the network.
Chapter 3
Type name
Length (bytes)
AS3 Type
Notes
int
4 *count
int
Four bytes times count is transferred over the network.
Array (count > 1) float
4 * count
Number Array (count > 1)
double
8 * count
Number Array (count > 1)
timestamp
8 * count
Date Array (count > 1)
Four bytes times count is transferred over the network. Eight bytes times count is transferred over the network. Eight bytes times count is transferred over the network.
Design of a game avatar
There are numerous pieces of information that need to be tracked by a game server. Beyond being just a game server, which enables multiple players to communicate, the deployment needs to be more of a social platform for attracting and retaining the player visit counts. One of the main pieces of information is of course related to the player's representation within the community—the avatar. We will use the word 'object' not purely in terms of object-oriented programming, which it could very well be, but here in this context to mean as a package of related data. Avatar is different from a user object, as they both serve different purposes: for example, the user object mostly deals with the actual person logging into the game. The user object contains things such as username and password to log in; it could also contain information such as date of birth, date of registration, and so on. Further, in some game environments, one user could have several avatars associated, although only one avatar gets to enter into the game for a user at the same time. Avatar is a representation of a player within a game or a community. Avatar is a placeholder for all the properties that is related to the player and more specifically the game related properties. For example, it contains name, points, experience, level, among others. An avatar object is also persisted to the DB. In an industry that has shifted towards social communities, it becomes all the more important for players to identify themselves with the avatar that grows in experience, level, etc. The players then come back to their avatar and friends that the platform remembers. The avatar could also be visually represented either with a 2D picture or in 3D. In addition, the avatar may also have the virtual money to spend, which is also tracked in the avatar object. [ 59 ]
Avatar and Chat
Modeling the avatar
If the game you are developing requires the avatar or player to carry more information than already defined in the GameAvatar object, you need to specify a class in the schema file with a parent set to GameAvatar. Note that there should only be one such subclass defined. Property
Notes
Score
Use this to keep track of the score within the game. The score is reset to zero when a player enters a room.
isHost
A Boolean property that is true if the avatar is the game room host.
exp
The total experience for the avatar is stored in this property.
cash
This property may be used to store remaining virtual cash the avatar has.
level
The overall level of the avatar on the game portal or of a game if only game is being hosted.
ready
When inside a game room, this property is true if the player is in ready status.
pos
The position of the player around the table inside of the game room. Starting from 0 for the game host and increasing by one for every player entering the room.
The developer may inherit from GameAvatar and specify an arbitrary number of properties. Note that the total number of bytes should not exceed 1K bytes. The reason for this limitation is that data objects are managed on the server in chunks of 1K bytes. Although this puts a limitation on the game developer, it is done to maximize efficiency and should be enough for any casual game that you want to implement.
Avatar display in Hello World
The default behavior of PulseUI shows avatars in a few places. Upon successfully logging in, the first screen that is rendered is the lobby interface; the avatar of the player is shown in the top right-hand corner of the screen. It is also shown in the same place inside the game screen. The avatar is also displayed in the friend list panel, where it shows friends of the player and during the game play where other players in the game room are. Following is the screenshot of the lobby screen:
[ 60 ]
Chapter 3
When inside the game room, as and when the players join the room, the player entering shows up on the left-hand side of the screen.
Customizing Player Display
In the HelloWorld sample, we don't customize any of the default behavior regarding avatar display. For this reason, we don't need to write any special code in the HelloWorld project. However, if you do need to customize the default behavior, the following describes various parts of PulseUI that deals with displaying the avatar. At this point, if you like, you may skip this section and come back to it later when you do feel like changing the appearance of the avatar. [ 61 ]
Avatar and Chat
The same PlayerDisplay class is used in multiple places for drawing a player's own avatar, in a friend's panel, and in the other player's panel in the game screen. Let's walk though some of the code for PlayerDisplay in the PulseUI framework. The PlayerDisplay class is a subclass of the Sprite class and adds the following variables: public class PlayerDisplay extends Sprite { // The corresponding avatar object public var m_av:GameAvatarClient; // A sprite that holds all the child sprites private var m_sprite:Sprite; // for score display private var m_score:TextField; // Special case to display the self private var m_avDisplay:Boolean; // Display sprite of a game host private var m_gameHost:Sprite; // If host sprite is displayed private var m_hostDisplayed:Boolean; // Make friend sprite button private var m_makeFriend:Sprite; // Avatar Display picture private var m_pixSprite:Sprite = null; … }
The constructor is shown below: public function PlayerDisplay(av:GameAvatarClient, avDisplay:Boolean = false) { m_av = av; m_avDisplay = avDisplay; drawMe(); }
The constructor takes in two parameters: av, which is the avatar object that the sprite is representing, and avDisplay, a Boolean value that should be passed in true if the instance is created to display the player himself or herself. The drawMe function does all the work of creating and placing the child sprites within the PlayerDisplay sprite. [ 62 ]
Chapter 3
The following items are displayed for each avatar: •
The avatar display picture
•
Score
•
Make or break friend button
•
Game host sprite, in case the avatar is one
The subclass of PlayerDisplay may override the drawMe method to provide the custom look and feel. The constructor also sets up mouse handlers for choosing a private chat and for making or breaking a friend relationship. The following shows an update method, which is called whenever the client receives an update for the avatar that is being displayed by this instance: public function update():void { if ( m_score != null ) { m_score.text = "Score: " + m_av.getScore(); } if ( m_avDisplay ) return; updateMakeFriend(); if ( isGameHost() ) addGameHost(); else removeGameHost(); }
The update method first updates the score field if the instance is not being displayed for the player. The method also updates the make friend-related sprites and finally checks if the game host sprite needs updating. The updateMakeFriend method checks if the avatar of this instance could be made friends with and updates the make friend button. private function updateMakeFriend():void { // Add add friend icon/button, // if not friend already and not myself if ( canMakeFriend() ) { // not self and not a friend, yet. if ( m_makeFriend == null ) { m_makeFriend = Skinner.getAdd(); new ButtonEffect(m_makeFriend); m_makeFriend.x = 70; m_makeFriend.y = 60; [ 63 ]
Avatar and Chat } m_sprite.addChild(m_makeFriend); m_makeFriend.addEventListener(MouseEvent.CLICK, makeFriend); } else { // For a new friend made, // remove the make friend // is exists if ( m_makeFriend != null && m_sprite.contains(m_makeFriend) ) m_sprite.removeChild(m_makeFriend); } } /** * * @return returns false for self and if already a friend * */ protected function canMakeFriend():Boolean { var gc:GameClient; gc = PulseGame.getInstance().getGameClient(); return ( (gc.getMyAvatar().getId() != m_av.getId()) && !gc.isFriend(m_av.getId()) ); }
As we will see in the next chapter, if the game host leaves the room, the server picks another player at random to be the game room host. The following methods take care of any such game room host updates: /** * Checks if the avatar is a game host * * @return true if avatar is the game room host * */ protected function isGameHost():Boolean { var gc:GameClient; gc = PulseGame.getInstance().getGameClient(); if ( gc.getMyGameRoom() != null ) return (gc.getMyGameRoom().getHostId() == m_av.getId()); else return false; } [ 64 ]
Chapter 3 /** * Safely adds game room host sprite */ private function addGameHost():void { if ( m_hostDisplayed ) return; if ( m_gameHost == null ) m_gameHost = Skinner.getGameHost(); m_gameHost.x = 70; m_gameHost.y = 10; m_sprite.addChild(m_gameHost); m_hostDisplayed = true; } /** * Safely removes game room host sprite */ private function removeGameHost():void { if ( !m_hostDisplayed ) return; m_sprite.removeChild(m_gameHost); m_hostDisplayed = false; }
The management of the player display is taken care of by the GameScreen class along with PlayersDisplay and PlayerDisplay classes in the PulseUI framework. Each player display also contains a small circle, which when clicked on, initiates a friend request. The circle does not show up for the player himself or herself and for those who are already friends. The game host is indicated with a star icon with the player display. The following figure shows the instances for both classes that are created within PulseGame:
[ 65 ]
Avatar and Chat
In order to supply our own PlayersDisplay, we first need to override the initPlayersDisplay method in our subclass of PulseGame (MyGame). The overriding code is as follows: protected override function initPlayersDisplay():void { m_playersDisplay = new MyPlayersDisplay(); }
If we want to supply our own subclass of PlayerDisplay (MyPlayerDisplay), we need to override the createNewPlayerDisplay method as follows: public override function createNewPlayerDisplay(av:GameAvatarClient, avDisplay:Boolean = false):PlayerDisplay { return new MyPlayerDisplay(av, avDisplay); }
Note that we should create the MyPlayerDisplay directly, but always via the above method. The constructor of PlayerDisplay takes in two parameters, one being the avatar object and the a Boolean value, which in most cases is false, hence the default value. One instance of PlayerDisplay class is also used for displaying the my avatar icon as part of the outline. This always appears on the top left-hand corner of the screen. In case of this instance, several things like the game host is not drawn.
Avatar-related APIs
Let us now explore some of the Pulse SDK APIs that are related to the avatar. This includes a subset of methods on GameClient and a subset of callback methods on the GameClientCallback interface.
PulseGame client APIs
The following API will return the player's own avatar; the returned value is valid only upon successful login: public function getMyAvatar():GameAvatarClient
The getPlayer method returns a player with the given ID. This method is only useful when the avatar is inside a game room, as the game client will not have access to other players until then.public function getPlayer(id:int):GameAvatarClient This getAllPlayers method returns all the players within the same game room as the player: public function getAllPlayers():Array [ 66 ]
Chapter 3
The getAvatarPix method returns bitmap data for the avatar's picture. The returned values may be null if the image is not loaded from the server. The avId is the ID of the avatar whose picture is being requested. The PulseUI framework, specifically PlayerDisplay and FriendDisplay, will use this API to get the image for the corresponding avatar: public function getAvatarPix(avId:int):BitmapData
Pulse game client callbacks
The callback method is fired to provide the developers subclass of the GameAvatarClient. This would be the instance of the class that is modeled within the schema. If no customized avatar was created, the implementation can simply return null. function onCreateNewAvatar():GameAvatarClient
Since loading images over the Internet is asynchronous, it often takes time to fetch the image and when that happens, the above callback is fired and PulseUI will update the corresponding PlayerDisplay or FriendDisplay instance. function onAvatarPixLoaded(avId:int, pixData:BitmapData):void
In order to conserve memory resources, Pulse only keeps track of other players that are relevant. This being friends and players in the room with the player. When the player leaves the current game room or when the player is no longer friend with another player, the associated resources are released. During this callback, the developer can uncache any such resources. function onAvatarPixUnloaded(avId:int):void
Friends management
Friends get people visit your site again, of course, besides offering fun games. Friends are established on the platform between people who already know each other and new ones are discovered while interacting on the game community site. Making new friends involves the following steps: 1. Player A sends a friendship invitation to Player B. 2. Player B receives a notification. 3. Player B either accepts or rejects the friendship. 4. If Player B accepts, a friendship relation is established. [ 67 ]
Avatar and Chat
The steps are fairly simple as you might have experienced in any standard instant messenger. The pending notifications are kept on the server until the recipient (Player B) responds. The pending notification may be purged in case the recipients don't respond in a certain time period. The friendship relation is kept in a different object or a database table, shown as follows:
Note that other additional properties may also be tracked, such as the date when players became friends, a status field to keep track if the friendship is fully established or is still pending, etc. Another important function for a game server to perform when an avatar logs into the game is to send notifications letting the friends know that the avatar is now online and also to let the entering avatar know of all the friends that are currently online. The game server also sends out similar notifications when an avatar logs out of the game.
Friends in Hello World
In Hello World, we leave all the default behavior related to friends up to the PulseUI framework. That is the reason why we don't see any code in the Hello World project.
The friends API
As you may have seen in any standard instant messenger, making friends starts with a player requesting to add another player as a friend. The recipient may accept or reject the offer. If accepted, the friendship is formed until one of them decides to terminate the relation. One of the API methods to make new friends or break existing friends is as follows: public function makeFriend(avatarName:String):void public function respondToFriendInvite(accept:Boolean, avName:String):void public function breakFriend(avatarName:String):void
[ 68 ]
Chapter 3
To make a new friend, the game code is required to call makeFriend API with the name of the avatar. The recipient will receive the onMakeFriend notification. function onMakeFriend(newFriend:GameAvatarClient):void;
The recipient may then respond by calling the respondToFriendInvite method. The avName parameter should be the same as the initiating avatar's name.
Customizing friends display
The following figure shows the classes that create the object instance that are responsible for displaying friends. You may observe the slight difference between the creations of player display objects and the friend display objects. Unlike player display objects where both are created by PulseGame class, the friends display is created by PulseGame, but the FriendDisplay is created by the FriendsDisplay class.
The following code overrides the method in MyGame to create our own subclass of FriendsDisplay (MyFriendsDisplay): protected override function initFriendsDisplay():void { m_friendsDisplay = new MyFriendsDisplay(); }
[ 69 ]
Avatar and Chat
To create our own version of FriendDisplay (MyFriendDisplay), we override the method in MyFriendsDisplay as shown: public override function createNewFriendsDisplay(av:GameAvatarClient):FriendDisplay { return new MyFriendDisplay(av); }
If we explored the PlayerDisplay, we will find that the little circle sprite registers for a mouse-click event that handles the request to make friends with the corresponding avatar associated with the PlayerDisplay instance. protected function makeFriend(event:MouseEvent):void { var gc:GameClient; gc = PulseGame.getInstance().getGameClient(); gc.makeFriend(m_av.getName()); }
Similarly, when a friend relation is broken by clicking on the little square sprite on the Friend Display object, a request is sent to the server in code as shown below: protected function breakFriend(event:MouseEvent):void { var gc:GameClient; gc = PulseGame.getInstance().getGameClient(); gc.breakFriend(m_av.getName()); }
[ 70 ]
Chapter 3
The following is the code that handles the notification sent by the server for a friend request. The code is handled in PulseGame. The following callback method is implemented, which simply accepts the friend request. In our subclass of PulseGame, we may want to inform that a request was received, and accept or reject the request based on the player's response. public function onMakeFriend(newFriend:GameAvatarClient):void { m_netClient.respondToFriendInvite(true, newFriend.getName()); }
PulseGame also handles all the updates regarding friends: public function onFriendUpdate(status:int, errorCode:int, aFriend:GNetFriend):void { switch(status) { case GameConstants.F_ACCEPT: // new friend, create a new FriendDisplay var av:GameAvatarClient; av = aFriend.getAvatar() as GameAvatarClient; m_friendsDisplay.addFriend(aFriend); m_playersDisplay.updatePlayer(av.getId()); break; case GameConstants.F_REJECT: // player rejected friendship :( // inform the player.. break; case GameConstants.F_BREAK: // no longer friends, anymore! :( remove the FriendDisplay m_friendsDisplay.removeFriend(aFriend.getId()); m_playersDisplay.updatePlayer(aFriend.getAvatar().getId()); break; case GameConstants.F_ENTER: case GameConstants.F_EXIT: // Friend came online, update the FriendDisplay // Friend went offline, update the FriendDisplay m_friendsDisplay.updateFriend(aFriend); break; } }
[ 71 ]
Avatar and Chat
All updates regarding friends are received via the onFriendUpdate. We first examine the type of update received by switching on the status parameter. A status value of F_ACCEPT indicates that another player accepted our friend request. At this point, we need to update the player display so as to remove the friend circle sprite. In addition, we need to add a new friend display. For a status value of F_REJECT, there is no screen update required; however, a subclass may inform the player of the fact. A status value of F_BREAK means an existing friendship was dissolved. In this case, we remove the corresponding friend display sprite and also update the player display sprite to again add the make friend circle button. PulseUI incorporates a visual hint as to whether a friend is online or offline; the server automatically sends notification whenever the status value is either F_ENTER when a friend enters the game or F_EXIT when a friend leaves the game. We simply find the corresponding player display and redraw it. Update is called with status F_ENTER and also in the case when a player enters the game for the first time, and for each friend the player has, the client will receive an F_ENTER, even though the friend may be offline. The sure way of finding out if the player is online is by calling the method, isFriendOnline, on the GameClient API. The following screenshot shows the visual hint. When the player is online, a shadow filter is added to the friend sprite. In the screenshot, we observe that Tony is friends with Wilson and Jessie, and Jessie is online.
[ 72 ]
Chapter 3
The chat feature
Chatting is one of the essentials of a multiplayer game, which we never see in a single player game. There are several types of chatting that can occur among players. •
Private chat °°
•
•
Team chat
Public chat °°
Lobby chat
°°
Room chat
System chat
Private chat or one-on-one chatting occurs in private between only two players. A public chat is when the player message is relayed to more than one player. Players in a lobby could broadcast a message to a large group of other players, and also in the lobby at the time. A room chat is where the players' messages are received by all the people within the game room that the player is in currently. Team chat is both public and private: public because it is broadcast to more than one player, but private because it is only sent to a select few. The team chat happens if the game supports some kind of team structure, where one team is battling another. For example, team chat enables private communication among the members to talk strategy. System chat is where a system administrator broadcasts a message to all players that are currently logged in. These are mainly used for letting players know of some important event, or perhaps to let players know that the server is coming down for maintenance.
The chat API
The following chat API allows the game to send chat messages either to a specific player or to all the players in the lobby or to every player in the room. public function sendChat(recipient:String, msg:String):void {
In order to send it to every player in the lobby, the player must be in the lobby. The recipient in this case must be set to null. To send a chat message to everyone in the game room, the player must be inside a game room and a null for recipient should be passed. To send a message to a specific player, the player's name must be passed in for the recipient parameter.
[ 73 ]
Avatar and Chat
The recipient will receive the chat message and is notified via the onReceivedChat callback. function onReceivedChat(msg:String, senderId:int,sender:String):void;
Chatting in Hello World
Here again we did not write any specific code to handle chat. For those who are curious, one may take a look at the Chat class to see what happens when a player types in a message and hits enter as well as the PulseGame class to see what happens when a chat message is received from the server.
Customizing chat display
The class that handles chat is called Chat. The Chat class is part of the Outline class. The Chat class includes both input handling, where it sends the player's chat message to the server for further broadcast, and receiving chat messages from the server and displaying it in the history textbox. To supply our own subclass of Chat, we may first subclass from the Chat class and then override the Outline class. The following shows the containment and subclassing for providing our own subclass of Chat:
We first create our subclasses of Outline (MyOutline) and Chat (MyChat). We then need to override the initOutline method in the subclass of PulseGame (MyGame) as follows: protected override function initOutline(avatar:GameAvatarClient):void { m_outline = new MyOutline(avatar); }
[ 74 ]
Chapter 3
This will make sure that when an instance of outline is being created, it creates our version of it. We override the createChat method in MyOutline as follows: protected override function createChat():Chat { return new MyChat(); }
We are now free to implement our own behavior for chat interface. The text input defined in the Chat class where the player types in the chat message is set to listen to the event when the user hits the Enter key. The implementation of the callback simply sends the message to the server as shown below: public function textEnter(e:Event):void { var gc:GameClient; gc = PulseGame.getInstance().getGameClient(); gc.sendChat(m_recipient, m_chat.text); m_chat.text = ""; }
For the receiving part, the chat message sent via the server is first handled by PulseGame since it is set up to the Pulse's GameClientCallback. The implementation simply calls the Chat instance's onChat method. The onChat simply creates a new string by appending the sender and the message and adds it to the chat history box as follows: public function onChat(chat:String, chatter:String):void { m_history.text = chatter + "> " + chat + "\n" + m_history.text; }
There is also a handy method in case we want to output the message to the chat box called onSystemMessage: public function onSystemMessage(message:String):void { m_history.text = message + "\n" + m_history.text ; }
This method is useful to output any error message from the server.
[ 75 ]
Avatar and Chat
Implementing high scores
Having high scores is a great motivator for those players who end up being fans of your game. Avatar objects usually posses the following properties pertaining to scoring and levels: • • •
Score Experience Level
Score is game round-specific. Whenever a player joins the game, the score is set to zero. The score is awarded at the end of each game round or when the game is considered finished. At the end of each game, a certain experience may be added based on the score. The number of experience points awarded is proportional to the score. Level is an overall number that the avatar displays to others in the community. The experience point gains could be linearly proportional to the score achieved within the game. But the level increase based on experience points is usually exponential, which means the avatar rises in levels faster when the avatar is in the lower levels. However, at higher levels progress to the next level is increasingly slower. When designing a multigame portal, the experience gained should be tracked for each game specifically, in addition to keeping track of the cumulative experience for the avatar. The top ten is a game-specific property. Every game must have a screen that shows the current standings of the players. The game should not only display the ranks of players within a game, but also how far or close is one player at a particular level from another level. Basically, the player must not only see the ranks, but also the experience points gained in the game so far. The Avatar and GameRank object relationship is shown as follows:
The above screenshot shows the only relevant properties related to this discussion. For every avatar, and for each game he or she has played, there must be a GameRank object. The GameRank object is displayed in the top ten screens within a specific game. [ 76 ]
Chapter 3
The following are the steps that must be tracked right after a game is considered complete: 1. Game finished. 2. The score is applied to the avatar experience. 3. The score is applied to the game rank. 4. The avatar level is updated. 5. The game rank of the player is re-computed.
High scores in Hello World
All scores earned by the player are automatically stored by the Pulse server and the top ten ranks are available and constantly updated on every client connected to the server.
Normally, there is not much customization to do in this screen except to change font or text color. In this case, we simply need to override the getFormat method. If we wanted to customize more than just font and color, we would need to override the showRanks method.
[ 77 ]
Avatar and Chat
Skinning the user interface
Simple skinning is implemented to go with the PulseUI framework. Skinning and PulseUI serves only to aid a new game to be developed at top speed, letting the developer focus only on the game play while the boring stuff such as chat, friendmaking, and lobby can be taken for granted. A different skin than the one provided with the SDK can be changed by simply replacing the three files under rsrc directory, namely: •
Ui.png
• •
Frame.png Outline.png
Note that the skin elements must follow the original size and positions. If, however, you need to have a different size or position, simply subclass the Skinner class from the PulseUI and tweak the Skinner itself.
Skinning for Hello World
This is where you customize the UI for your game. In this class, you only need to load the three skin files. The super class Skinner defined in the PulseUI will actually do all the cutting of individual UI elements. We gain access to the image files via the following definition: [Embed(source="/hw/rsrc/Outline.png")] private static var OutlineClass:Class; [Embed(source="/hw/rsrc/ui.png")] private static var UIClass:Class; [Embed(source="/hw/rsrc/frame.png")] private static var FrameClass:Class;
We override the load() method and actually create the bitmap data from the image files ready to be sliced and diced. Note that the properties m_outline, m_ui, and m_frame are protected properties defined in the super class Skinner. public function HelloSkinner() { super(); } protected override function load():void { m_outline = (new OutlineClass() as BitmapAsset).bitmapData; m_ui = (new UIClass() as BitmapAsset).bitmapData; [ 78 ]
Chapter 3 m_frame = (new FrameClass() as BitmapAsset).bitmapData; }
Summary
In this chapter, we learnt a very important concept—the Pulse modeler—which we will use to design the avatars (like we saw in this chapter) and also room and game objects that we will see in the subsequent chapters. We also saw relevant features regarding the avatar—chat, friends, and high scores. Although the HelloWorld sample did not write specific code to manage these features, we saw how PulseUI does it and explored how we can customize the default behavior.
[ 79 ]
Lobby and Room Management In this chapter, we will learn all about game lobby and game rooms, the management of which is essential for any multiplayer game. With Pulse SDK, we hardly need to write code for either on the server side, which is already taken care of by the server out of the box or on the client-side, which is taken care of by the PulseUI framework. We will see how we can directly use the PulseUI framework to jumpstart our multiplayer game as well as customize it to suit the needs for your game. Finally, once players either create a room or join an existing one, we will see how the game screen gets initialized and displayed for the game to start. More specifically, we will learn the following: •
States of a game room
•
Creating a new game room
•
Player management within game rooms
•
Initializing the game screen
Introduction to lobby and room management
A lobby, in a multiplayer game, is where people hang around before they go into a specific room to play. When the player comes out of the room, the players are dropped into the lobby again. The main function of a lobby is to help players quickly find a game room that is suited for them and join.
Lobby and Room Management
When the player is said to be in a lobby, the player will be able to browse rooms within the lobby that can be entered. The player may be able to see several attributes and the status of each game room. For example, the player will be able to see how many players are in the room already, giving a hint that the game in the room is about to begin. If it is a four-player game and three are already in, there is a greater chance that the game will start soon. Depending on the game, the room may also show a lot of other information such as avatar names of all the players already in the room and who the host is. In a car race game, the player may be able to see what kind of map is chosen to play, what level of difficulty the room is set to, etc. Most lobbies also offer a quick join functionality where the system chooses a room for the player and enters them into it.
The act of joining a room means the player leaves the lobby, which in turn means that the player is now unaware or not interested in any updates that happen in the lobby. The player now only receives events that occur within the game room, such as, another player has entered or departed or the host has left and a new host was chosen by the server.
[ 82 ]
Chapter 4
When a player is in the lobby, the player constantly receives updates that happen within the lobby. For example, events such as new room creation, deletion, and room-related updates. The room-related updates include the players joining or leaving the room and the room status changing from waiting to playing. A sophisticated lobby design lets a player delay switching to the room screen until the game starts. This is done so as to not have a player feel all alone once they create a room and get inside it. In this design, the player can still view activities in the lobby, and there's an opportunity for players to change their mind and jump to another table (game room) instantaneously. The lobby screen may also provide a chatting interface. The players will be able to view all the players in the lobby and even make friends. Note that the lobby for a popular game may include thousands of players. The server may be bogged down by sending updates to all the players in the lobby. As an advanced optimization, various pagination schemes may be adopted where the player only receives updates from only a certain set of rooms that is currently being viewed on the screen. In some cases, lobbies are organized into various categories to lessen the player traffic and thus the load on the server. Some of the ways you may want to break down the lobbies are based on player levels, game genres, and geographic location, etc. The lobbies are most often statically designed, meaning a player may not create a lobby on the fly. The server's responsibility is to keep track of all the players in the lobby and dispatch them with all events related to lobby and room activity. The rooms that are managed within a lobby may be created dynamically or sometimes statically. In a statically created room, the players simply occupy them, play the game, and then leave. Also in this design, the game shows with a bunch of empty rooms, say, one hundred of them. If all rooms are currently in play state, then the player needs to wait to join a room that is in a wait state and is open to accepting a new player into the room.
[ 83 ]
Lobby and Room Management
Modeling game room
In the last chapter, we saw how to create the game schema and model the avatar. The game room required for the game is also modeled via the schema file. Subclassing should be done when you want to define additional properties on a game room that you want to store within the game room. The properties that you might want to add would be specific to your game. However, some of the commonly required properties are already defined in the GameRoom class. You will only need to define one such subclass for a game. The following are the properties defined on the GameRoom Class: Property
Notes
Room name
Name of the game room typically set during game room creation
Host name
The server keeps track of this value and is set to the current host of the room. The room host is typically the creator. If the host leaves the room while others are still in it, an arbitrary player in the room is set as host.
Host ID
Is maintained by the server similar to host name.
Password
Should be set by the developer upon creating a new game room.
Player count
The server keeps track of this value as and when the players enter or leave the room.
Max player count
Should be set by the developer upon creating a new game room. The server will automatically reject the player joining the room if the player count is equal to the max player count
Room status
The possible values for this property are GameConstants. ROOM_STATE_WAITING or GameConstants.ROOM_STATE_ PLAYING The server keeps track of these value-based player actions such as PulseClient.startGame API.
Room type
The possible values for this property are value combinations of GameConstants.ROOM_TURN_BASED and GameConstants. ROOM_DISALLOW_POST_START The developer should set this value upon creating a new room. The server controls the callback API behavior based on this property.
Action
A server-reserved property; the developer should not use this for any purpose.
The developer may inherit from the game room and specify an arbitrary number of properties. Note that the total number of bytes should not exceed 1K bytes. [ 84 ]
Chapter 4
Game room management
A room is where a group of players play a particular game. A player that joins the room first or enters first is called game or room host. The host has some special powers. For example, the host can set the difficulty level of the game, set the race track to play, limit the number of people that can join the game, and even set a password on the room. If there is more than one player in the room and the host decides to leave the room, then usually the system automatically chooses another player in the room as a host and this event is notified to all players in room. Once the player is said to be in the room, the player starts receiving events for any player entering or leaving the room, or any room-property changes such as the host setting a different map to play, etc. The players can also chat with each other when they are in a game room.
Seating order
Seating order is not required for all kinds of games, for example, a racing game may not place as much importance on the starting position of the player's cars, although the system may assign one automatically. This is also true in the case of two-player games such as chess. But players in a card game of four players around a table may wish to be seated at a specific position, for example, across from a friend who would be a partner during the game play. In these cases, a player entering a room also requests to sit at a certain position. In this kind of a lobby or room design, the GUI shows which seats are currently occupied and which seats are not. The server may reject the player request if another player is already seated at the requested seat. This happens when the server has already granted the position to another player just an instant before the player requested and the UI was probably not updated. In this case, the server will choose another vacant seat if one is available or else the server will reject the player entrance into the room.
Room states
The room may be said to be in one of two states: •
Waiting
•
Play
[ 85 ]
Lobby and Room Management
In case of a dynamically created room, the room is said to be in a wait state upon creation. This state signifies that the game has not begun yet. In the case of a statically created room, all rooms start out in this state. This is also the time when other players may join the room. During the wait state, the host may still be allowed to change the room attributes, such as the difficulty level, or depending on the game developers' preference, the room properties are all set when the player is in the process of creating the room. The room goes into a play state when the host broadcasts a start message to all the players.
As you can see from the illustration, the room is created in wait state, and goes into play state when the game is started. The room object will be purged in case of a dynamically created room. The room is usually purged when all the players in the room leave. The host may decide to start the game at any time, except for games that must have a minimum number of players. For example, a card game may require at least four players to start or a game of tic-tac-toe may need at least two players to start. In the case of racing game, the host may wait for as many players as he or she wishes. The server will prevent more than the maximum number of players from joining.
Player states
As with rooms, the players within the game room may also be in one of the following two states: • •
Ready Not ready
[ 86 ]
Chapter 4
Once the player has joined the room, the player usually starts out in the Not Ready state and each player may have to set the state to ready. The game may not start unless every player in the game room has set their state to ready. The player may change their state before the game starts. Some games force the player to be ready and are allowed to change the state. It is usually in good intention of the game developer to give players a chance to ready themselves before the game starts. But most often, it does happen that a player forgets to click the button to set themselves as ready, or even may simply leave the computer without closing the game leaving the player hanging in the game room. The flip side is also the reason why some games expose this feature, meaning you only want to play with people that are active at the computer playing the game. When a game round ends, the server sets the player's status back to "not ready" and the player must actively choose to go into the ready state. Failing to do so, the host or other players assume that the player has left the computer physically leaving their avatar hanging in the game room. In this case, we need to kick these players out of the game room.
Kicking out a player
There are various reasons that you want to kick a player out. Sometimes it is not pleasant and sometimes it simple is a necessity. The player may be kicked out when the room is in the wait state, that is when the game has not started yet. You might want to kick someone out of the game room because the player is using foul or offensive language during chat. Or it could be that a player for some reason has not set the status to ready. In such events, the host has the power to initiate a kick-out process. Another reason might be that the host wants the room to have challenging players so as to make each game round as interesting as possible. There could be several ways or policies to kick someone out: •
Anarchy
•
Imperial
•
Diplomatic
Anarchy being not much of a policy where anyone may kick anyone else, this method usually becomes destructive and is not recommended to implement. In the Imperial policy, only the host has the right to kick someone out; this policy is also quite easy to implement. Diplomatic, the third policy, is quite common in a lot of multiplayer games. The host may get a consensus from all the players, except from the player that is being kicked out. It happens in three steps: 1. Host initiates a kick-out—may state a reason. 2. Players receive the notification—may agree or disagree. 3. The player is kicked out if all players agree. [ 87 ]
Lobby and Room Management
If everyone agrees, then the server forces the kicked-out player out of the room and into the lobby. The host may also optionally give a reason as to why somebody is being kicked out. The other players would usually have a dialog window that comes up with a countdown timer. The player has a choice to say yes or no to kick the player out, and if the player does not respond, then the default response is a yes to the kick out. It is very rare that an implementation allows even for a host to be kicked out.
Room types
Room types define the behavior of how the game is played within the room and additional policies for players entering the room. The following are a few attributes that define a room type and are not mutually exclusive: •
Password protected
•
Turn-based °°
•
Clockwise
Open
Password protected is a very common implementation where the host creating the room may set a simple password on the room. Only players knowing the password can enter into it. The password is then given out to friends via private chat or even an external instant messenger. Turn-based, is where, in a game each player takes a turn to make their move. When the game starts, the host or a player at random is chosen to play first. It could be determined by last round's winner or loser. The turn then goes round the table in the seating order, either in clockwise or counterclockwise direction. The turn management is typically controlled by the server; it notifies the corresponding client when it's their turn. The turn is usually controlled by a countdown clock, when it goes to zero, either a default move must be implemented by the game developer on behalf of the player, or if the game rules allow, the player's turn could also be considered a pass. Some games may also allow any random player to call in when it is not their turn. This means the player may request the server to skip all other players, making it the players turn. If two player's call for a turn simultaneously, the server arbitrarily chooses one. An open room is rare but typically allows players to join the game room even after the game has started. For example, in a game like multiplayer jigsaw, you could allow new players to join no matter how much the game has progressed. Of course, a maximum number of players would still be enforced.
[ 88 ]
Chapter 4
Audience
Audience is a non-playing avatar inside a game room. An audience can join in the room at any time even when the game has already begun. Depending on the game, the audience may simply observe or may encourage a certain player who is playing the game. This feature is a great opportunity for novice players to learn from expert players in a game of chess or Go, for example. The audience game screen may look identical to that of the players actually playing the game. For example, in a game of chess, an audience would view pretty much the same screen as that of a player. But in a racing game, the screen may look completely different. For example, the actual players may see the screen as though they were driving the car, whereas the audience may see the game as a top-down view, showing the current positions of all players on the track.
Room properties
In addition to room states and type, there are numerous other things that are needed to keep track of for each instance of a room: •
Maximum players
•
Current player count
•
Member player names and summary
•
Game host name
The above list is a generic list of properties that players browsing the rooms would want to know before joining the room. But there's a lot of game-related information that a game developer may expose to the players, giving them the choice to and thereby maximizing the fun while playing the game.
The lobby screen implementation
In this section, we will learn how to implement the room display within the lobby.
Lobby screen in Hello World
Upon login, the first thing the player needs to do is enter the lobby. Once the player has logged into the server successfully, the default behavior of the PulseGame in PulseUI is to call enterLobby API.
[ 89 ]
Lobby and Room Management
The following is the implementation within PulseGame: protected function postInit():void { m_netClient.enterLobby(); }
Once the player has successfully entered the lobby, the client will start listening to all the room updates that happen in the lobby. These updates include any newly created room, any updates to the room objects, for example, any changes to the player count of a game room, host change, etc.
Customizing lobby screen
In the PulseUI, the lobby screen is the immediate screen that gets displayed after a successful login. The lobby screen is drawn over whatever the outline object has drawn onto the screen. The following is added to the screen when the lobby screen is shown to the player: •
Search lobby UI
•
Available game rooms
•
Game room scroll buttons
•
Buttons for creating a new game room
•
Navigation buttons to top ten and register screens
When the lobby is called to hide, the lobby UI elements are taken off the screen to make way for the incoming screen. For our initial game prototype, we don't need to make any changes. The PulseUI framework already offers all of the essential set of functionalities of a lobby for any kind of multiplayer game. However, the one place you may want to add more details is in what gets display for each room within the lobby.
Customizing game room display
The room display is controlled by the class RoomsDisplay, an instance of which is contained in GameLobbyScreen. The RoomsDisplay contains a number of RoomDisplay object instances, one for each room being displayed. In order to modify what gets displayed in each room display, we do it inside of the class that is subclassed from RoomDisplay. The following figure shows the containment of the Pulse layer classes and shows what we need to subclass in order to modify the room display: [ 90 ]
Chapter 4
In all cases, we would subclass (MyGame) the PulseGame. In order to have our own subclass of lobby screen, we first need to create class (MyGameLobbyScreen) inherited from GameLobbyScreen. In addition, we also need to override the method initLobbyScreen as shown below: protected override function initLobbyScreen():void { m_gameLobbyScreen = new MyGameLobbyScreen(); }
In order to provide our own RoomsDisplay, we need to create a subclass (MyRoomsDisplay) inherited from RoomsDisplay class and we need to override the method where it creates the RoomsDisplay in GameLobbyScreen as shown below: protected function createRoomsDisplay():void { m_roomsDisplay = new MyRoomsDisplay(); }
Finally, we do similar subclassing for MyRoomDisplay and override the method that creates the RoomDisplay in MyRoomsDisplay as follows: protected override function createRoomDisplay (room:GameRoomClient):RoomDisplay { return new MyRoomDisplay(room); }
Now that we have hooked up to create our own implementation of RoomDisplay, we are free to add any additional information we like. In order to add additional sprites, we now simply need to override the init method of GameRoom and provide our additional sprites.
Filtering rooms to display
The choice is up to the game developer to either display all the rooms currently created or just the ones that are available to join. We may override the method shouldShowRoom method in the subclass of RoomsDisplay (MyRoomsDisplay) to change the default behavior. The default behavior is to show rooms that are only available to join as well as rooms that allow players to join even after the game has started. [ 91 ]
Lobby and Room Management
Following is the default method implementation: protected function shouldShowRoom(room:GameRoomClient):Boolean { var show:Boolean; show = (room.getRoomType() == GameConstants.ROOM_ALLOW_POST_START); if(show == true) return true; else { return (room.getRoomStatus() == GameConstants.ROOM_STATE_WAITING); } }
Lobby and room-related API
Upon successful logging, all game implementation must call the enterLobby method. public function enterLobby(gameLobbyId:String = "DefaultLobby"):void
You may pass a null string in case you only wish to have one default lobby. The following notification will be received again by the client whether the request to enter a lobby was successful or not. At this point, the game screen should switch to the lobby screen. function onEnteredLobby(error:int):void
If entering a lobby was successful, then the client will start to receive a bunch of onNewGameRoom notifications, one for each room that was found active in the entered lobby. The implementation should draw the corresponding game room with the details on the lobby screen. function onNewGameRoom(room:GameRoomClient):void
The client may also receive other lobby-related notifications such as onUpdateGameRoom for any room updates and onRemoveGameRoom for any room objects that no longer exist in lobby. function onUpdateGameRoom(room:GameRoomClient):void function onRemoveGameRoom(room:GameRoomClient):void
If the player wishes to join an existing game room in the lobby, you simply call joinGameRoom and pass the corresponding room object. public function joinGameRoom(gameRoom:GameRoomClient):void
[ 92 ]
Chapter 4
In response to a join request, the server notifies the requesting client of whether the action was successful or failed via the game client callback method. function onJoinedGameRoom(gameRoomId:int, error:int):void
A player already in a game room may leave the room and go back to the lobby, by calling the following API: public function leaveGameRoom():void
Note that if the player successfully left the room, the calling game client will receive the notification via the following callback API: function onLeaveGameRoom(error:int):void
New game screen implementation
NewGameScreen is where a new game room is created when the player clicks on
the new game room button in the lobby screen.
New game screen in Hello World
The lobby screen gets replaced by the NewGameScreen or that of the subclass we have set up. In all cases, you must subclass the NewGameScreen and override at least the createNewGameRoom method. This method is called when the player finally clicks on OK.
The following is the overridden method from the Hello World sample: protected override function createNewGameRoom():void { // Create the new Game room object [ 93 ]
Lobby and Room Management var room:GameRoomClient = new GameRoomClient(); // Set the name of the room from what is entered // in the text field room.setRoomName(m_ti.text); // Limit maximum players that can // enter the room to three room.setMaxPlayerCount(3); // Let players join the game // even after it is started room.setRoomType(GameConstants.ROOM_ALLOW_POST_START); // set the player to be in ready // state as soon as they enter the room room.setAutoReady(1); // Send a request to the server // to create the game room PulseGame.getInstance().getGameClient(). createGameRoom(room); }
All samples provided with the Pulse package simply take in a name for the room. But for a real game, you may want to provide more options, such as password, max players, and others, for a player to control.
Customizing the new game screen
In order to provide our own implementation for the new game screen, we need to override the initNewGameScreen defined in the PulseGame.
The subclass of game screen must then override init where we create all the sprites needed. The show and hide methods are overridden to display them onto the screen and then remove them respectively. protected override function initNewGameScreen():void {
[ 94 ]
Chapter 4 m_newGameScreen = new MyNewGameScreen(); m_newGameScreen.init(); }
Next, we subclass NewGameScreen defined in PulseUI called NewGameRoom screen and override just one method called createNewGameRoom. In the method, we specify what kind of room needs to be created. For this sample, we will keep it real simple: protected override function createNewGameRoom():void { var room:GameRoomClient = new GameRoomClient(); room.setRoomName(m_ti.text); room.setMaxPlayerCount(3); room.setRoomType(GameConstants.ROOM_ALLOW_POST_START); room.setAutoReady(1); PulseGame.getInstance().getGameClient(). createGameRoom(room); }
First, we create a game room object of class GameRoomClient, which is defined in the Pulse package. We then set the properties on the object. We give it a name as entered by the player and limit the number of players joining the room to three. We also set the room type to say that players may join the room for which the game has already started. We set the auto-ready to 1 (true) meaning the player status will be set to ready as soon as they join the game, and this will allow the host to start the game anytime. Note that, in this sample, we don't provide any UI to set the status of the player to ready or wait. Finally, we call the Pulse API createGameRoom(…) and pass the instance of the room. Once the server creates the room successfully, the new room will appear for every player that is currently in the lobby.
New game room API
To create a new game room as we saw in the HelloWorld example, you simply need to create the room object, set the necessary properties, and call createGameRoom API. public function createGameRoom(gameRoom:GameRoomClient):void
The game client will receive the following notification to let the game client know if the request was successfully created or had any errors. function onCreateGameRoom(gameRoomId:int, error:int):void;
Note that players in the lobby will be notified of the newly created room so that their respective UI can be updated. [ 95 ]
Lobby and Room Management
Designing the game screen
The heart of any game—this is where all the action takes place. Because this is where the game will be implemented, most code is written here to implement the game. The standard init, show, and hide methods must be overridden and implemented to initialize, add, and remove the needed sprites respectively. In addition to the three methods, let us examine a few other methods that get called when we need to implement. If the game is designed for players to join after the game has already started, the game code should not wait for the startGame method to be called. Instead, the show method must check if the game has already started and update the game screen with all the game states that it receives.
The method startGame gets called when the host starts the game; this method gets called in all the clients that are in the room. In this method, we begin the race in case of a racing game or we start distributing the cards in the case of a card game, and so on. The show method must additionally check if the player is the game host. If so, then the game screen should show the start sprite and for all other players (non-hosts) the player should show wait sprite. Finally, in case the game has not started and the host quits the room for some reason, the method onHostAlert would be called. In this case, the player that becomes the host must have the wait sprite replaced with the start sprite.
[ 96 ]
Chapter 4
Implementing the game screen
The HelloGameScreen is subclassed from GameScreen defined in PulseUI package. We will add two buttons for adding and removing game states from the game instance. Like other subclassing where we will need to override the specific methods, the PulseUI framework will take care of calling them at the right time. For example, the init method is overridden to initialize any sprites that you may need during the game, the show method must be overridden to actually put the sprites onto the screen, and finally, the hide method should be overridden to remove them from the screen. Note that the init method is called once, but the show and hide may be called several times, that is every time when the player enters or leaves the game room. Here is a partial listing of the class that shows the property definitions, and the overriding of the init, show, and hide methods: public class HelloGameScreen extends GameScreen { private var m_addBtn:Button; private var m_removeBtn:Button; private var m_gs:Map = new Map(); // keep track of selected. private var m_selected:GameStateSprite; public function HelloGameScreen() { super(); } public override function init():void { super.init(); initBtns(); } public override function show():void { super.show(); var gc:GameClient; gc = PulseGame.getInstance().getGameClient(); // If the game has already started, add the buttons if ( gc.getMyGameRoom().getRoomStatus() == GameConstants.ROOM_STATE_PLAYING ) { addChild(m_addBtn); addChild(m_removeBtn); } } public override function hide():void { // Remove all the sprites and cleanup var states:Array = m_gs.values(); for ( var i:int=0; i<states.length; i++ ) { [ 97 ]
Lobby and Room Management removeChild(states[i]); } m_gs.reset(); if ( m_addBtn.parent != null ) removeChild(m_addBtn); if ( m_removeBtn.parent != null ) removeChild(m_removeBtn); super.hide(); }
Notice that along with the needed sprite for the buttons, we also keep track of the game state sprites in a hash map (m_gs), as well as keeping track of the colored circle that was last clicked on (m_selected). Another important method to override is the start method, which is called when the host starts the game. Since in this sample we allow the players to enter even after the game has started, the show method does a check if the room status is already in playing state. If so, then the add and remove buttons are drawn. public override function startGame():void { super.startGame(); // Game started, add the buttons addChild(m_addBtn); addChild(m_removeBtn); }
As we saw in the implementation of the main class, there were three methods that would be called when we received a new game state, when a game state was removed, and when it was modified. The implementation of this is simply delegated to this game screen. Now let us examine the code to see what happens in each of the three cases. When a new game state is added, we create a new game state sprite and add it to the screen. After creating the game state sprite, we add it to the map so that we can access it later during removal or update. We also hook up the mouse event listeners so that we can implement the drag and selection capabilities. Finally, we set the x and y coordinates for the colored circle from the values found in the game state object and add it to the screen. private function newGameState(gameState:HelloGameStateClient):void { // Create and show a corresponding // sprite for the game state var s:Sprite = new GameStateSprite(gameState); m_gs.put(gameState.getId(), s); s.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler); s.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler); s.x = gameState.getX(); s.y = gameState.getY(); [ 98 ]
Chapter 4 addChild(s); }
When we receive a notification from the server that a game state was removed, we simply remove the corresponding game state sprite from the screen. public function onRemoveGS(gameState:GameStateClient):void { // Received from server to remove a game state var s:GameStateSprite; s = m_gs.getValue(gameState.getId()); if ( s != null ) { removeChild(s); // remove from screen m_gs.remove(gameState.getId()); // remove from cache } }
We also make sure to remove it from the hash map. When we receive a notification that a game state was updated, find the corresponding game state sprite in the hash map and update the x and y from the received updated game state: public function onUpdateGS(gameState:GameStateClient):void { // An update game state was received from server var gs:HelloGameStateClient; gs = gameState as HelloGameStateClient; var s:GameStateSprite = m_gs.getValue(gs.getId()); if ( s == null ) { // This can happen when the player // joins in the middle of the game newGameState(gs); } else { s.x = gs.getX(); s.y = gs.getY(); } }
Now that we have handled the notification from the servers when a game state was added, removed, or modified, how do we generate these game states in response to player actions? Let us examine them now one by one. The player may add a new game state by clicking on the Add button. The callback method that gets called when the player clicks on the Add button is shown below: private function addGS(e:Event):void { // Add button was clicked! var newGS:HelloGameStateClient; newGS = new HelloGameStateClient(); [ 99 ]
Lobby and Room Management newGS.setX(300); newGS.setY(300); PulseGame.getInstance(). getGameClient().addGameState(newGS); }
When a player wishes to add a new game state, we simply create a new game state object, initialize the properties, and call the Pulse API to add the game state. Upon the server receiving the message, it will be broadcasted to all the players in the room, including the one who sent it in the first place. This will result in the calling of the method newGameState in all the players' rooms. So in effect, the colored circle is not drawn until a notification is received from the server, including for the player that created it. The code below shows how the add and remove buttons are created and initialized. This method is called from the init method, which we saw earlier: private function initBtns():void { m_addBtn = new Button(); m_addBtn.x = 200; m_addBtn.y = 150; m_addBtn.height = 20; m_addBtn.width = 40; m_addBtn.label = "Add"; m_addBtn.addEventListener(MouseEvent.CLICK, addGS); m_removeBtn = new Button(); m_removeBtn.x = 250; m_removeBtn.y = 150; m_removeBtn.height = 20; m_removeBtn.width = 80; m_removeBtn.label = "Remove"; m_removeBtn.addEventListener(MouseEvent.CLICK, removeGS); }
Similar to the add mechanics, the removal of a game state is triggered when the player clicks on the remove button. private function removeGS(e:Event):void { if ( m_selected != null ) { PulseGame.getInstance(). getGameClient(). removeGameState(m_selected.m_gs); } }
During the callback that is fired when the remove button is clicked, we check if there is any selection of a colored circle. If so, we call the pulse API to remove the game state. The server, on receiving the request, will broadcast to all the players in the game room including the one sending the request. When the request is received by the clients, the onRemoveGS method is called and the game state sprite gets removed from the screen as shown above. [ 100 ]
Chapter 4
In order to demonstrate the game state update, the Hello World sample allows the players to drag the colored circle around within the game screen. For this, we need to implement the mouse event handlers. The handlers also keep track of the currently selected sprite. Here is the code for the mouse event handlers. The mouse down handler will first figure out the sprite on which the click was made and initiate the sprite drag. private function mouseDownHandler(event:MouseEvent):void { //trace("mouseDownHandler"); var sprite:Sprite; sprite = Sprite(event.target); sprite.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler); sprite.startDrag(); }
When the mouse button is released we stop the mouse drag process. We also record the final position of the sprite. We then get the corresponding game state associated with the colored circle and send in the request to the server to modify the game state. We also update the m_selected property.
//
private function mouseUpHandler(event:MouseEvent):void { //trace("mouseUpHandler"); var sprite:GameStateSprite; sprite = Sprite(event.target) as GameStateSprite; trace("x: " + sprite.x + " y: " + sprite.y); sprite.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler); sprite.stopDrag(); var gs:HelloGameStateClient; gs = sprite.m_gs; if ( gs != null ) { gs.setX(sprite.x); gs.setY(sprite.y); PulseGame.getInstance(). getGameClient().updateGameState(gs); } setSelected(sprite); }
This concludes the walk-through of the Hello World sample. The readers are encouraged to study the sample from the downloaded package and give it a test drive.
[ 101 ]
Lobby and Room Management
Customizing the game screen
The game screen—where all the action happens—also inherits from Screen class. We must inherit this class to provide the gameplay.
To supply our own implementation of MyGameScreen, we need to override the initGameScreen method defined in PulseGame. So the overridden method in MyGame looks like the following: protected override function initNewGameScreen():void { m_newGameScreen = new NewJigsawGameScreen(); m_newGameScreen.init(); }
Summary
In this chapter, we dealt with the implementation and user interface customizations as related to a game lobby and a room within the lobby. We saw how we can leverage the PulseUI framework to quickly have the functionality available for our new game. We also saw how (if needed) to customize the lobby screen and the rooms within it. We saw how the new game rooms are created and how to customize the user interface for the same. Finally, we saw how the players enter into the game screen and perform essential state-specific checks related to the game host if the game has already started. In the next chapter, we will delve deep into the specifics of implementing the game itself.
[ 102 ]
Game Logic In this chapter, we will learn all about exchanging game information among players during game play. We will walk through the code related to game state exchange and manipulation in the Hello World sample. We will also go over the Pulse Game state-related APIs. We will learn the following in specific detail: •
Types of game states
•
Sending and receiving game states
•
Synchronization of game states
Gameplay implementation
Gameplay is implemented by sharing user actions within the same environment—the game. It gets interesting for game developers and designers because of the resulting interaction due to multiple players affecting one another. The player, upon interacting with the scene, sets a chain of events affecting other players. In order to implement this type of interaction in an identical way across all player's screen, we often need a central place to synchronize the events and to make sure that all players are kept in sync. The server also has the responsibility to be fair to all players, and finally offer security in the case of a player attempting to cheat with a modified or hacked client. However, simple or casual games may be implemented without customizing the server for each particular game. The server, however, must provide some basic model or a set of constructs with which the clients may interact to provide a good gaming experience to the players.
Game Logic
Each player within the game must perceive pretty much the same state of the game. The enemy of multiplayer gaming is Lag. It is something that can destroy a player's experience. The way to approach lag is simply to not add more lag in the software layers, such as buffering messages for no apparent reason, avoiding under utilization of CPU with badly written multithreaded implementations, etc. A game server's responsibility is not only to exchange data but also for validation and synchronization. Validation is mostly against modified or hacked clients that, for example, may claim to never decrease life, carry infinite ammunition, or walk through walls, etc. The server spends a considerable amount of CPU making sure all players have a fair game. Synchronization is where the server avoids two players from picking up one weapon that's on the ground. If two players claim something, the server is the ultimate arbitrator in deciding who wins. The server simply processes the message in a firstcome-first-serve basis. Another example is the server in a racing game having the final say on who crossed the finishing line first. If you have played enough online racing games, you will sometimes notice that on your screen you seemed to have won the race by a hair, but the server reports back otherwise. This is due to the fact that the client continues to draw your car's position while the exact position of your opponent has not arrived and thus not displayed.
Modeling game states
Depending on your game, you may need to define more than one game state. Each subclass of the game must have the parent set to GameState. The following are the property descriptions defined in the GameState class: Developer Usable Property
Notes
Action
A server-reserved property; the developer should not use this for any purpose.
State type
The developer should set the value of this property upon creating a game state object. Possible values for this property are: GameConstants.GS_IS_INITIAL GameConstants.GS_IS_MUTABLE GameConstants.GS_IS_ TRANSIENT GameConstnats.GS_IS_UNIQUE
[ 104 ]
Chapter 5
Developer Usable Property
Notes
State key
In case the game state is unique, the game state object is required to set this property.
Error ID
A server-reserved property; the developer should not use this for any purpose.
Sender ID
The Pulse framework will worry about setting this property. The developer may read this value for its use.
The developer may inherit as many custom classes from game state and specify arbitrary number of properties. Note that the total number of bytes for each class should not exceed 1K bytes.
Game states types
Game state refers to a piece of data that is shared among all the players currently playing the same instance of the game. One game state represents a small chunk of information of the entire state of the game. The entire collection of game states represents the full state of the game at the current time and it is not a compete history of the game, although some game states may live longer than others. Generally, there are four kinds of game state data: •
Initial game states: These are shared before the game starts and may remain unchanged through several rounds of the game within a game room. An example of an initial state could be the picture that is played in the current round of a multiplayer jigsaw game or the race track that has been selected in a car race game.
•
Normal game states: They have no special purpose, except that they represent a part of the entire game states similar to other kinds. These game states usually form the bulk of the game states. The normal game states live from the time when a player adds them to the time the game is said to end. For example, these may be the current position of the pawns in a game of chess.
[ 105 ]
Game Logic
•
Unique game states: These are one of a kind in the game, and in order to ensure uniqueness, these game states have an associated key. The lives of unique game states are similar to that of normal game states. An example where this is useful is when two or more players claim on something in the game and where only one player will be allowed to have it. A player claiming to have won the race or a player claiming to have picked an item on the ground are some of the examples where we would use unique game state to implement.
•
Actions game state or actions: These are different from others in terms of how long they live. Unlike all the other game states, an action game state is not persisted, but the server quickly passes the game state to the rest of the players. Actions are useful to implement when a player fires a bullet. When a player sends this kind of action, it is received by all the players and could animate the action in their own screens. This is also great for implementing emoticons such as waving or saying hello to another player in a 2D/3D virtual world kind of game.
If an audience or a player joins a game that already has started, all the initial, unique, and normal game states are pushed to the player entering the room so that the player can get up to speed with the current state of the game. After this the newly arrived client/player will send and receive game states like the rest of the players who were already in the game. The newly entered player does not receive any game state actions that were exchanged before the player entered the game.
As we learned in Chapter 4, a room can exist in two states, waiting or playing. From the previous figure, we observe that when the room is in the waiting state, only the game host is allowed to add game states and these game states must be marked as initial. Any other game state types that are attempted to be added will be rejected by the server.
[ 106 ]
Chapter 5
Once the game room host starts the game and is in the playing state, any player in the game room may add, remove, update, or send game states. The added game states are cached on the server and clients. Once the game is finished, all the added (non-initial) game states will be cleared from cache.
Game states in Hello World
Game states in the Hello World sample demonstrate only the use of normal game states. The following screenshot is of the Hello World sample's game screen:
A new game state is added to the game by clicking on the Add button. When the game state is successfully added, a visual representation of the game state is shown as a circle on the screen. The circles may be moved around by dragging them. When the circle is let go, an update is called on the corresponding game state. The update is reflected on every client within the game room. To remove a game state, we may first select one of the circles and then click on the Remove button. If the remove was successful, the corresponding circle will be removed from the game and will disappear from all the other clients in the game room as well.
Code walk-through
Let us now explore the actual code within the Hello World sample that deals with adding, updating, and removing game states. In order to visualize the game states in a game client, we draw circles to represent the game state. For this purpose, we make use of a simple sprite-based class. [ 107 ]
Game Logic
GameStateSprite class
This is a simple helper class to aid us in rendering a game state on the game screen. This is the class that takes the form of a colored circle. The constructor takes a HelloGameStateClient and initializes the color based on the ID of the game state. Instances of these are created by the game screen class that we will visit next. A new instance is created whenever we receive a message from the server that a new game state has been added to the game room. package hw.ui { import flash.display.Sprite; import hw.gsrc.client.HelloGameStateClient; public class GameStateSprite extends Sprite { public var m_gs:HelloGameStateClient; public function GameStateSprite(gs:HelloGameStateClient) { super(); m_gs = gs; var color:uint; if ( gs.getId()%2 ) color = 0xFF0000; else if ( gs.getId()%3 ) color = 0x00FF00; else color = 0x000FF; graphics.beginFill(color, 0.5); graphics.drawCircle(0, 0, 25); graphics.endFill(); } } }
General flow of events
When a client adds, updates, removes, or sends game states to the server, all the clients will receive the state update, including the client that sent it. We should defer taking any action on the sending client in case the server rejects the request. In case the server validation fails, the sending client will receive a corresponding notification. For validation rules regarding the game state types and room states, refer to the previous section. [ 108 ]
Chapter 5
The previous figure shows a successful game state request that is broadcast to all the players in the room, including the sender. In the following illustration where a validation error occurred, the server will send back the error to the sender and the rest of the game clients will not receive the notifications regarding the failed request.
Not all games are implemented as shown above. For a fast twitchy game, we simply send the game state to the server hoping that it will pass validation and immediately update the client screen. When the server does send back an error, the sending client will then make a correction. As we will see in Chapter 9, during the implementation of a racing game, the client will send the game state action and update its own spaceship position without waiting for the server's confirmation.
Game state schema
The following is the snippet of schema file for the Hello World sample. It is a very simple game state containing three properties: <property index="0" name="x" count="1" type="int"/> <property index="1" name="y" count="1" type="int"/> <property index="2" name="color" count="1" type="int"/>
The name of the game state class is HelloGameState, which means a HelloGameStateClient class will be available for us after the modeling phase. [ 109 ]
Game Logic
It defines three properties. The x and y coordinates determine the position of the circle on the screen. The color property will determine the color of the game state sprite.
Adding a new game state
In Hello World, we add a game state by clicking on the Add button. For this, we need to handle the mouse click as follows: private function addGS(e:Event):void { // Add button was clicked! var newGS:HelloGameStateClient; newGS = new HelloGameStateClient(); newGS.setX(300); newGS.setY(300); PulseGame.getInstance(). getGameClient().addGameState(newGS); }
We simply create an instance of the game state, set the properties, and call the addGameState API. Note that we don't add the circle to the screen yet. We wait for the server's response. When the clients receive the callback for adding game state, we update the screen with a circle as follows: public function onAddGS(newGS:GameStateClient):void { // Received from server var gs:HelloGameStateClient; gs = newGS as HelloGameStateClient; newGameState(gs); } private function newGameState(gameState:HelloGameStateClient):void { // Create and show a corresponding // sprite for the game state var s:Sprite = new GameStateSprite(gameState); m_gs.put(gameState.getId(), s); s.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler); s.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler); s.x = gameState.getX(); s.y = gameState.getY(); addChild(s); }
[ 110 ]
Chapter 5
We create an instance of a GameStateSprite class with the properties of the received game state and add it to the screen. We also set up the mouse listeners in order to implement dragging. It is important to know that the ID property of a game state should be zero during addition. After the server validates the new game state, it will assign a unique ID to the game state and return it in the add notification. After creating a new sprite for the game state, we will add the sprite to a hash table for easy access, and for later processing during update and remove. Note that the client receives the update from the server; the PulseGame is set up to be the callback handler. The HelloGame—the sub-class of PulseGame—overrides the onNewGameState, and the implementation of this will call the HelloGameScreen's onAddGS method. This flow is the same for all game state notifications received from the server. The following is the simple delegation in HelloGame class: public override function onNewGameState(gameState:GameStateClient):void { (m_gameScreen as HelloGameScreen).onAddGS(gameState); }
Updating game state
The mouse handlers are listed below. During the mouse up event, we retrieve the game state that the circle sprite represents and call the updateGameState API. private function mouseDownHandler(event:MouseEvent):void { //trace("mouseDownHandler"); var sprite:Sprite; sprite = Sprite(event.target); sprite.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler); sprite.startDrag(); } private function mouseUpHandler(event:MouseEvent):void { //trace("mouseUpHandler"); // get the GameStateSprite which was dragged. var sprite:GameStateSprite; sprite = Sprite(event.target) as GameStateSprite; //trace("x: " + sprite.x + " y: " + sprite.y); sprite.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler); sprite.stopDrag(); [ 111 ]
Game Logic // Get the game state that is represented by this sprite var gs:HelloGameStateClient; gs = sprite.m_gs; if ( gs != null ) { gs.setX(sprite.x); gs.setY(sprite.y); PulseGame.getInstance(). getGameClient().updateGameState(gs); } setSelected(sprite); }
As in the case of handling the adding of game state, we will update the game state sprite after we receive the update notification from the server. So in the HelloGame, we override the callback method as follows: public override function onUpdateGameState(gameState:GameStateClient):void { (m_gameScreen as HelloGameScreen).onUpdateGS(gameState); }
The method onUpdateGS is listed below: public function onUpdateGS(gameState:GameStateClient):void { // An update game state was received from server var gs:HelloGameStateClient; gs = gameState as HelloGameStateClient; var s:GameStateSprite = m_gs.getValue(gs.getId()); if ( s == null ) { // This can happen when the player // joins in the middle of the game newGameState(gs); } else { s.x = gs.getX(); s.y = gs.getY(); } }
During an update, we get the sprite that the game state is represented by and update the position based on the received game state. We first get the sprite based on the ID value of the received game state. Note that we need to handle a special case. If the received game state did not have a corresponding sprite, we would need to create it. This should not happen normally, but could happen for a client that has entered the room after a few game states were already added by another client in the room. [ 112 ]
Chapter 5
Removing a game state
For removing an existing game state, we need to handle the Remove button. The handler is shown below: private function removeGS(e:Event):void { // Remove button was clicked if ( m_selected != null ) { PulseGame.getInstance(). getGameClient(). removeGameState(m_selected.m_gs); } }
Whenever a mouse up is detected on the game state sprite, we keep a track of the current selected sprite. The following is the setSelected method that keeps track of the current selection. It also takes care of setting the filter on the sprite so that we can see the selected sprite visually. When a new selection is made, we also need to remove the filters from the previously selected filter. private function setSelected(sel:GameStateSprite):void { var filters:Array; if ( m_selected != null ) { // existing selection filters = m_selected.filters; filters.splice(0, 1); m_selected.filters = filters; } m_selected = sel; if ( m_selected != null ) { // add the filter filters = m_selected.filters; filters.push(FilterFactory. createFilter(FilterFactory.DROP_SHADOW_FILTER)); m_selected.filters = filters; } }
[ 113 ]
Game Logic
Game state API
Once a player is in the game room, the player may add, remove, or update game states based on the game state rules discussed earlier in the chapter. For adding a new game state, you simply create a new game state and call the add API method, the same is true for sendGameStateAction. However, if you are modifying or removing the game state, you must pass the existing game state when calling the API methods. In the case of any errors during the above game state method calls,
onGameStateError notification will be invoked. If the game state updates were
successful, one of the corresponding callback methods will be fired.
public function addGameState(newGameState:GameStateClient):void
The addGameState method adds a new state into the game. The game state ID must be zero. Upon successfully adding the game state, the onAddGameState callback is fired on all players within the room. A unique ID is set for the new game state. The uniqueness is only among the game states for a given game instance (room). This unique ID is different from the stateKey. The stateKey should be set for unique game states as required for the specific game being implemented. If the game state is unique and a previously added game state with the same key was found, onGameStateError is fired on the requesting client and the game state will not be propagated to other players in the room. public function updateGameState(gameState:GameStateClient):void
The updateGameState method should be called to update an existing state in the game. The game state with the ID must exist upon successfully updating the game state. The onUpdateGameState callback is fired on all players within the room. If the game state with the ID was not found on the server, then onGameStateError will be fired on the requesting client and the game state update will not be propagated to other players in the room. public function removeGameState(gameState:GameStateClient):void
The removeGameState method should be called to remove an existing state from the game. The game state with the ID must exist. Upon successful deletion of the game state, the onRemoveGameState callback is fired on all players within the room. If the game state with the ID was not found on the server, then onGameStateError will be fired on the originator and the game state deletion will not be propagated to other players in the room. public function sendGameStateAction(gameState:GameStateClient):void
[ 114 ]
Chapter 5
The sendGameStateAction method can be used to send a state to all players in a game. The game state type is set to transient and will not be persisted for the game session. The ID is also not assigned for a game state action. function onNewGameState(gameState:GameStateClient):void;
The onNewGameState callback is fired when any game client in the game room successfully updates a new game state to the game (via updateGameState). The updated game state is also received by the originator. The server validates if the game state with the ID already exists. Unique and initial game states may not be updated. Any violation is notified back to the sender via onGameStateError. function onUpdateGameState(gameState:GameStateClient):void;
The onUpdateGameState callback is fired when any game client in the game room successfully updates new game state to the game (via updateGameState). The updated game state is also received by the originator. The server validates if the game state with the ID already exists. Unique and initial game states may not be updated. function onRemoveGameState(gameState:GameStateClient):void;
The onRemoveGameState callback is fired when any game client in the game room successfully deletes the game state (via removeGameState). The callback is also fired on the originator of the delete game state call. The server validates if the game state with the ID exists. Unique and initial game states may not be deleted. function onGameStateAction(gameState:GameStateClient):void;
The onGameStateAction callback is fired when any game client in the game room calls sendGameStateAction. The callback is also fired on the originator of the call. The server does not assign an ID and does not persist for the game's session period. Newly joined players do not receive any previously sent game states via sendGameStateAction.
Miscellaneous classes
Among the essential screen classes, PulseUI framework also provides some useful classes that we may use for our game implementation. The SpriteDissolve class used to make a bunch of sprites to disappear included in the PulseUI along with other useful game related classes are presented in Appendix-2.
[ 115 ]
Game Logic
The Button Effect class
This class takes in any sprite and adds two things that provide a hint to the player that it is clickable. Firstly, when the mouse is over the sprite, it adds a shadow filter and removes it when the mouse leaves the bounds of the sprite. Secondly, it turns the cursor from the normal arrow to a hand.
Note that you still need to hook up your mouse event handler to actually handle any mouse events. Here is a full listing of the class ButtonEffect.as: package pulseui.util { import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; public class ButtonEffect { private var m_btn:Sprite; public function ButtonEffect(btn:Sprite) { m_btn = btn; m_btn.addEventListener(MouseEvent.MOUSE_OVER, mouseOver); m_btn.addEventListener(MouseEvent.MOUSE_OUT, mouseOut); btn.buttonMode = true; btn.useHandCursor = true; } private function mouseOver(event:Event):void { var filters:Array = m_btn.filters; filters.push(FilterFactory. createFilter(FilterFactory.DROP_SHADOW_FILTER)); m_btn.filters = filters; } private function mouseOut(event:Event):void { var filters:Array = m_btn.filters; filters.splice(0, 1); [ 116 ]
Chapter 5 m_btn.filters = filters; } } }
Here is an example from the login screen class on how you can add the button effect in a single line of code: public function addOKBtn():void { // Get the sprite for the OK button // from the skinner m_okBtn = Skinner.getOK(); // Position it m_okBtn.x = 170; m_okBtn.y = 220; // Set up the mouse event for click m_okBtn.addEventListener(MouseEvent.CLICK, mouseClickHandler); // Add the button effect new ButtonEffect(m_okBtn); … … … }
The Slider class
Although flash.motion package provides a complete set of functionality to move sprites in numerous ways, it is often helpful to have a simple class to move things around with a simple interface. If you have a sprite that you need to move from its current position to a new position, you may use the Slider class to achieve this. Sample usage of using this may be found in PlayersDisplay. When the scores are updated for players and the players' ranks change, the sprites representing the player slide to their new position, rather than simply being redrawn at the new position. Here is how you can slide the sprite to a new position: new Slider(aSprite, new Point(newX, newY)).play();
The above line causes aSprite to move from its current position to the new X and Y. The play method is what causes the sprite to move. In addition to the slider class that is responsible for moving the sprite, the accompanying class SliderEvent is dispatched, notifying any listener that the sprite has finally arrived at the new position. [ 117 ]
Game Logic
Here is a complete listing for the Slider class: package pulseui.util { import flash.display.Sprite; import flash.events.EventDispatcher; import flash.events.TimerEvent; import flash.geom.Point; import flash.utils.Timer; /** * A class that moves a sprite * from its current poistion to * the new specified position. */ public class Slider extends EventDispatcher { /** * Determines how may pixels * to move along X on every timer event. */ private var m_deltaX:Number; /** * Determines how may pixels * to move along Y on every timer event. */ private var m_deltaY:Number; /** * The timer that is used to * move the sprite. */ private var m_timer:Timer; /** * The sprite being moved by * this instance. */ private var m_sprite:Sprite; /** * Final destination of the sprite */ private var m_to:Point; /** * A user object that will * included in the SliderEvent instance */ [ 118 ]
Chapter 5 private var m_userObject:Object; /** * * @param sprite Sprite to move * @param toPos Final X & Y * @param delta Change in X & Y at each step * @param userObject To include in Slider Event * */ public function Slider(sprite:Sprite, toPos:Point, delta:Number=2, userObject:Object=null) { m_sprite = sprite; m_to = toPos; var diffX:Number = Math.abs(sprite.x - toPos.x); var diffY:Number = Math.abs(sprite.y - toPos.y); if ( diffX > diffY ) { // x has more ground to cover m_deltaX = delta; m_deltaY = (diffY/diffX)*delta; } else { m_deltaY = delta; m_deltaX = (diffX/diffY)*delta; } m_userObject = userObject; } /** * Method that actually starts * the moving of the sprite. * */ public function play():void { m_timer = new Timer(10); m_timer.addEventListener(TimerEvent.TIMER, slide); m_timer.start(); } /** * Handles the internal timer event * updates the sprite position by delta * * @param event The timer event [ 119 ]
Game Logic * */ private function slide(event:TimerEvent):void { // we are close enough... // make sure the sliding delta is // smaller than "close enough" value if ( Math.abs(m_sprite.x - m_to.x) <= 3 && Math.abs(m_sprite.y - m_to.y) <= 3 ) { m_sprite.x = m_to.x; m_sprite.y = m_to.y; m_timer.stop(); var e:SliderEvent; e = new SliderEvent(SliderEvent.DONE, false, false, m_userObject); dispatchEvent(e); return; } if ( m_sprite.x != m_to.x ) { if ( m_sprite.x > m_to.x ) { m_sprite.x -= m_deltaX; } else m_sprite.x += m_deltaX; } if ( m_sprite.y != m_to.y ) { if ( m_sprite.y > m_to.y ) { m_sprite.y -= m_deltaY; } else m_sprite.y += m_deltaY; } } } }
The SliderEvent is dispatched when the sliding is complete. The following is the listing: package pulseui.util { import flash.events.Event; public class SliderEvent extends Event [ 120 ]
Chapter 5 { /** * Dispatched when the slider is complete */ public static const DONE:String = "done"; private var m_userObject:Object; public function SliderEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false, userObject:Object=null) { super(type, bubbles, cancelable); m_userObject = userObject; } public function getUserObject():Object { return m_userObject; } } }
The ShakeEffect class
Many times we want a particular sprite to grab players' attention. One way to do it is to simply shake it. A handy class to do just that is available in the PulseUI package. The shake effect can be applied to any sprite in the following manner: new ShakeEffect(sprite).play();
The following is the complete listing for the ShakeEffect.as: package pulseui.util { import flash.display.Sprite; import flash.events.TimerEvent; import flash.utils.Timer; public class ShakeEffect { /** * Sprite to shake. */ private var m_s:Sprite; /** [ 121 ]
Game Logic * Original X co-ordinate * of the sprite. */ private var m_x:Number; /** * Internal timer. */ private var m_timer:Timer = new Timer(3, 100); /** * Constrctor to shake a given sprite. * The internal timer is set up, for * every timer event, the position x * is varied by 1 * * @param s Sprite to shake * */ public function ShakeEffect(s:Sprite) { m_s = s; m_x = m_s.x; m_timer.addEventListener(TimerEvent.TIMER, onTimer); m_timer.addEventListener(TimerEvent.TIMER_COMPLETE, onTimerEnd); } /** * This method will actually * cause the sprite to shake * */ public function play():void { m_timer.start(); } /** * On every timer event, change * the position of the sprite by delta * @param e * */ private function onTimer(e:TimerEvent):void { if ( m_s.x >= m_x ) m_s.x = m_x - 1; [ 122 ]
Chapter 5 else m_s.x = m_x + 1; } /** * Timer ends. Restore the original * x co-ordinate of sprite. * * @param e * */ private function onTimerEnd(e:TimerEvent):void { m_s.x = m_x; } } }
Summary
This chapter concludes the code walk-through of the Hello World sample. In this chapter, we mostly dealt with game states, their types, and the rules associated with each type. We saw the actual game state API usage in the Hello World sample and a detailed API description. We also saw some helpful classes that are part of PulseUI, such as the button effect, Slider, and the shaker. In the following chapters, we will do a detailed walk-through of few other simple games, starting with tic-tac-toe, a multiplayer jigsaw, and a racing game.
[ 123 ]
Multiplayer Game Example: Tic-tac-toe In this chapter, we will walk through the tic-tac-toe sample that is part of the freely downloadable Pulse SDK package. We will focus solely on this sample, exploring every class in detail that is involved in building this simple multiplayer game. The walk-through starts with schema design for the game, then proceeds to the subclassing of the PulseUI screen classes as required for this game, and finally focuses on the game screen and gameplay implementation. Tic-tac-toe, for those who are not familiar with it, is a turn-based game where each player draws a circle or a cross in one of the available nine spots. If the player is able to place three of their symbols in a straight line, either in a straight row, column, or diagonally, then the player wins. If neither of the players is able to make a straight line and there is no more room to place the symbol, then the play is considered a draw—no one wins. If a player wins, he or she will be a awarded a score of 1. The game round ends if one of the players wins or the game is a tie. The board is cleared and a new round begins. In this chapter, we will cover the following with regards to the multiplayer tic-tac-toe game: •
The Schema design
•
Subclassing from PulseUI
•
Player turn management
•
Implementing the game graphics
•
Determining a win or a loss
Multiplayer Game Example: Tic-tac-toe
The following is a screenshot from the tic-tac-toe game showing two player screens side-by-side:
Running the game from sample directory
Start the Pulse server from the installed directory. Start the game from the sample directory. Please note that with the free server version you can only run one game at any given time. If you want to try a different sample, you first need to close all the game client instances of the other game and restart the server. Complete details on downloading, installing, and running the samples can be found in Chapter 1.
The Pulse UI framework
The Pulse UI framework is a great way to start your new multiplayer game. It lets you focus on your game and takes care of the UI for general screen flow login, room management, chatting, friend making, player display, etc. You will find the entire source for the framework in the download package; feel free to modify it as needed for your game.
[ 126 ]
Chapter 6
Setting up the project
Fire up the Flash Builder and create an ActionScript Project; call it TicTacToe or any other suitable name you like. You may choose to create the project from existing sources that are already installed on your computer. The project creation is similar to that described in Chapter 2 for the Hello World project. We also need to include the two library packages Pulse and PulseUI to the project. The library packages pulse.swc and pulseUI.swc may be found in the $\lib folder in the Pulse installation folder. The following is a screenshot showing the included swc packages in the project:
We also need to set the stage size to 800 (width) and 600 (height). The Pulse UI requires the stage size to be exactly this size and we may also set the background color of our choice, as shown in the screenshot below:
[ 127 ]
Multiplayer Game Example: Tic-tac-toe
Getting started: Modeling the game
The first thing to think about is the best way to share the game states among all the players in the room, or in other words what player actions are needed to be seen by the other player. For the tic-tac-toe game, we would need to send the player action to the other player, meaning the client has to send up the x and y positions of where the player placed the symbol. The first thing to do is to create a simple XML game schema file that defines the game state we need during game play. There are two steps: 1. Create the XML file and save it to the root game project folder. 2. Generate the source using the PulseCodeGen located in the bin of the installed folder. <property index="0" name="PutRow" count="1" type="int"/> <property index="1" name="PutColumn" count="1" type="int"/> <property index="2" name="PutValue" count="1" type="int"/>
In the above schema file, we would like to communicate to other players their actions. What is needed for this game is quite simple. The player chooses the spot (row and column) and the value, meaning an X or an O, and the Put value is determined at the start of the game depending on who actually created the game room (the game host). Having designed the game states that are needed for the game, we need to run the above XML file through the PulseCodeGen tool. The following is a listing of batch file that will do exactly that. The batch file may be saved to the project root folder. You may need to modify the paths in the batch file as required, as your directory structure may be different from what is presented here.
[ 128 ]
Chapter 6 REM ********** init.bat ************ IF EXIST .\src\tictactoe\gsrc\client del .\src\tictactoe\gsrc\ client\*.as CALL "%GAMANTRA%"\bin\PulseCodeGen.bat .\tictactoeSchema.xml tictactoe.gsrc .\src\tictactoe\gsrc IF NOT %ERRORLEVEL% == 0 GOTO ERROR ECHO Success! GOTO END :ERROR ECHO oops! :END Pause REM ***** end of init.bat ***********
The above batch file is convenient. So, if you changed the schema, the regeneration of source files can be quickly regenerated.
Project directory structure
If we explored the game's directory structure, we will find the game folder containing a couple of helper classes that allows us to draw things on the game screen, is shown as follows. Under gsrc, all the generated class files are stored. The rsrc folder contains all the image assets for the game. The ui folder contains the class files that are subclassed from the Pulse UI package.
[ 129 ]
Multiplayer Game Example: Tic-tac-toe
Code walk-through
Once we have generated the class files based on the schema we designed, we need to prepare some of the game screens for the game. For this we will leverage from the screen classes that are already available to us from the Pulse UI package. We simply need to subclass from these classes as shown next:
TictactoeGame
This is the main class where the game code begins. This is also the place where we tell the Pulse UI of the game specific classes that are being overridden.
Overriding the constructor
Constructor is a good place to instantiate the game-specific skinner class. More on implementing the skinner class is discussed later. We also set the super-class defined as s_instance to this so that any reference to the singleton points to our subclass instead of the parent. public function TictactoeGame() { s_instance = this; new TictactoeSkinner(); super(); }
Override the getGameId method from PulseGame as follows: public override function getGameId():String { return "Tictactoe"; }
[ 130 ]
Chapter 6
The Pulse runtime needs a unique game ID for each game, as many games could be hosted on a single server. This ID is also used by the server to match up other players playing the same game. It means players of jigsaw will not be able to go into a room that is playing tic-tac-toe.
Overriding the initNetClient method
A factory object must be created as shown below. The factory class is one of the classes that are created for us during code generation. The m_netClient is a protected property of the parent class PulseGame. import tictactoe.gsrc.client.GNetClientObjectFactory; … … … protected override function initNetClient():void { var factory:GNetClientObjectFactory; factory = new GNetClientObjectFactory(); m_netClient = new GameClient(factory, this); }
This (m_netClient) is the main API class through which we send all the game states to the server, which are then broadcast to all the players in the room. We will also override two more methods, one for the game screen where the tic-tactoe game will be played and the other for the new game screen where a new room will be created by the player. protected override function initGameScreen():void m_gameScreen = new TictactoeGameScreen(this); m_gameScreen.init(); }
{
protected override function initNewGameScreen():void m_newGameScreen = new TictactoeNewGameScreen(); m_newGameScreen.init(); }
[ 131 ]
{
Multiplayer Game Example: Tic-tac-toe
Implementing a turn-based game
Remember that the PulseGame also implements the GameClientCallback interface, which means that when any notification is received by the client from the server, the appropriate callback method is invoked on the implementing instance. Since tic-tac-toe is a turn-based game (see subclass details of
TictactoeNewGameScreen), the Pulse API fires a callback to let the client know that
it is now the player's turn. During this time, a visual hint should be displayed to the player. public override function onPlayerTurn():void { (m_gameScreen as TictactoeGameScreen).onPlayerTurn(); }
The logic here in this class is to simply call the game screen to display the hint.
Sending and receiving player actions
There are two methods that need to be implemented, one to receive the action of another player and the other to send the action to the other player. To receive the action of another player, we simply override the onGameStateAction and pass it to the game screen. public override function onGameStateAction(gameState:GameStateClient) :void{ (m_gameScreen as TictactoeGameScreen).onPlayerMoved(gameState); }
To send the player action, we write a convenience sendGameState method, which is called by the game screen object. During this time, we also tell the server that the player's turn is done so that the server can inform about the next player's turn. Note that the PutClient was defined in the schema file and generated during code generation phase. public function sendGameState(xPos:int,yPos:int,value:int):void { var putMsg:PutClient = new PutClient(); putMsg.setStateType(GameConstants.GS_IS_UNIQUE); putMsg.setPutRow(xPos); putMsg.setPutColumn(yPos); putMsg.setPutValue(value); m_netClient.sendGameStateAction(putMsg); m_netClient.nextTurn(); }
[ 132 ]
Chapter 6
TictactoeSkinner
You need to subclass the Skinner class (PulseUI) so as to read in the image (skin) files that will be used to draw all the UI for all of the screens. public class TictactoeSkinner extends Skinner { [Embed(source="tictactoe\\rsrc\\Outline.png")] private static var OutlineClass:Class; [Embed(source="tictactoe\\rsrc\\ui.png")] private static var UIClass:Class; [Embed(source="tictactoe\\rsrc\\frame.png")] private static var FrameClass:Class; public function TictactoeSkinner() { } protected override function load():void { m_outline = (new OutlineClass() as BitmapAsset). bitmapData; m_ui = (new UIClass() as BitmapAsset).bitmapData; m_frame = (new FrameClass() as BitmapAsset).bitmapData; } }
The custom skinner is quite simple. The above code is the complete listing for the class. The load method should be overridden to create BitmapAsset for the outline, UI, and frame. These are protected fields defined in the skinner class. The skinner object will do all the hard work of cutting up the asset and piecing it altogether during the runtime.
TictactoeNewGameScreen
It is optional to subclass NewGameScreen class (PulseUI) for this game. However, we need to tell the Pulse server the specific properties of the game room, such as it being turn-based, having a maximum of two players per room, etc. public override function createNewRoom():void { PulseGame.getInstance().setCreatingRoom(); var room:GameRoomClient = new GameRoomClient(); room.setAutoReady(1); room.setRoomName(m_ti.text); room.setMaxPlayerCount(2); room.setRoomType(GameConstants.ROOM_TURN_BASED); PulseGame.getInstance(). getGameClient().createGameRoom(room); } [ 133 ]
Multiplayer Game Example: Tic-tac-toe
The new game screen is required to be customized only for the purpose of defining the kind of room that is needed for the game. The above code is specific to the tictac-toe game. The code creates a new room object instance, specifies that the player is automatically set to ready when they join the room, sets up the room name for others to see in the lobby, allows a maximum of two players into the room, specifies that the room is turn-based, and finally requests the server to create the room. Once it is created on the server, the room will appear in the lobby screen for other players to join.
TictactoeGameScreen
Lastly, there is the class where all the action happens inside of the game room, once the game has started. This class is sub-classed from GameScreen class found in PulseUI framework. In order to implement the required game display, we define a helper class, TictactoeHotspot, a subclass of sprite used for tracking the nine regions of the game board and onto which we add a sprite child, either O or X, depending on the player. We instantiate the nine objects of this class for each spot. package tictactoe.game { import flash.display.Sprite; public class TictactoeHotspot extends Sprite { public var value:int; // who's clicked? public var row:int; // my pos public var col:int; // my pos public var win:Boolean; // part of win? public function TictactoeHotspot(c:int, r:int) { value = 0; col = c; row = r; win = false; } } }
Additionally, TictactoeGameStatus is a simple class for holding constants required for the game. package tictactoe.game { public class TictactoeGameStatus { public static var CONTINUE:int = 100; public static var HOST_WIN:int = 101; public static var GUEST_WIN:int = 102; [ 134 ]
Chapter 6 public static var TIE:int = 103; } }
Initializing the game screen
The init method is listed as follows. The init method is called only once during the entire game execution. public override function init():void { super.init(); //init the border sprite var borderBitmapAsset:BitmapAsset = new Border() as BitmapAsset; m_borderSprite = new Sprite(); m_borderSprite.graphics. beginBitmapFill(borderBitmapAsset.bitmapData); m_borderSprite.graphics. drawRect(0,0,borderBitmapAsset.width, borderBitmapAsset.height); m_borderSprite.graphics.endFill(); m_borderSprite.x = 150; m_borderSprite.y = 120; //init the 9 hotspots sprite array var hotspotBitmapAsset:BitmapAsset; hotspotBitmapAsset = new HotSpot() as BitmapAsset; m_hotspotArray = new Array(); var x:int, y:int; var y_offset:int = 0; for ( x=0; x < 3; x++) { m_hotspotArray[x] = new Array(); var x_offset:int = 0; for ( y=0; y < 3; y++) { var hotspot:TictactoeHotspot; hotspot = new TictactoeHotspot(x, y); m_hotspotArray[x][y] = hotspot; hotspot.graphics. beginBitmapFill(hotspotBitmapAsset.bitmapData); hotspot.graphics. drawRect(0, 0, hotspotBitmapAsset.width, hotspotBitmapAsset.height); hotspot.graphics.endFill(); [ 135 ]
Multiplayer Game Example: Tic-tac-toe hotspot.x = HOSTSPOT_POS_OFFSET_X + x_offset; hotspot.y = HOSTSPOT_POS_OFFSET_Y + y_offset; hotspot.addEventListener(MouseEvent.CLICK, playAction); x_offset += HOSTSPOT_STRIDE; } y_offset += HOSTSPOT_STRIDE; } //init the circle BitmapAsset m_circleBitmapAsset = new Circle() as BitmapAsset; //init the cross BitmapAsset m_crossBitmapAsset = new Cross() as BitmapAsset; }
The implementation looks a bit hairy, but it is essentially doing three things. Firstly, the tic-tac-toe board (border) is drawn. Next, nine hotspot objects are laid out where the player may click on the board. Lastly, bitmap assets for circle and cross are initialized.
A player goes in and out of a room (game screen) many times. Every time a player enters the game room, the show() method is called, and hide() is called whenever the player leaves the room. public override function show():void { super.show(); }
In this game, we choose not to draw anything in the show method, but rather wait until the game starts. The super.show() calls the show method in the GameScreen class that displays the go or the wait sprite. The room creator automatically makes the host for the room and PulseUI will make sure to display the go for the host player and wait for the other player joining the room. If no special handling is required for our game, you may very well omit the override; it is listed here simply for clarity.
[ 136 ]
Chapter 6
The startGame method is called on all the player clients when the host clicks on the go sprite. The field m_putValue is initialized, which is used to determine whether an O or X should be displayed. public override function startGame():void { super.startGame(); if ( PulseGame.getInstance(). getGameClient().isGameHost() ) { m_putValue = 1; } else { m_putValue = -1; } addChild(m_borderSprite); for each (var hotspotArray:Array in m_hotspotArray) { for each (var hotspot:TictactoeHotspot in hotspotArray) { m_borderSprite.addChild(hotspot); } } m_isGaming = true; }
The super implementation removes the go or the wait sprite. The game board is now displayed to the player. In the following hide method, we simply make sure we remove every game screen-related sprite: public override function hide():void { super.hide(); if(m_borderSprite.numChildren > 0) { for each (var hotspotArray:Array in m_hotspotArray) { for each (var hotspot:TictactoeHotspot in hotspotArray) { if(hotspot.numChildren > 0) hotspot.removeChildAt(0); m_borderSprite.removeChild(hotspot); } } } if(m_isGaming) { removeChild(m_borderSprite); [ 137 ]
Multiplayer Game Example: Tic-tac-toe if (PulseGame.getInstance(). getGameClient().isMyTurn() == true) removeChild(m_turnSprite); } m_isGaming = false; }
In this game, there could only be two players. So what happens when one of the players simply closes the browser or gets disconnected from the server? In this case, we terminate the game and move the remaining player to the lobby. The player may also actively jump to the lobby initiating the game termination. If the game has started, we remove the game board; we also check if the Your Turn sprite is being displayed and remove that as well. The lobbyHit method is called when the player chooses to quit the game screen. At this point, we simply end the game. For this particular game implementation, there is no point in having one player stay in the room after the other player has quit. So, as soon as one player quits, we send the remaining player to the lobby. protected override function lobbyHit():void { if ( m_isGaming ) m_tictactoeGame.getGameClient().finishGame(); super.lobbyHit(); }
Now that we have seen screen management for the game board, we will see how the game plays out.
Displaying player turn
Recall that this method is called from our subclass of PulseGame object instance. When the server indicates that it is the player's turn to play, we show the Your Turn sprite to the player. public function onPlayerTurn():void { // show the turn indicator m_showingTurn = true; addChild(m_turnSprite); }
[ 138 ]
Chapter 6
Letting the player make the move
For each hotspot we saw earlier (in the init method), we subscribe the mouse click. The callback method is set to be the previous method, playAction. First, we check if it is indeed the player's turn to move. If not, we simply ignore it. Second, we check if the clicked hotspot is an empty one. If so, we send the player action to the server, which in turn will send the game state to the player itself and to the other player. private function playAction(event:MouseEvent):void { var gc:GameClient; gc = PulseGame.getInstance().getGameClient(); if (gc.isMyTurn() == true) { var sel:TictactoeHotspot; sel = event.target as TictactoeHotspot; if (sel == null ) return; var xPos:int = sel.col; var yPos:int = sel.row; if ( m_hotspotArray[xPos][yPos].value == 0 ) { m_tictactoeGame. sendGameState(xPos, yPos, m_putValue); removeChild(m_turnSprite); } } }
When the player action is received by the client, the TictactoeGame (refer above) object calls the following method. The implementation here simply updates the screen and checks if the game was won, lost, or tied. public function onPlayerMoved(gameState:GameStateClient):void { var putMsg:PutClient = gameState as PutClient; showSprite(putMsg.getPutRow(), putMsg.getPutColumn(), putMsg.getPutValue()); checkGameEnd(); }
[ 139 ]
Multiplayer Game Example: Tic-tac-toe
Who won?
For every move any of the players make, we need to check if the player won (the player was able to put three of their symbols in a straight line.) We need to check if there are three of the same kind of symbol in the same row, or in the same column, or along the diagonals. Note that we perform this check in the onPlayerMoved method. This method is called when either the player or the opponent makes a move. private function checkGameEnd():void { var status:int = checkGameStatus(); var gc:GameClient; gc = PulseGame.getInstance().getGameClient(); if ( status != TictactoeGameStatus.CONTINUE) { if ( status == TictactoeGameStatus.HOST_WIN || status == TictactoeGameStatus.GUEST_WIN ) { var iWin1:Boolean, iWin2:Boolean; iWin1 = (status==TictactoeGameStatus. HOST_WIN && gc.isGameHost()); iWin2 = (status==TictactoeGameStatus. GUEST_WIN && !gc.isGameHost()); if ( iWin1 || iWin2 ) { // I win! var av:GameAvatarClient; av = gc.getMyAvatar(); // APPLY THE SCORE! av.setScore(av.getScore()+1); gc.publishMyAvatar(); showGameEnd(m_winSprite); } else { // I loose! showGameEnd(m_looseSprite); } } else if ( status == TictactoeGameStatus.TIE ) { // Game tied! showGameEnd(m_tieSprite); } finishRound(); } }
[ 140 ]
Chapter 6
The previous method (checkGameEnd) checks if the game was won by either of the player or was tied. The status tells us the current status of the game. The status values can be one of the values from the TictactoeGameStatus: •
CONTINUE
•
HOST_WIN
•
GUEST_WIN
•
TIE
If the status is CONTINUE, then the game can still be played. HOST_WIN or GUEST_WIN indicates who won the game already. TIE, in case all the spots are marked and there was no winner. Note that when we determine that this player has won the game, we increase the score by 1 and call the Pulse API, publishMyAvatar. The status itself is determined by the checkGameStatus method called at the beginning of the checkEndGame method. private function checkGameStatus():int { var ret:int,totalBit:int = 0; for(var row:int = 0;row < 3;row++) { for(var column:int = 0;column < 3;column++) { ret = checkCell(row,column); if(ret == 3) return TictactoeGameStatus.HOST_WIN; else if(ret == -3) return TictactoeGameStatus.GUEST_WIN; else totalBit += ret; } } if(totalBit == 9) { markWin(4, -1); return TictactoeGameStatus.TIE; } else return TictactoeGameStatus.CONTINUE; }
The above method (checkGameStatus) adds the value of the marks along all possible rows, columns, and diagonally. If the sum of the values adds up to 3, then the host has won; if it is -3, the guest has won.
[ 141 ]
Multiplayer Game Example: Tic-tac-toe
The actual adding and checking to see if anyone has won, or if the game is still in progress, or if the game is tied, is determined by the following method: private function checkCell(checkRow:int,checkColumn:int):int { var i:int=0; //reset the value //var verifyElement:int = 0; for (i = 0; i < m_verifyArray.length; i++) m_verifyArray[i] = 0; var hotspot:TictactoeHotspot = null; for(i = 0; i < 3; i++) { //check vertical if ((checkRow+i) < 3) { m_verifyArray[0] += m_hotspotArray[checkRow+i] [checkColumn].value; if ( m_verifyArray[0] == 3 || m_verifyArray[0] == -3 ) markWin(0, checkColumn); } //check horizontal if ((checkColumn+i) < 3) { m_verifyArray[1] += m_hotspotArray[checkRow] [checkColumn+i].value; if ( m_verifyArray[1] == 3 || m_verifyArray[1] == -3 ) markWin(1, checkRow); } //check right diagonal if ((checkRow+i) < 3 && (checkColumn+i) < 3) { m_verifyArray[2] += m_hotspotArray[checkRow+i] [checkColumn+i].value; if ( m_verifyArray[2] == 3 || m_verifyArray[2] == -3 ) markWin(2, -1); } //check left diagonal if ((checkRow-i) >= 0 && (checkColumn+i) < 3) { m_verifyArray[3] += m_hotspotArray[checkRow-i] [checkColumn+i].value; if ( m_verifyArray[3] == 3 || [ 142 ]
Chapter 6 m_verifyArray[3] == -3 ) markWin(3, -1); } } for each (var verifyElement:int in m_verifyArray) { if (verifyElement == 3 || verifyElement == -3) { return verifyElement; } } return (m_hotspotArray[checkRow] [checkColumn].value!=0)?1:0; }
Finding the winner
When the game has been won by a player, we want to show how it was won. The following method turns the winning row or column or the diagonal hotspots to true: private function markWin(type:int, index:int):void { switch (type) { case 0: { m_hotspotArray[0][index].win = true; m_hotspotArray[1][index].win = true; m_hotspotArray[2][index].win = true; break; } case 1: { m_hotspotArray[index][0].win = true; m_hotspotArray[index][1].win = true; m_hotspotArray[index][2].win = true; break; } case 2: { m_hotspotArray[0][0].win = true; m_hotspotArray[1][1].win = true; m_hotspotArray[2][2].win = true; break; } case 3: { m_hotspotArray[0][2].win = true; m_hotspotArray[1][1].win = true; m_hotspotArray[2][0].win = true; break; } [ 143 ]
Multiplayer Game Example: Tic-tac-toe case 4: { for(var r:int = 0; r<3; r++) for(var c:int=0; c<3; c++) m_hotspotArray[r][c].win = true; break; } } }
After the hotspots have been marked, we call the showEndGame method, which will keep the winning symbols and dissolve them slowly; the others are removed immediately. This will let the players know how the game was won. private function showGameEnd(result:Sprite):void { result.x = 400 - result.width/2; result.y = 300 - result.height/2; addChild(result); // Display the game result var a:Array = new Array(); a.push(result); var sd:SpriteDissolve; sd = new SpriteDissolve(this, a); sd.play(); // Fade away the wins only a = new Array(); for(var r:int = 0; r<3; r++) { for(var c:int=0; c<3; c++) { if (m_hotspotArray[r][c].win) { a.push((m_hotspotArray[r][c] as Sprite).getChildAt(0)); } } } sd = new SpriteDissolve(this, a); sd.play(); }
[ 144 ]
Chapter 6
Other screens and features
Note that we did not write a single line of code for chat, lobby display, friend making, top-ten, or registration. But all these features are part of this game! This is made possible since we have based the code on the PulseUI framework.
Lobby screen
Lobby screen includes the display of available game rooms that can be joined, the rooms that have less than two players, and those that have not yet started. In this particular game, however, the game may not start with only one player and there can be a maximum of two players.
[ 145 ]
Multiplayer Game Example: Tic-tac-toe
Chat
Chat is available in the lobby and in the game room, further, we can have a private chat as well. To enable private chat, we need to click on the avatar icons either among friends (in the lobby) or from the player list (within the game room).
By clicking on the icon, the text next to the input field changes to show the recipient of the private message. Any message typed will be received only by one—in the above case, Guest-1120. Clicking on the text Recipient will change it back to the public mode, in which case all the messages typed will be received by all the players in the room.
[ 146 ]
Chapter 6
TopTen
We did not write a line of code for top-ten as well.
The server keeps track of the score at the end of each game and will add it to the overall scores for each player of the game. If there is any change in ranking, the server sends this information to all the clients playing the game.
[ 147 ]
Multiplayer Game Example: Tic-tac-toe
Registration screen
A standard registration screen may be accessed via the lobby. The registration screen takes just enough information to let the players log in with their username and password and have an avatar with a name of their choice. The Pulse server includes a duplicate avatar name check. If the avatar name chosen by the user already exists, the user will be informed of the fact and must choose another avatar name.
Summary
In this chapter, we walked though every bit of code implementing the multiplayer tic-tac-toe. We saw that, leveraging the Pulse UI framework, we were able to focus solely on the game implementation, leaving the rest of the multiplayer game necessities to default behavior of the framework. Mainly we saw how we can use the game state actions to exchange information between the players. We also saw how to handle the player's turns and score keeping. Lastly, we looked at the default screens for which we did not write a single line of code. With this chapter under your belt, you should be able to implement similar games such as Four-In-a-Row, Chess, Rock-Paper-Scissors, and many more! [ 148 ]
Multiplayer Game Example: Jigsaw In the previous chapter, we saw the implementation of a simple multiplayer game, tic-tac-toe. In this chapter, we will see how we can implement a multiplayer jigsaw game. Unlike tic-tac-toe, the jigsaw game is not implemented as turn-based and players may join the game room after the game is well underway. Here we will discuss the following: • • • •
Graphics programming of the game Schema design Multiplayer synchronization Various game screen implementation
The following is a screenshot from the jigsaw game showing two player screens side by side:
Multiplayer Game Example: Jigsaw
The simple game design for this sample game is for each player to match the right pieces—whoever matches the most pieces wins the game. Before we dive deep into the multiplayer aspects of the game, we will first walk through the code that manages the graphics part of the game. We will see how, for a given image, we cut it up into jigsaw pieces and then how we manage to stick them back together. We will then see the game design schema required to implement it as a multiplayer game. Lastly, we will put it all together by leveraging code from PulseUI and finish the game.
Setting up the project
The complete source for the sample game may be found (in the freely available Pulse SDK package) installed in $GAMANTRA\samples\jigsaw. To create a project in Flash Builder from the existing source, please follow the steps similar to those described for the Hello World sample in Chapter 2.
Files in the project
The list of files that are part of the game project is shown next. The game folder contains all the classes required to implement the graphics part of the game, such as scaling the image, cutting the images into jigsaw pieces, the mini-map showing the complete image, and so on. All the generated class files are stored under gsrc. The rsrc folder contains all the image assets required for the game. The ui folder contains the class files that are subclassed from the PulseUI package and finally the main class in the root package JigsawGame.as.
[ 150 ]
Chapter 7
The game graphics
In this section, we will go through and see how to cut the picture into pieces, manage them, keep track of the pieces that can be connected and also check if they are connected.
DisplayManager
This is a top level class that manages all the action when a picture is selected to play. It contains two other important class instances, one being the PieceCutter, which takes in an image and cuts them to pieces, the other being the group, which is a collection of already connected pieces.
[ 151 ]
Multiplayer Game Example: Jigsaw
DisplayManager also has the responsibility of finding if the puzzle is done, thereby initiating the end game effect.
Managing pieces—Group
An important class called group is used to manage the pieces that are currently connected. To begin with, there will be as many groups as there are pieces. So each group object is instantiated with one piece. Groups may merge when one piece of a group is connected to a piece from another group. When two groups merge, we transfer all the pieces from one group to the other and we purge the other group. We choose the bigger group to transfer the pieces to; if the two groups are of the same size then we simply pick one. The puzzle is completed when there is only one group left.
The PieceSprite class
The PieceSprite class is the sprite representing a single jigsaw piece. The actual visual sprite that is seen on the screen is contained (m_sprite) in this class. There will be as many piece sprite instances as there are pieces in the puzzle. The players may drag around a PieceSprite on the screen. When a piece sprite is moved and if it is a part of a group with other pieces, we need to take care to move all the pieces in the group. Each piece could have four neighboring pieces, except for those in the corners and edges of the puzzle. The pieces are stored in a two-dimensional array. Given the number of rows and columns, we could easily figure out the correct neighboring pieces. To begin with, the piece sprites are themselves randomly distributed on the screen. When a piece is moved by the player, we simply check if any of the correctly matching neighboring pieces are close enough; if so, we trigger the merge.
[ 152 ]
Chapter 7
Creating a piece
To create the jigsaw pieces, we start out with masks. The mask merged with the part of the puzzle image is shown as follows:
There are totally 64 different masks that are applied to different parts of the image. Each mask is stored as a PNG file in the jig/rsrc/mask folder of the project. Each mask is brought into memory by jig.game.MaskAsset, like the snippet shown next: package jig.game { public class MaskAsset { [Embed(source="jig\\rsrc\\mask\\17.png")] public static const C17: Class; [Embed(source="jig\\rsrc\\mask\\18.png")] public static const C18: Class; [Embed(source="jig\\rsrc\\mask\\20.png")] public static const C20: Class; [Embed(source="jig\\rsrc\\mask\\21.png")] public static const C21: Class;
Each mask has an ID, which has great significance. Note that the filenames don't start from 0 and go all the way to 63. Instead they represent the shape of the mask. The edges of a piece can be of three different types: •
Flat
•
Convex
•
Concave
If the edge of the mask is flat, then we give it a binary value of 00. The edge will be flat only if the piece is in the corner or along the edge. If the edge is convex, then we give it a value of 01, and if the edge is concave, we give it a value of 10. The name of the file is then formed by concatenating the piece's edge value in the top, bottom, left, and right order. [ 153 ]
Multiplayer Game Example: Jigsaw
Here are a few examples: Binary Value: 00 01 00 01 and Decimal Value: 17:
Binary Value: 01 01 01 01 and Decimal Value: 85:
Binary Value: 01 10 10 01 and Decimal Value: 105:
From the ID of a mask instance, we can easily determine what shape the mask is. This is useful in determining which mask the neighboring piece should be when we are assigning the mask to each part (piece) of the puzzle image. The routine that does so is in the jig.game.ShapeGen class. The static gen method does the picking of mask shapes for each puzzle. We know the size of the image and how big each piece will be. From this we know how many rows and columns of pieces we require. The gen method returns a two-dimensional array, each element containing the shape object, which will then be used to merge it with the corresponding part of the puzzle image.
[ 154 ]
Chapter 7
Let us look at the top level pseudo code for the gen method: •
For each piece in the column: °°
For each piece in the row
°°
TopEdge = topEdge()
°°
BottomEdge = bottomEdge()
°°
LeftEdge = leftEdge()
°°
RightEdge = rightEdge()
°°
Shape = TopEdge + BottomEdge + LeftEdge + RightEdge
To figure the top edge, we know that if it is the top-most row (row = 0), the edge must be flat (binary 00). Otherwise, we will make the top edge compatible with the bottom edge of the piece on top. Similarly, to figure out the bottom edge, if the piece is on the last row, the edge will be flat; if not, we choose convex or concave randomly. We do a similar thing for the right and the left edges. Finally, we combine the edge values to come up with the shape ID. shapeID = ((topData << TOP_SIDE_BIT_SHIFT) | (bottomData << BOTTOM_SIDE_BIT_SHIFT) | (leftData << LEFT_SIDE_BIT_SHIFT) | (rightData << RIGHT_SIDE_BIT_SHIFT));
Here, topData, bottomData, leftData, and rightData are the edge values as 00 for flat, 10 for concave, 01 for convex. The final shape ID is obtained by bit or-ing the values after shifting them by the right amount as show next: private private private private
static static static static
const const const const
TOP_SIDE_BIT_SHIFT:int = 6; BOTTOM_SIDE_BIT_SHIFT:int = 4; LEFT_SIDE_BIT_SHIFT:int = 2; RIGHT_SIDE_BIT_SHIFT:int = 0;
Here is a complete listing of the gen method: public static function gen(numCols:int, numRows:int):Array { var workspaceNumRowDiv:int = 1; var workspaceNumColDiv:int = 1; var shapeIDs:Array = new Array(); // init the array var i:int, j:int; for ( i=0; i
Multiplayer Game Example: Jigsaw } } //Random random = new Random(); var rand:int = 0; var numColsPerDivision:int = numCols/workspaceNumColDiv; var numRowsPerDivision:int = numRows/workspaceNumRowDiv; var workspaceDivColIndex:int = 0; var workspaceDivRowIndex:int = 0; var topData:int, bottomData:int, leftData:int, rightData:int; //byte prvBottomData=0, prvRightData=0; var shapeID:int = 0; for(var col:int = 0; col> BOTTOM_SIDE_BIT_SHIFT) ; prvRowBottomData = (tmp & TYPE_MASK); if(prvRowBottomData == TYPE_CONVEX) { topData = TYPE_CONCAVE; } [ 156 ]
Chapter 7 else { topData = TYPE_CONVEX; } } //bottom if(row == (numRows-1)) { bottomData = TYPE_FLAT; } else { rand = Math.random()*10; if(rand%2) { bottomData = TYPE_CONVEX; } else { bottomData = TYPE_CONCAVE; } } //left if(col == 0) { leftData = TYPE_FLAT; } else { var prvColData:int; prvColData = shapeIDs[col-1][row]; var prvColRightData:int; var tmp1:int; tmp = (prvColData >> RIGHT_SIDE_BIT_SHIFT); prvColRightData = (tmp & TYPE_MASK); if(prvColRightData == TYPE_CONVEX) { leftData = TYPE_CONCAVE; } else { leftData = TYPE_CONVEX; } } //right if(col == (numCols-1)) { rightData = TYPE_FLAT; } else { rand = Math.random()*10; [ 157 ]
Multiplayer Game Example: Jigsaw if(rand%2) { rightData = TYPE_CONVEX; } else { rightData = TYPE_CONCAVE; } } shapeID = ((topData << TOP_SIDE_BIT_SHIFT) | (bottomData << BOTTOM_SIDE_BIT_SHIFT) | (leftData << LEFT_SIDE_BIT_SHIFT) | (rightData << RIGHT_SIDE_BIT_SHIFT)); shapeIDs[col][row] = shapeID; } } return shapeIDs; }
Once we have figured out the shapes that need to be applied to each piece, let's look at the code that creates the final piece that we see on the screen. To do this we make use of the copyPixel method available on Bitmapdata. The description of the method quoted from Adobe's online documentation is as follows: public function copyPixels(sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point, alphaBitmapData:BitmapData = null, alphaPoint:Point = null, mergeAlpha:Boolean = false):void
Provides a fast routine to perform pixel manipulation between images with no stretching, rotation, or color effects. This method copies a rectangular area of a source image to a rectangular area of the same size at the destination point of the destination BitmapData object. If you include the alphaBitmap and alphaPoint parameters, you can use a secondary image as an alpha source for the source image. If the source image has alpha data, both sets of alpha data are used to composite pixels from the source image to the destination image. The alphaPoint parameter is the point in the alpha image that corresponds to the upper-left corner of the source rectangle. Any pixels outside the intersection of the source image and alpha image are not copied to the destination image. The mergeAlpha property controls whether or not the alpha channel is used when a transparent image is copied onto another transparent image. To copy pixels with the alpha channel data, set the mergeAlpha property to true. By default, the mergeAlpha property is false. [ 158 ]
Chapter 7
Parameters sourceBitmapData:BitmapData—The input bitmap image from which to copy pixels. The source image can be a different BitmapData instance, or it can refer to the current BitmapData instance. sourceRect:Rectangle—A rectangle that defines the area of the source image
to use as input.
destPoint:Point—The destination point that represents the upper-left corner of
the rectangular area where the new pixels are placed.
alphaBitmapData:BitmapData (default = null)—A secondary, alpha
BitmapData object source.
alphaPoint:Point (default = null)—The point in the alpha BitmapData
object source that corresponds to the upper-left corner of the sourceRect parameter.
mergeAlpha:Boolean (default = false)—To use the alpha channel, set the value to true. To copy pixels with no alpha channel, set the value to false.
Let's examine the cut method in the PieceCutter class. The above copyPixels is used in the following way: pd.copyPixels(bmd, rect, new Point(0, 0), mask.bmd, new Point(0, 0), true);
Here bmd is the puzzle image bitmapdata, rect represents the part of the image for which the piece is being created and the mask.bmd is the shape. The rectangle (rect) is not as straightforward to determine for each as one would expect. Depending on the shape, we need to account for an offset if the edge is concave or convex. The cut method takes in the bitmap data of the puzzle image and then returns a two dimensional array of group objects. Each group object contains a one-piece object. These groups are then merged as and when the player matches the pieces together. public function cut(bmd:BitmapData):Array { // The return array var ret:Array = new Array(); var i:int, j:int; var cols:int = bmd.width/IML.PIECE_SIZE; [ 159 ]
Multiplayer Game Example: Jigsaw var rows:int = bmd.height/IML.PIECE_SIZE; // Init the puzzle sprite space PieceSprite.initMax(cols, rows); // Generate shape for the puzzle pieces var shapes:Array = ShapeGen.gen(cols, rows); var id:int = 0; for ( i=0; i 0 ) { // Account for edge x -= mask.ox; } else x = -1; y = j*IML.PIECE_SIZE; if ( j > 0 ) { y -= mask.oy; } else y = -1; var rect:Rectangle; rect = new Rectangle(x, y, wide, high); var pd:BitmapData; pd = new BitmapData(wide, high, true, 0); // Merge the shape with the // part of the puzzle picture pd.copyPixels(bmd, rect, new Point(0, 0), mask.bmd, new Point(0, 0), [ 160 ]
Chapter 7 true); // Create the sprite out of it var sprite:Sprite = new Sprite(); sprite.graphics.beginBitmapFill(pd); sprite.graphics.drawRect(0, 0, wide, high); sprite.graphics.endFill(); sprite.x = 0; sprite.y = 0; // Apply Bevel filter var filters:Array = sprite.filters; filters.push(BEVEL); sprite.filters = filters; // Create the piece sprite var p:PieceSprite = new PieceSprite(id, mask); // Create the group var group:Group = new Group(p); ret.push(group); p.setGroup(group); //if ( j == (rows-1) && i < 2 ) addChild(p); p.init(i, j, sprite); sprite.x = (Math.random()*1000000)%280 + 110; sprite.y = (Math.random()*1000000)%250 + 110; } } // Assign the 'right' neighbors // for each piece PieceSprite.initNeighbors(); m_clean = false; return ret; }
Dragging of pieces
Dragging a sprite around the stage is quite straightforward. How about moving around a bunch of sprites at the same time? In this implementation of the game, we don't create a new sprite once two or more pieces are matched, but they are kept as separate sprites. However, we need to move the whole bunch when any one piece belonging to the group is moved by the player.
[ 161 ]
Multiplayer Game Example: Jigsaw
We start by adding the mouse event listener to the PieceSprite class. When a mouse down is detected, we record the current position of the piece. private function mouseDownHandler(event:MouseEvent):void { //trace("mouseDownHandler"); //var sprite:Sprite = Sprite(event.target); PieceCutter.getInstance().bringToFront(this); m_sprite.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler); m_dsx = m_sprite.x; m_dsy = m_sprite.y; m_sprite.startDrag(); }
We also subscribe to the mouse move event. private function mouseMoveHandler(event:MouseEvent):void { //trace("mouseMoveHandler"); var sprite:Sprite = Sprite(event.target); m_group.move((m_sprite.x-m_dsx), (m_sprite.y-m_dsy), this); m_dsx = m_sprite.x; m_dsy = m_sprite.y; event.updateAfterEvent(); }
In the mouse move handler, we call the move method on the group that will move all the pieces in the group by the delta, which is the difference between the sprite's current position and the last recorded position (m_dsx and m_dsy). After we have moved the sprites, we reset the m_dsx and m_dsy to the current position. The move method in the Group class is shown as follows: public function move(dx:Number, dy:Number, skip:PieceSprite):void { var i:int=0; //trace("Group move: " + m_pieces.length); for ( i=0; i<m_pieces.length; i++ ) { var p:PieceSprite = m_pieces[i]; if ( skip != null && p == skip ) continue; p.move(dx, dy); } }
Note that we skipped the piece that is being held by the player, as Flash takes care of moving it for us already. [ 162 ]
Chapter 7
Checking for matches
How do we determine if two pieces were matched by a player? The way it is done in this implementation is quite straightforward. Each piece already knows which piece is supposed to be on top of it, on the bottom, and on its sides. When a piece is moved, we simply check if any of these pieces are close enough within tolerance. Note that if the dragged piece belongs to a group, then we need to go through every piece in the group to check whether, for each piece, it got close enough to its correct neighboring piece. The check for a match gets triggered when the player moves a piece, or more specifically, when the player lets go of the piece (mouse up event) at the end of dragging it. As noted earlier, when two pieces are matched, the groups the pieces belong to are merged. It could happen that more than two groups are merged at the same time. Let's take a look at the mouse up event handler on the PieceSprite class: private function mouseUpHandler(event:MouseEvent):void { var sprite:Sprite = Sprite(event.target); sprite.removeEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler); sprite.stopDrag(); var finalGroup:Group = null; var mergeGroup:Group = m_group; // We merge as many groups there // are to be merged. // The final aggregated group is // pointed to by finalGroup while (mergeGroup != null ) { // returns non-null group // if there was a merge mergeGroup = mergeGroup.checkMatch(); if ( mergeGroup != null ) { // save a reference to // the aggregated group finalGroup = mergeGroup; } } // Piece match effect if ( finalGroup != null ) { _showEffect(finalGroup); } }
[ 163 ]
Multiplayer Game Example: Jigsaw
Note that we start with the piece's group and then call checkMatch. The check match will return a merged group if there was a merge with another group. We keep a reference to the merged group in finalGroup. We then call the checkMatch with the last merged group again to see if there is another group to merge with. We do this until there are no more groups to be merged. Let's see what happens in the checkMatch method inside the group class: public function checkMatch():Group { var ret:Group = null; var i:int=0; var len:int = m_pieces.length; // go through each piece and check if // piece was merged with a piece from // another group. for ( i=0; i
The method goes though each piece in the group and calls the checkMatch method on the piece. The checkMatch will return the merged group if the groups merge. Let's follow the checkMatch on the PieceSprite class: public function checkMatch():Group { var mergeGroups:Array = new Array(); // returns all the groups that were // merged when a group was moved _checkMatch(mergeGroups) var var var for
merge:Boolean = false; finalGroup:Group = m_group; i:int; (i=0; i<mergeGroups.length; i++ ) { var g:Group = mergeGroups[i]; // probably already merged if ( g.getSize() == 0 ) continue; // avoid merging with self or // final merged group if ( finalGroup == g ) continue; [ 164 ]
Chapter 7 merge = true; finalGroup = m_group.merge(finalGroup, mergeGroups[i]); } if ( merge ) { return finalGroup; } else { // no new group was formed return null; } }
The crucial logic is in the _checkMatch method, which checks if there are any correct pieces around it that are close enough. If so, the group is added to the returned array. We then actually merge the groups into one group and return it. If there were no groups to merge, we return null. In each direction, we calculate the difference—that is, how close the correct piece is to this piece. If it is within the tolerance (MATCH_ALLOWANCE) that is seven pixels, we move the group exactly to the pixel so that they appear to be attached. The following listing shows the listing for the _checkMatch method that deals with the correct top piece: public function _checkMatch(merge:Array):Boolean { var ret:Boolean = false; var diff:Point = null; if ( !m_topMatch && (m_top != null) ) { diff = _getDiff(DIR_TOP); if ( Math.abs(diff.x) < MATCH_ALLOWANCE && Math.abs(diff.y) < MATCH_ALLOWANCE ) { ret = true; m_topMatch = true; m_top.getGroup().move(diff.x, diff.y, null); _sendPieceMatch(DIR_TOP); merge.push(m_top.getGroup()); } }…
The exact same logic is applied for the other three directions.
[ 165 ]
Multiplayer Game Example: Jigsaw
Multiplayer and networking
Interestingly, we don't try to keep individual piece positions synchronized across players. We only broadcast a message to all the players in the room when two or more pieces were matched by any of the players. This implementation also does not change the piece's or group's movement in real time, although this could be done. Imagine if all the pieces moved around because of other players; it would just be plain distracting. We only send the piece ID and the direction in which the match was made. If two groups of pieces were matched, then for each matching edge we would send a message. The Schema file for the game is listed below: <property name="pieceId" type="int"/> <property name="dir" type="int"/>
The schema file defines one game state called PieceMatch, which contains two properties as follows: •
pieceId—Uniquely identifies the piece
•
dir—The direction, four possible values
As we can see, the information exchanged between the players within the room is quite simple. Let's now look at the code that is responsible for sending this information to other players and the code that receives it and how it is dealt with. When the player moves a piece or a group of pieces and if we find that two pieces were matched (refer _checkMatch method), then we call the _sendPieceMatch method as follows: private function _sendPieceMatch(dir:int):void { // Create a new instance of the game state var gs:PieceMatchClient = new PieceMatchClient(); [ 166 ]
Chapter 7 // Assign a unique key gs.setStateKey(""+m_id+dir); gs.setStateType(GameConstants.GS_IS_UNIQUE); // Set the properties gs.setPieceId(m_id); gs.setDir(dir); // Add it to the room and distribute to // other players var gc:GameClient; gc = PulseGame.getInstance().getGameClient(); gc.addGameState(gs); }
The method creates the game state object, initializes the properties, and sends it to the server. In this implementation, there are two important things to note: the first is that all game states are unique, and the second is that they are added instead of just broadcast to all the players. When a game state is added, the server persists the game states until the game is over. This persistence allows players to join the game room after the game has already started. The newly joined game player will first receive all the game states that have been added by all players. So what happens after the client sends the piece match game state to the server? Since for each of our piece match game states we set a unique key property, the server first checks if a previous game state with the same key was already added in the game. If one was already found, the server rejects the new add request and sends back a notification to the sender. If no previous game state was found with the same unique key, then the game state is added to the persistent cache and it is broadcast to all the players in the room. Note that the client who requested the addition of the new game state will also receive back the game state. Let's examine the code that handles the game state when it arrives at a client. The notification is first handled by the main class and then passes the information to the onPieceMatch method. This method simply gets the piece in question and then calls _onPieceMatch method on it. public static function onPieceMatch(pid:int, dir:int):void { var p:PieceSprite; p = getWithId(pid); p._onPieceMatch(pid, dir); }
The onPieceMatch will simply bring the piece and the correct piece in the direction close enough so that they seem attached, and the groups of the two pieces are also merged together. [ 167 ]
Multiplayer Game Example: Jigsaw
The following code snippet shows the top direction case. Similar logic is implemented for the other three directions. public function _onPieceMatch(pid:int, dir:int):void { var os:Sprite = null; var dx:Number, dy:Number; var tx:Number; var ty:Number var otx:Number var oty:Number; var diff:Point = null; switch(dir) { case DIR_TOP: if ( !m_topMatch ) { // mark top is matched m_topMatch = true; // we move the group // that is smaller! if ( m_group.getSize() >= m_top.getGroup().getSize() ) { // group of this piece is smaller // than the other // get the amount of pixels // to move diff = _getDiff(DIR_TOP); m_top.getGroup(). // move it! move(diff.x, diff.y, null); } else { // The other group is smaller // get the difference in // position in pixels diff = m_top._getDiff(DIR_BTM); // move the other group m_group.move(diff.x, diff.y, null); } // merge the groups & // show the glow effect. _showEffect(m_group.merge(m_group, m_top.getGroup())); } break;
[ 168 ]
Chapter 7
Code generation
After having designed the game states that are needed for the game, we need to run the XML schema file through the PulseCodeGen tool. The following is a listing of a batch file that will do exactly that. The batch file can be saved to the project root folder. You may need to modify the paths in the batch file as required, as your directory structure may be different than what is presented here. REM *************** init.bat *************** @ECHO OFF IF EXIST .\src\jig\gsrc\client del .\src\jig\gsrc\client\*.as CALL "%GAMANTRA%"\bin\PulseCodeGen.bat .\JigsawSchema.xml jig.gsrc .\ src\jig\gsrc IF NOT %ERRORLEVEL% == 0 GOTO ERROR ECHO Success! GOTO END :ERROR ECHO oops! :END pause REM ************ end of init.bat ************
The previous batch file is convenient if you change the schema; the regeneration of source files can be quickly regenerated by simply running it again.
Screen classes
Once we have generated the class files based on the schema we designed, we need to prepare some of the game screens for the game. For this we will leverage from the screen classes, which are already available to us from the PulseUI package. We simply need to subclass from these classes as shown next:
[ 169 ]
Multiplayer Game Example: Jigsaw
The JigsawGame class
This is the main class where the game code begins. This is also the place where we tell the Pulse UI of the game about the specific classes that are being overridden, for example, the following three classes.
Overriding the constructor
The constructor is a good place to instantiate the game-specific skinner class; more on implementing the skinner class is discussed later. We also set the super-class defined as s_instance to this, so any reference to the singleton points is to our subclass instead of the parent. public function JigsawGame() { s_instance = this; new JigsawSkinner(); super(); }
Override the getGameId method from PulseGame as follows: public override function getGameId():String { return "Jigsaw-MP"; }
The Pulse runtime needs a unique game ID for each game, since many games could be hosted on a single server. This ID is also used by the server to match other players playing the same game. This means players of jigsaw will not be able to go into a room that is playing tic-tac-toe.
Overriding the initNetClient method
This is the main API class instance (m_netClient) through which we send all the game states to the server, which are then broadcast to all the players in the room. import jig.gsrc.client.GNetClientObjectFactory; protected override function initNetClient():void { var factory:GNetClientObjectFactory; factory = new GNetClientObjectFactory(); m_netClient = new GameClient(factory, this); }
[ 170 ]
Chapter 7
A factory object must be instantiated as shown previously and should be assigned to m_netClient property. The factory class is one of the classes that is created under the gsrc folder for us during code generation. The m_netClient is a protected property of the PulseGame parent class. We will also override two more methods; one is for the game screen where the jigsaw game will be played, and the other is for the new game screen where a new room will be created by the player. protected override function initGameScreen():void { m_gameScreen = new JigsawGameScreen(); m_gameScreen.init(); } protected override function initNewGameScreen():void { m_newGameScreen = new NewJigsawGameScreen(); m_newGameScreen.init(); }
Server communication
Since the parent class (PulseGame) implements the Pulse SDK's GameClientCallback, all the notifications are processed by the parent class. For this game, we are interested in knowing when a new (PieceMatch) game state is added. public override function onNewGameState(gameState:GameStateClient):void { var gs:PieceMatchClient; gs = gameState as PieceMatchClient; trace("Received new game state."); trace("sender: " + gs.getSenderId() + " Id: " + gs.getId() + " PieceId: " + gs.getPieceId() + " Direction: " + gs.getDir()); // Up the score of the sender. var av:GameAvatarClient; av = m_netClient.getPlayer(gs.getSenderId()); if ( av == null ) return; // cannot find player!? av.setScore(av.getScore()+1); m_netClient.publishMyAvatar(); if ( gs.getSenderId() != m_netClient.getMyAvatar().getId() ) { PieceSprite. onPieceMatch(gs.getPieceId(), gs.getDir()); } } [ 171 ]
Multiplayer Game Example: Jigsaw
We override the parent class' implementation onNewGameState and insert what is required for this game. First, we figure out which player has successfully added the game state (matched a piece), then incremented the score for the player, and then call the onPieceMatch method to visually update the screen so as to make the pieces appear attached.
The JigsawSkinner class
We need to subclass the skinner to read in the image (skin) files that will be used to draw all the UI for all of the screens. public class JigsawSkinner extends Skinner { [Embed(source="jig\\rsrc\\Outline.png")] private static var OutlineClass:Class; [Embed(source="jig\\rsrc\\ui.png")] private static var UIClass:Class; [Embed(source="jig\\rsrc\\frame.png")] private static var FrameClass:Class; public function JigsawSkinner() { } protected override function load():void { m_outline = (new OutlineClass() as BitmapAsset). bitmapData; m_ui = (new UIClass() as BitmapAsset).bitmapData; m_frame = (new FrameClass() as BitmapAsset).bitmapData; } }
The custom skinner is quite simple. The previous code is the complete listing for the class. The load method should be overridden to create BitmapAsset for the outline, ui, and frame. These are protected fields defined in the Skinner class. The Skinner object will do all the hard work of cutting up the asset and piecing it altogether during runtime.
[ 172 ]
Chapter 7
The NewJigsawGameScreen class
It is optional to subclass this for this game, however, we need to tell the Pulse server the specific properties of the game room, such as whether its turn-based, has a maximum of two players per room, and so on. protected override function createNewGameRoom():void { // Create the new Game room object var room:GameRoomClient = new GameRoomClient(); // Set the name of the room from what is entered // in the text field room.setRoomName(m_ti.text); // Limit maximum players that can // enter the room to three room.setMaxPlayerCount(3); // Let players join the game // even after it is started room.setRoomType(GameConstants.ROOM_ALLOW_POST_START); // set the player to be in ready // state as soon as they enter the room room.setAutoReady(1); // Send a request to the server // to create the game room PulseGame.getInstance(). getGameClient().createGameRoom(room); }
The new game screen has to be customized only for the purpose of defining the kind of room that is needed for the game. The previous code is specific to the multiplayer jigsaw game. The code creates a new room object instance, specifies the player to be automatically set to ready when they join the room, sets up the room name for others to see in the lobby, allows a maximum of three players into the room, specifies that the room is turn-based, and finally requests the server to create the room. We also set the room type so that new players may join the game midway. Once created on the server, the room will appear in the lobby screen for other players to join.
[ 173 ]
Multiplayer Game Example: Jigsaw
The JigsawGameScreen class
Lastly, there is the class where all the action happens inside of the game room, once the game has started. This class contains a few important objects that are required to manage the puzzle pieces that we saw earlier in the chapter. The DisplayManager instance that shows the group management and finish logic, a MiniMap instance that shows the miniature image of the puzzle, and an image that is shown once the puzzle is completed are all initialized during the init method. The show method adds all the above three sprite objects as child, so they are ready to be displayed. The hide method cleans up everything and removes the sprites from the screen. When the game is started by the game host, the startGame method is called, where we create the pieces and groups as well as display the mini-map. The implementation in this class is simple, as all the hard work is done by the class we saw earlier, such as DisplayManager, PieceSprite, Group, and so on. Here is the complete listing of the class: package jig.ui { import flash.display.BitmapData; import import import import import
jig.JigsawGame; jig.game.DisplayManager; jig.game.MiniMap; jig.game.PuzzlePix; jig.game.Mask;
import mx.core.BitmapAsset; import pulseui.GameScreen; public class JigsawGameScreen extends GameScreen { private var bmd:BitmapData; [Embed(source="jig\\rsrc\\ml.jpg")] private var MLClass: Class; // The puzzle manager private var m_displayManager:DisplayManager; // To show the puzzle image private var m_miniMap:MiniMap; //private var m_playBtn:PlayBtnSprite; // A completed puzzle image private var m_pp:PuzzlePix; [ 174 ]
Chapter 7 public function JigsawGameScreen() { super(); } public override function init():void { super.init(); // initialize the masks Mask.init(); // create the minimap m_miniMap = new MiniMap(); // Init the PuzzlePix m_pp = new PuzzlePix(); m_displayManager = new DisplayManager(); } public override function show():void { super.show(); addChild(m_miniMap); addChild(m_pp); addChild(m_displayManager); } public override function hide():void { super.hide(); m_displayManager.clearAll(); m_miniMap.clear(); removeChild(m_miniMap); m_pp.removeFromStage(); removeChild(m_pp); removeChild(m_displayManager); } public override function startGame():void { super.startGame(); startPuzzle(); } private function startPuzzle():void { var img:BitmapAsset = null; img = new MLClass() as BitmapAsset; m_displayManager.startPuzzle(img.bitmapData); setupStage(img); } private function setupStage(img:BitmapAsset):void { m_pp.init(img.bitmapData); m_miniMap.init(img); } } }
[ 175 ]
Multiplayer Game Example: Jigsaw
When you run the jigsaw sample, it is complete with chat, lobby display, friend making, top-ten, or registration. Because we leverage the game code from PulseUI, all these features are readily available without any additional code on our part.
Summary
In this chapter, we walked though a lot of code implementing the multiplayer Jigsaw. We saw how the pieces are formed, and how we check if two pieces are matched; if it was a match, we have them stick together. We also saw the multiplayer aspects of the game, specifically how we communicate the piece matching to the other players. We saw how unique game state comes in handy for this game. We looked at leveraging the PulseUI framework; we were able to focus solely on the game implementation leaving the rest of the multiplayer game necessities to the default behavior of the framework. Mainly we saw how we can use the game state actions to exchange information between the players to implement a game like the multiplayer jigsaw.
[ 176 ]
Card-based Racing Game Tutorial In this chapter, we will look at the implementation of a board game called Schildkroten-Rennen designed by Reiner Knizia. In the original game, where each player race a colored tortoise to the finish line. In this implementation, we use frogs instead of tortoises. In this chapter, we will learn the following: •
Graphics programming for the game
•
Multiplayer implementation design
•
Multiplayer card distribution
•
Game play implementation
•
Game UI screen implementation
Jump Jump Frog is a card-based racing game. Each player has a frog of a specific color that must reach the finish line first in order to win the game. Specifically, we will see how a deck of cards is distributed among players, making sure only one player holds a specific card in the deck without the aid of custom server coding. For this we will rely on the unique game state feature available in Pulse. There are five colored frogs, so there can be at most five players racing at a time. However, there could be as few as two players playing the game, but there would still be five colored frogs on the race track. There are ten spots that the frogs need to jump in order to reach the final spot. Frogs in the same spot will be stacked on top of one another.
Card-based Racing Game Tutorial
When the frogs jump from one spot to the other, it carries along all the frogs on top of it. The frog may move a step closer towards the finish or backwards towards the start. To make the game interesting, each player is unaware of the other player's color. The frogs move either forward or backward based on the card that a player plays. Each player gets three cards at the beginning of the game. A player may discard the card or play it to affect any of the frogs in the race. When a player plays or discards one of the three cards in hand, he or she gets a fresh card from the deck, so the player always holds three cards in hand. In this implementation of the game, when one of the frogs wins, the race starts again, with each player getting a randomly new color and the race starting from spot 0. The initial position of the frogs is randomly assigned, but they all begin at spot 0. The winning player gets a score of 1.
Implementation
We will first walk through the graphics part of the implementation. We then will see how the cards in the game are managed and then finally how we can put it all together to make it a multiplayer game based on Pulse API.
Graphics
The game source is organized in the following way:
[ 178 ]
Chapter 8
jjf is the root package under which we find game, which contains the sources for card management and visual movement of the frogs on the map. The graphics package is a simple embedding of the resources found in the rsrc folder. The gsrc is the generated class file from the Pulse modeler. Finally, the ui package contains the screen management for the game.
The map and frog movement
Let us first look at the Map class. It essentially draws the map background, the steps, and also the frogs in the map. In this implementation, there is no background image—it is empty. The map is a sprite, which is added to the stage by the game screen class that we will visit later. The 10 spots are added as children of this instance as well as the five frogs.
The map contains 10-step instances for each step that is seen by the player. There are five frog instances, one for each color. In the constructor, we call addStep, a private method that draws a step at a predetermined position. The addFrog method is called to put a frog within the map.
[ 179 ]
Card-based Racing Game Tutorial
For each step, the class maintains an array to keep track of each frog. This array m_track is two dimensional. For example, m_track[3][2] points to the frog on the fourth step and third from the bottom.
When a frog moves to another spot, we simply move the frog to a different position array and we also move the frogs on top of it. The position of the frogs after they move to the new spot may change depending on whether there are any frogs in the new spot. When a frog is initially added to the map, it is always added to the spot (step) 0. The position of each frog is determined in the order they are added. This order of adding each frog is random. public function addFrog(frog:Frog):void { (m_track[0] as Array).push(frog); frog.step = 0; frog.pos = ((m_track[0] as Array).length - 1); m_frogs.push(frog); addChild(frog); }
[ 180 ]
Chapter 8
The step class
The step class is very simple. It is a sprite representation of the 2D art asset. The following is the source code for the class: package jjf.game { import flash.display.BitmapData; import flash.display.Sprite; import mx.core.BitmapAsset; public class Step extends Sprite { public static var WIDTH:int; [Embed(source="jjf\\rsrc\\step.png")] public static const StepClass: Class; public function Step() { super(); var bma:BitmapAsset; bma = new StepClass() as BitmapAsset; var bmd:BitmapData = bma.bitmapData; graphics.beginBitmapFill(bmd); graphics.drawRoundRect(0, 0, bmd.width, bmd.height, 7, 7); graphics.endFill(); WIDTH = bmd.width; } }
The frog class
The Frog class, in addition to drawing itself depending on its spot and position in the race, updates its position coordinates on the screen. The m_spot property determines which of the 10 spots the frog is currently at. If there is more than one frog in the spot, the m_position property determines at which position the frog is at; 0 being the bottommost. With any update to m_spot or m_position, the x and y of the frog is also updated.
[ 181 ]
Card-based Racing Game Tutorial
The m_color determines which color the frog represents. The color constants are defined in the Color class: package jjf.game { public class Color { public static const public static const public static const public static const public static const
BLUE:int GREEN:int RED:int PURPLE:int YELLOW:int
= = = = =
10; 20; 30; 40; 50;
public function Color() { } } }
The following is the complete listing of the Frog class: package jjf.game { import flash.display.BitmapData; import flash.display.Sprite; import flash.events.MouseEvent; import flash.geom.Point; public class Frog extends Sprite { private static var HEIGHT:int; private var m_color:int; // determines where this // frog is on the track private var m_step:int; // 0 is the bottom most private var m_pos:int; public function Frog(color:int, map:Map, bmd:BitmapData) { super(); m_color = color; graphics.beginBitmapFill(bmd); graphics.drawRoundRect(0, 0, [ 182 ]
Chapter 8 bmd.width, bmd.height, 7, 7); graphics.endFill(); HEIGHT = bmd.height; } public function get color():int { return m_color; } public function set step(step:int):void { m_step = step; var p:Point; p = Map.spotPos[m_step] as Point; x = p.x; } public function get step():int { return m_step; } public function set pos(pos:int):void { m_pos = pos; var p:Point; p = Map.spotPos[step] as Point; y = p.y - ((HEIGHT-10)*m_pos) ; } public function get pos():int { return m_pos; } }
Card management
The game consists of 35 cards. Each card in the game is implemented as the Card class. Each card has a unique ID (m_id) value. The color (m_color) determines the frog it affects; m_value is the number of steps the frog will move, either -1, +1, or +2. Each card also listens to the mouse events, specifically MOUSE_OVER, where we make card appear bigger and MOUSE_OUT where we resize it back to normal. The MouseClick is where we pass clicked on card to the manager. The mouse click means the player wants to play this card or discard it. Here is the full listing of the class Card: package jjf.game { [ 183 ]
Card-based Racing Game Tutorial import flash.display.BitmapData; import flash.display.Sprite; import flash.events.MouseEvent; import pulse.gsrc.client.GameStateClient; public class Card extends Sprite { private var m_id:int; private var m_color:int; private var m_value:int; private var m_mgr:CardManager; private var m_gs:GameStateClient; public function Card(id:int, color:int, value:int, bmd:BitmapData, mgr:CardManager) { super(); m_id = id; m_color = color; m_value = value; m_mgr = mgr; graphics.beginBitmapFill(bmd); graphics.drawRoundRect(0, 0, bmd.width, bmd.height, 7, 7); graphics.endFill(); addEventListener(MouseEvent.MOUSE_OVER, onMouseOver); addEventListener(MouseEvent.MOUSE_OUT, onMouseOut); addEventListener(MouseEvent.CLICK, onMouseClick); buttonMode = true; } public function get id():int { return m_id; } public function get color():int { return m_color; } public function get value():int { return m_value; } private function onMouseOver(e:MouseEvent):void { width = width+10; height = height+10; [ 184 ]
Chapter 8 } private function onMouseOut(e:MouseEvent):void { width = width-10; height = height-10; } private function onMouseClick(e:MouseEvent):void { m_mgr.playCard(this); } public function get gameState():GameStateClient { return m_gs; } public function set gameState(gs:GameStateClient):void { m_gs = gs; } } }
All cards are managed by the CardManager class. Each card is first put into the deck (m_deck) array property. The m_myHand array consists of cards that belong to the player. The m_others array consists of cards that belong to other players. Cards are moved from one array to the other by calling the appropriate method. For example, toMyHand moves the card from the deck to the myHand array. The allToDeck method will move all the cards in myHand and in others to the deck and so on. Here is the full listing of the CardManager class: package jjf.game { import flash.display.BitmapData; import jjf.graphics.Asset; import jjf.ui.Game; public class CardManager { private var m_nextId:int=1; private private private private
var var var var
m_game:Game; m_deck:Array = new Array(); m_myHand:Array = new Array(); m_others:Array = new Array();
private var m_colors:Array = new Array(); [ 185 ]
Card-based Racing Game Tutorial public function CardManager(game:Game) { m_game = game; // build up the deck makeCards(-1, Color.BLUE, Asset.getBlueMinus(), 2); makeCards(1, Color.BLUE, Asset.getBluePlus(), 4); makeCards(2, Color.BLUE, Asset.getBluePlusPlus(), 1); makeCards(-1, Color.RED, Asset.getRedMinus(), 2); makeCards(1, Color.RED, Asset.getRedPlus(), 4); makeCards(2, Color.RED, Asset.getRedPlusPlus(), 1); makeCards(-1, Color.GREEN, Asset.getGreenMinus(), 2); makeCards(1, Color.GREEN, Asset.getGreenPlus(), 4); makeCards(2, Color.GREEN, Asset.getGreenPlusPlus(), 1); makeCards(-1, Color.PURPLE, Asset.getPurpleMinus(), 2); makeCards(1, Color.PURPLE, Asset.getPurplePlus(), 4); makeCards(2, Color.PURPLE, Asset.getPurplePlusPlus(), 1); makeCards(-1, Color.YELLOW, Asset.getYellowMinus(), 2); makeCards(1, Color.YELLOW, Asset.getYellowPlus(), 4); makeCards(2, Color.YELLOW, Asset.getYellowPlusPlus(), 1); shuffle(); // create the color cards var cc:ColorCard; cc = new ColorCard(Color.BLUE, Asset.getBlue()); m_colors.push(cc); cc = new ColorCard(Color.RED, Asset.getRed()); m_colors.push(cc); cc = new ColorCard(Color.GREEN, Asset.getGreen()); m_colors.push(cc); cc = new ColorCard(Color.PURPLE, Asset.getPurple()); m_colors.push(cc); cc = new ColorCard(Color.YELLOW, Asset.getYellow()); m_colors.push(cc); } public function traceCount():void { trace("Deck: " + m_deck.length); trace("MyHand: " + m_myHand.length); trace("Others: " + m_others.length); } public function shuffle():void { var i:int; var card:Card; [ 186 ]
Chapter 8 // shuffle the cards for ( i=0; i<1000; i++ ) { var r:int; r = (Math.random()*1000)%m_deck.length; card = m_deck[r]; m_deck.splice(r, 1); m_deck.push(card); } } public function copyColors():Array { var ret:Array = new Array(); var c:ColorCard; for each ( c in m_colors ) { ret.push(c); } return ret; } public function getColor(color:int):ColorCard { var i:int, imax:int = m_colors.length; var cc:ColorCard; for ( i=0; i
Card-based Racing Game Tutorial public function getCardInHand(cid:int):Card { return getCardIn(m_myHand, cid); } public function getCardInOthers(cid:int):Card { return getCardIn(m_others, cid); } private function getCardIn(array:Array, cid:int):Card { var card:Card = null; for each ( card in array ) { if ( card.id == cid ) return card; } return null; } private function removeCardIn(array:Array, cid:int):Card { var card:Card = null; var i:int, imax:int = array.length; for ( i=0; i
Chapter 8 // and move to my hand var card:Card=null; card = removeCardIn(m_deck, cid); if ( card != null ) m_myHand.push(card); } public function toOthers(cid:int):void { // Remove from deck // and move to others var card:Card=null; card = removeCardIn(m_deck, cid); if ( card != null ) m_others.push(card); } public function toBottom(cid:int):void { // move the card to the bottom // of the deck. // from either myHand or from others var card:Card=null; card = removeCardIn(m_myHand, cid); if ( card == null ) card = removeCardIn(m_others, cid); if ( card != null ) m_deck.push(card); } public function playCard(c:Card):void { m_game.playCard(c); } } }
We will see how the card distribution is implemented in the Multiplayer design section of this chapter.
[ 189 ]
Card-based Racing Game Tutorial
Screen management
As with other samples based on Pulse, we will subclass the minimum required classes from PulseUI to implement the game screen. The subclasses are as follows:
Class Skin
The Skin class is subclassed from the Skinner class in PulseUI to provide the UI elements for the game. Here is the full listing: package jjf.ui { import mx.core.BitmapAsset; import pulseui.Skinner; public class Skin extends Skinner { [Embed(source="jjf\\rsrc\\Outline.png")] private static var OutlineClass:Class; [Embed(source="jjf\\rsrc\\ui.png")] private static var UIClass:Class; [Embed(source="jjf\\rsrc\\frame.png")] private static var FrameClass:Class; public function Skin() { super(); } protected override function load():void {
[ 190 ]
Chapter 8 m_outline = (new OutlineClass() as BitmapAsset).bitmapData; m_ui = (new UIClass() as BitmapAsset).bitmapData; m_frame = (new FrameClass() as BitmapAsset).bitmapData; } } }
Class JJF
This is the main class for the game and it inherits from the PulseGame in PulseUI framework. In addition to the methods we normally override (please refer to examples in previous chapters), we need to override the methods shown next. Since the PulseGame implements the GameClientCallback, it handles any updates regarding the game state notifications from the server. In our implementation, we simply override them and delegate the notifications to the game screen class. public override function onNewGameState(gameState:GameStateClient):void { // delegate to game screen (m_gameScreen as Game).onNewGameState(gameState); } public override function onGameStateAction(gameState:GameStateClient):void{ // delegate to game screen (m_gameScreen as Game).onGameStateAction(gameState); } public override function onGameStateError(gameState:GameStateClient):void{ // delegate to game screen (m_gameScreen as Game).onGameStateError(gameState); } public override function onPlayerTurn():void { // delegate to game screen (m_gameScreen as Game).onPlayerTurn(); }
[ 191 ]
Card-based Racing Game Tutorial
Class NewGame
This class is inherited from PulseUI's NewGameScreen. We override the createNewGameRoom method to create a new game room object as follows: protected override function createNewGameRoom():void { // Create the new Game room object var room:GameRoomClient = new GameRoomClient(); // Set the name of the room from what is entered // in the text field room.setRoomName(m_ti.text); // Limit maximum players that can // enter the room to three room.setMaxPlayerCount(5); // set the player to be in ready // state as soon as they enter the room room.setAutoReady(1); // its a turn based game room.setRoomType(GameConstants.ROOM_TURN_BASED); // Send a request to the server // to create the game room PulseGame.getInstance(). getGameClient().createGameRoom(room); }
Multiplayer design
In order to implement this game, we first need to think of what we need to communicate between the clients in a room. In the case of a client-server implementation, the card distribution is simple; the server will simply send a message to each player informing them of the cards that they should start with. And when a player plays or discards a card in hand, the card will be moved to the bottom of the deck and a new card from the top of the deck will be sent to the player. But in Pulse-based games, we need to control the distribution without the help of the custom central server logic. In order to implement the card distribution, we would need the help of the unique game state feature available in Pulse.
[ 192 ]
Chapter 8
Unique game states are just like normal games states, but with a key. When any one player adds a game state with a key, no other player can add a game state with the same key. If a subsequent game state is added with the same key, the add operation will fail and the sender is notified. So even if two players add a game state with the same key at the same time, the Pulse server guarantees that only one will succeed and the other will receive an error.
Card distribution
Among the 35 cards, we need to make sure that in our multiplayer implementation a card is only at one place—either in the deck or with one of the players. After we create the necessary card in card manager, we also shuffle the cards. Note that each client does this operation independently. So the order of the cards in the deck in each player will be different. At the start of the game, each player needs to start with three cards. To implement this, we use the unique game states. Note that, the ID of each card it represents is the same in all clients.
From the previous flow-chart, we can see that there are three entry points into getting three cards into player's hand. The first one is when the game is set to start. We get a card from the local deck and create a unique game state with the key set to the card ID. The card ID is unique and the ID that represents the card is the same on all the clients. However, after shuffling the initial deck, the order may be different within each client. Because of different orders on different clients, the conflict of two players getting the same card is also minimized. The top card of the deck is at index 0. [ 193 ]
Card-based Racing Game Tutorial
One of the events may happen after the client requests to add a unique game state—it may either succeed or fail. If it fails, then it means that another player has already added the card to his or her hand; in this case, the client requesting will get an error message via onGameStateError. We simply move the card with the ID to the other's array and we check if we have the three cards in the myHand array. If not, we get a card from the deck and attempt to add the unique game state. If we receive onNewGameState, we first check if the sender of this is self. If so, it means that the client successfully added the game state and the card now belongs to his or her hand. If the sender was not self, then we add it to the other's array.
Frog position
It is required that all the frogs start at position 0. But how do we make sure that all frogs are at the same position in all the clients? To do this, we let the host determine the starting position of the frogs. Upon starting the game, the client who is the game host will send out the position information to all the players in the room and each clients will then move or add the frog to that position.
Assigning player color
Before the game can begin, each player must get a frog with a color. There are only five colors and no two players can get the same color. There are two ways to do this: one is having the host assign colors to everyone and the other is similar to how we distribute the hand cards. In this implementation, we have chosen to implement it the same way as the hand-card distribution. Now the game is ready to begin!
Schema
In order to achieve all the above implementation, we first need to define the necessary game states in the schema file. Here is the schema file for the game: <property name="color" type="int"/> [ 194 ]
Chapter 8 <property name="cardId" type="int"/> <property name="cardId" type="int"/> <property name="discard" type="byte"/> <property name="frog" type="int"/> <property name="position" type="int"/>
This needs to be in the root file of the game project folder along with the init batch file as shown below: @ECHO OFF IF EXIST .\src\jjf\gsrc\client del .\src\jjf\gsrc\client\*.as CALL "%GAMANTRA%"\bin\PulseCodeGen.bat .\Schema.xml jjf.gsrc .\src\ jjf\gsrc IF NOT %ERRORLEVEL% == 0 GOTO ERROR ECHO Success! GOTO END :ERROR ECHO oops! :END Pause
The paths in the batch file may need to be modified as required on your local project folder structure. Let us examine each of the game states defined in the schema file: •
PlayerColor is used for assigning the color to the frog of each player at the
start of the game. This game state is added as a unique game state.
[ 195 ]
Card-based Racing Game Tutorial
•
Frog is used to communicate each frog's initial position, 0 being the
•
HandCard game state represents one of the cards. This game state is added by a player in order to acquire a card in hand. Note that this game state is used (added) as a unique game state.
•
PlayerAction is a game state that is broadcast when the player plays or
bottommost. All frogs start at spot zero. This game state is added as a normal game state. Property frog is the color of the frog. At the start of the game, the host adds one game state for each frog. This is how the frog positions are synchronized across all clients.
discards a card. This is used as a game state action in the game.
Gameplay implementation
In this last section, we will see how the game states we defined are put into action to implement the game. The following are the properties of the game screen (Game) class: private var m_mgr:CardManager; private var m_pulse:GameClient; private var m_frogs:Array; private var m_colors:Array; private var m_myColor:ColorCard; public var m_map:jjf.game.Map; public var m_discard:Discard; public var m_countDown:CountDownTimer; private var m_endRound:Boolean;
The m_frogs property is an array of all the frogs. The m_colors property is an array of all colors. The m_myColor property is initialized after successfully acquiring one of the colors. Map is the class that is responsible for drawing the steps and frogs on the screen. The screen also displays a discard switch button for players to discard a card in hand instead of playing it; m_discard property is the sprite. When it is the player's turn, the player must make the move in 10 seconds. To implement this, a countdown timer is used. The countdown timer implementation is found in the util package of the PulseUI framework. The property m_endRound is true when the game is finished and turned to false when the game begins.
[ 196 ]
Chapter 8
The constructor and the init method are shown below: public function Game() { super(); } public override function init():void { super.init(); m_discard = new Discard(); m_countDown = new CountDownTimer(); m_countDown.addEventListener(CountDownTimerEvent.DONE, onTimerUp); m_countDown.x = 550; m_countDown.y = 230; m_pulse = PulseGame.getInstance().getGameClient(); m_mgr = new CardManager(this); m_frogs = new Array(); m_frogs.push(new Frog(Color.BLUE, m_map, Asset.getBlueRacer())); m_frogs.push(new Frog(Color.RED, m_map, Asset.getRedRacer())); m_frogs.push(new Frog(Color.PURPLE, m_map, Asset.getPurpleRacer())); m_frogs.push(new Frog(Color.GREEN, m_map, Asset.getGreenRacer())); m_frogs.push(new Frog(Color.YELLOW, m_map, Asset.getYellowRacer())); m_map = new Map(); }
Assigning colors
When the game begins, we first need to assign each player a different color. For this we call the request color within the start method: private function requestColor():void { var index:int; index = (Math.random()*1000)%m_colors.length; var color:ColorCard; color = m_colors[index]; var gsm:PlayerColorClient; gsm = new PlayerColorClient(); gsm.setStateKey("color-"+color.color); [ 197 ]
Card-based Racing Game Tutorial gsm.setColor(color.color); m_pulse.addGameState(gsm); trace("Requesting Color: " + color.color); }
The request color attempts to add a PlayerColor. We also set the state key so that the game state is treated as a unique game state. If we receive the PlayerColor game state and the sender ID is self, we set the color of this player. public function processPlayerColor(gameState:GameStateClient, error:Boolean):void { var color:PlayerColorClient; color = (gameState as PlayerColorClient); // if error then try another available color // ... // If not error // If mine, init color // else, remove the option if ( error ) { trace("Error on Color: " + color.getColor()); removeColor(color.getColor()); requestColor(); } else { if ( color.getSenderId() == m_pulse.getMyAvatar().getId() ) { m_myColor = m_mgr.getColor(color.getColor()); m_myColor.x = 550; m_myColor.y = 120; addChild(m_myColor); } else { removeColor(color.getColor()); } } }
The previous method is called from the onNewGameState method or from onGameStateError. The Boolean flag error determines the case. The onNewGameState implementation is shown next: public function onNewGameState(gameState:GameStateClient):void { var type:int; [ 198 ]
Chapter 8 type = gameState.getClassId(); switch (type) { case GNetClientObjectFactory.PLAYERCOLOR_CID: processPlayerColor(gameState, false); break; case GNetClientObjectFactory.HANDCARD_CID: processHandCard(gameState, false); break; case GNetClientObjectFactory.FROG_CID: processFrogPos(gameState); break; default: trace("[new]Unknown game state type!!! " + type); } }
The onGameStateError is shown below: public function onGameStateError(gameState:GameStateClient):void { var type:int; type = gameState.getClassId(); var error:int; error = gameState.getError(); switch (type) { case GNetClientObjectFactory.PLAYERCOLOR_CID: if ( error == GameErrors.ERR_GS_ADD_DUP_UNQ ) processPlayerColor(gameState, true); break; case GNetClientObjectFactory.HANDCARD_CID: if ( error == GameErrors.ERR_GS_ADD_DUP_UNQ ) processHandCard(gameState, true); break; default: trace("[err]Unknown game state type!!! " + type); } }
[ 199 ]
Card-based Racing Game Tutorial
Determining the initial frog positions
The frog positions are assigned by the game host during the start method. The start method is listed below: private function start():void { m_endRound = false; if ( m_pulse.isGameHost() ) { shuffleFrogs(); // send the frog positions var i:int; var f:Frog; for ( i=0; i<m_frogs.length; i++) { f = m_frogs[i]; var fp:FrogClient; fp = new FrogClient(); fp.setPosition(i); fp.setFrog(f.color); m_pulse.addGameState(fp); } } // Choose Color. m_colors = m_mgr.copyColors(); // Send a color request requestColor(); // Get first three cards getCard(); m_discard.x = 130; m_discard.y = 400; addChild(m_discard); }
When the Frog game state is received, we process it as follows: public function processFrogPos(gameState:GameStateClient):void { var gs:FrogClient; gs = gameState as FrogClient; var f:Frog; f = getFrogWithColor(gs.getFrog()); m_map.addFrog(f); }
[ 200 ]
Chapter 8
Getting the initial three cards
In the start method, notice that we make a call to the getCard method. The following is the listing of the getCard method. This method is called during the start method as well as after the player plays or discards a card during his or her turn. private function getCard():void { if ( m_mgr.getMyHand().length == 3 ) return; var c:Card; c = m_mgr.getTopCard(); if ( c == null ) { trace("No more cards in the deck!!"); m_mgr.traceCount(); } var req:HandCardClient; req = new HandCardClient(); req.setCardId(c.id); req.setStateKey(""+c.id); m_pulse.addGameState(req); }
The game always requires there to be three cards in the player's hand. If there are already three cards, we simply return from the method. If not, we get a card from the deck and attempt to put it on the player's hand. This attempt may fail, in which case the card already belongs to another player, or may succeed, in which case we move the card to player's hand and refresh the display. The process hand card is listed below: public function processHandCard(gameState:GameStateClient, error:Boolean):void { var gs:HandCardClient; gs = gameState as HandCardClient; var cid:int; cid = gs.getCardId(); // if error then try another card from deck // ... // If not error // If mine, put on screen, move to my hand // else, move card to others if ( error ) { m_mgr.toOthers(cid); getCard(); } [ 201 ]
Card-based Racing Game Tutorial else { if ( gs.getSenderId() == m_pulse.getMyAvatar().getId() ) { m_mgr.toMyHand(cid); var card:Card; card = m_mgr.getCardInHand(cid); card.gameState = gameState; refreshCards(); getCard(); } else { m_mgr.toOthers(cid); } } }
Playing the game
Upon the player's turn, we add a countdown timer. The player must make a move before the timer runs out. If the timer runs out, a card is played or discarded randomly. public function onPlayerTurn():void { if ( m_endRound ) return; addChild(m_countDown); m_countDown.start(10); // 10 seconds m_mgr.traceCount(); }
If the player plays or discards a card, the _playCard is called. The listing is shown below: private function _playCard(c:Card, discard:Boolean):Boolean { if ( !m_pulse.isMyTurn() ) return false; var d:Boolean var canPlay:Boolean = true; var f:Frog; f = getFrogWithColor(c.color); if ( !discard ) { if ( f.step == 0 && c.value == -1 ) { canPlay = false; } if ( f.step >= 9 && c.value == 1 ) { [ 202 ]
Chapter 8 canPlay = false; } if ( f.step >= 8 && c.value == 2 ) { canPlay = false; } } if ( canPlay ) { var pa:PlayerActionClient; pa = new PlayerActionClient(); pa.setCardId(c.id); pa.setDiscard(discard?1:0); m_pulse.sendGameStateAction(pa); m_countDown.stop(); if ( m_countDown.parent != null ) removeChild(m_countDown); m_pulse.nextTurn(); } return canPlay; }
If the timer runs out, the callback for the timer, onTimerUp, is called. We choose a random card from the hand trying to play it; if we cannot, then we simply discard the card. public function onTimerUp(e:Event):void { // auto play for the player // Play a random card // if cannot play, throw it. var rand:int; rand = (Math.random()*1000)%3; var card:Card; card = m_mgr.getMyHand()[rand]; if ( !_playCard(card, false) ) _playCard(card, true); }
If the player plays the card, the mouse event is handled by the card instance, passed to the card manager and then finally the following playCard method is called: public function playCard(c:Card):Boolean { var ret:Boolean; ret = _playCard(c, m_discard.discard()); m_discard.reset(); return ret; } [ 203 ]
Card-based Racing Game Tutorial
Summary
In this chapter, we went through a complete implementation of a card-based racing game. We saw how the graphics for the game are handled and we distributed a deck of cards among players while making sure that no two players got the same card in the deck. We designed the schema that is needed to implement this game and how the game states are used to implement the game play. Specifically, we implemented the card distribution using the unique game state feature of Pulse. This game implementation should serve as a good basis for any card game that you have in mind to implement.
[ 204 ]
Real-time Racing Game Tutorial So far in this book all of the game examples that we visited were mostly turn-based, except for jigsaw. In this final example, we will see a non-turn-based game that is a fast action, real-time racing game. To make things simple, that is to avoid any kind of physics, we will race with spaceships through tons of asteroids. We will call this game Astrorace! In this chapter, we will learn the following: • • • • • • •
Game programming to move a ship through space Adding items along the racetrack Collision detection of items with the spaceship Quickly creating different spaceships via skinning Optimizing the item loads for a long racetrack Implementing radar and mini-map Multiplayer implementation with an emphasis on network lag
Astrorace is a 2D platform or a side-scroller kind of racing game. Since this game requires data to be distributed among the players within the room at a much higher frequency, we will also look at the performance considerations and at the lag issues that a game such as this one will face. This chapter assumes that the reader has read the Hello World tutorial (Chapter 2) and is familiar with the PulseUI framework, as we will not be delving deep into the screen management or the Pulse game state APIs. Only relevant bits and pieces of the game code implementation are presented in this chapter, complete source code may be found at http://forum.gamantra.com.
Real-time Racing Game Tutorial
Game design
The game design is very simple. Like any other racing game we start at the starting line and dodge the obstacles on the way to reach the final destination. In Astrorace, the obstacles are asteroids. When an asteroid collides with the spaceship, the ship's speed is cut down to a minimum. There are also other kinds of items on the way that will increase the maximum speed of the ship or drop it.
The game client
We will first see the various objects that make up the game and leave the multiplayer specifics until we understand what data is needed to synchronize among the players. The classes are organized into the following folders: • • • •
game gsrc rsrc ui
The game folder contains all the classes that are required by the game during a race, such as the ship, asteroids, the map, radar, mini-map, etc. The familiar gsrc is where all the generated source files from the schema are located. The rsrc folder has all the graphic assets needed. The ui contains the screen classes that are subclassed from the PulseUI framework. A screenshot of the game client with three players in action is as follows:
[ 206 ]
Chapter 9
The main game loop
When the race begins, the RaceTrack class is where all the action begins. It adds the ship of the player and lays the items along the racetrack. Most importantly, the racetrack is at the top level because it contains the game loop. As with most game implementations, the game loop continuously does the following: 1. Reads the user input. 2. Processes user input. 3. Calls update on all the objects. 4. Handles any network events. As we explore each class, we will see what each object does during it's update call. For example, depending on the speed, the ship moves a certain distance, the radar object updates its display, and so on.
The spaceship class
We will start with the spaceship class. The spaceship is what we race with in this game. The class inherits from the Sprite class, so it can draw itself. There would be at least one instance of this class created for the player's own ship and one for the other player's ship that is part of the race.
Controlling movement
We will use arrow keys to control its movement. The ship may move in three directions—forward, up, and down—by pressing the right, up, and down arrow keys respectively. The left arrow may be used to slow down the speed of the ship, but not move backwards. In appendix 2, we explore one way to handle arrow keys that allows players to press more than one key at a time and not stall when the player furiously switches between keys.
[ 207 ]
Real-time Racing Game Tutorial
The arrow keys are handled in the Racetrack class and they pass the user inputs to the ship object. In fact, the ship moves in only a limited area. We will move the world around it, namely the items on the track at the speed of the ship. The RaceTrack object traps the ups and downs of a key as follows: private function keyUp(event:KeyboardEvent):void { var key:int; key = event.keyCode; switch (key) { case Keyboard.RIGHT: m_keyRIGHT = false; break; case Keyboard.LEFT: m_keyLEFT = false; break; case Keyboard.DOWN: m_keyDOWN = false; break; case Keyboard.UP: m_keyUP = false; break; } } private function keyDown(event:KeyboardEvent):void { var key:int; key = event.keyCode; switch (key) { case Keyboard.RIGHT: m_keyRIGHT = true; break; case Keyboard.LEFT: m_keyLEFT = true; break; case Keyboard.DOWN: m_keyDOWN = true; break; case Keyboard.UP: m_keyUP = true; break; case 83: // Key 's' m_ship.shieldReq(); break; } } [ 208 ]
Chapter 9
Notice that we also detect if the Key 's' was pressed, which is to put up the ship's shields. In the main loop, we call the processUserInput method that will update the coordinates and/or update the current speed of the ship. private function processUserInput():void { var speed:Number = m_ship.getSpeed(); var i:int; if ( m_keyRIGHT ) { m_ship.right(); } if ( m_keyLEFT ) { m_ship.left(); } if ( m_keyDOWN ) { m_ship.down(); } if ( m_keyUP ) { m_ship.up(); } }
The four methods on the SpaceShip class that implement the case when each key is pressed are as follows: public function right():void { updateSpeed(m_speedDelta); if ( x < MAX_X ) x += m_speedDelta; } public function left():void { updateSpeed(-m_speedDelta); if ( x > 0 ) x -= m_speedDelta; } public function up():void { if ( y > 0 ) y -= m_vt; } public function down():void { if ( y < MAX_Y ) y += m_vt; }
[ 209 ]
Real-time Racing Game Tutorial
The left and right method applies the change in the ship's speed. The speed changes by a delta (m_speedDelta) each time the key press is processed. This determines the acceleration or deceleration of the ship. There is a lot of scope for game design right on this variable. The m_speedDelta could depend on the ship type that a player owns or buys in the game. The delta can also be affected by the pick-ups along the track. Further, this change may be temporary for a certain amount of time, again depending on other factors, such as the kind of pick-up. Note that the x and y are limited by MAX_X and MAX_Y. This is done to keep the ship visible on the screen. The maximum value of x for the ship could be 800, the screen width, that is at really high speed the ship will appear at the right edge of the screen, making it harder for the player to dodge the oncoming asteroids. All this depends on the taste of the game designer. As with increasing or decreasing the ship's speed, the ship is also allowed to move vertically, and again by a delta. This delta value effectively controls how maneuverable the ship is. The updateSpeed method is listed next: public function updateSpeed(update:Number):void { if ( (update < 0 && m_speed > 0) || (update > 0 && m_speed < m_maxSpeed) ) m_speed += update; // make sure speeds are within allowed values if ( m_speed <= 0 ) m_speed = 0.1; if ( m_speed > m_maxSpeed ) m_speed = m_maxSpeed; }
The update parameter can either be positive or negative. We first check to make sure that the current speed is between zero and the maximum speed of the ship. After applying the speed update, we again make sure that the speed is within the range.
Skinning the ship
In order to create a wide variety of ships, we can do something similar to what we did in the jigsaw sample. Instead of the jigsaw shape, we have a base image of the ship and then we can apply various masks onto the ship. This will allow us to create tons of different ships in no time.
[ 210 ]
Chapter 9
In the game implementation as it is, we simply assign masks depending on the order in which the players enter the room. But for a real game, there is a lot of scope for these masks to be purchased. Creating a different base ship, you can still apply the same masks to create a wide variety of ships. The code that applies the mask to the ship's base image is listed as follows: public function SpaceShip(m:int, av:GameAvatarClient) { super(); m_mask = m; m_av = av; var colors:Array = new Array(); colors.push(0xFFFF00); m_shieldFilter = new GlowFilter(0xFFFF00); var bma:BitmapAsset; bma = new Ship() as BitmapAsset; var bmd:BitmapData; bmd = bma.bitmapData; // Get the mask data var mask:BitmapAsset; if ( m_mask == 0 ) mask = new Mask() as BitmapAsset; else if ( m_mask == 1 ) mask = new Mask1() as BitmapAsset; else if ( m_mask == 2 ) mask = new Mask2() as BitmapAsset; var maskBMD:BitmapData = mask.bitmapData; // Create a BMD to blend ship and the mask var pd:BitmapData = new BitmapData(128, 48, true, 0); var rect:Rectangle = new Rectangle(0, 0, 128, 48); pd.copyPixels(maskBMD, rect, new Point(0, 0), bmd, new Point(0, 0), true); [ 211 ]
Real-time Racing Game Tutorial graphics.beginBitmapFill(pd); graphics.drawRect(0, 0, bmd.width, bmd.height); graphics.endFill(); }
The racetrack module
The racetrack is where we race the ship. Along the way, the player must dodge the asteroids in order to retain the current speed or pick up an item that may increase the ship's maximum speed. The racetrack could be quite long and loading these items all at once may be too much for the Flash Player. For this reason, we cut the entire track into different quadrants.
Mapping coordinates
We will introduce the universe coordinates for the ship mainly along the x-axis. This has nothing to do with the actual rendering of a ship on-screen, but with this, we keep track of the ship's position relative to the entire racetrack. In this implementation, the universe coordinates start from zero and go up to the racetrack length. The racetrack length is equal to the number of quadrants times the length of each quadrant.
Loading quadrants
In this implementation, we have 10 quadrants and each is of length 1,600. Each quadrant gets loaded as it approaches it. At any given time, there are two quadrants loaded—the ship currently in and the subsequent one.
[ 212 ]
Chapter 9
As it passes the current quadrant and enters the next one, we unload the quadrant that the ship crossed over. When a quadrant object is loaded, it also adds all the items to the screen. Note that all items may not be immediately visible on the screen. When the quadrant is unloaded, all items belonging to it will also be removed from the display list. This technique will help us create tracks of arbitrary length without compromising the performance. All quadrant objects are created in the constructor of the racetrack object. public function RaceTrack(stage:Stage, pulse:GameClient) { … // Create all quadrant objects var i:int; for ( i=0; i
We increment the ship's universe position as the ship moves forward based on its current speed. Since we know the length of each quadrant, we can determine in which quadrant the ship is currently in. Within the game loop, we load and unload quadrants as the ship progresses through the track. private function gameLoop(event:TimerEvent):void { … … var cqi:int = m_ship.getCoord()/QUADRANT_LENGTH; if ( cqi >= QUADRANT_COUNT ) { … … // Reached the end of race, unload // all quadrants unloadQuadrant(m_quads[cqi-1] as Quadrant); unloadQuadrant(m_quads[cqi] as Quadrant); unloadQuadrant(m_quads[cqi+1] as Quadrant); … … } … … // Load and unload quadrant data loadQuadrant(m_quads[cqi] as Quadrant);
[ 213 ]
Real-time Racing Game Tutorial loadQuadrant(m_quads[cqi+1] as Quadrant); unloadQuadrant(m_quads[cqi-2] as Quadrant); … … }
The two private methods that actually load and unload the quadrants are listed next: private function loadQuadrant(q:Quadrant):void { var items:Array = null; var item:Sprite = null; var i:int; if ( q == null ) return; if ( !q.load(m_pulse) ) return; items = q.getItems(); for ( i=0; i
In each case, we first check if the quadrant is already in the loaded or unloaded state. If it is already in the required state, we simply return from the method. If not, in the case of load, we get all the items in that quadrant and add them as a child of the racetrack, and in the case of unload, we simply remove the items from the display list.
[ 214 ]
Chapter 9
The mini-map class
Mini-map shows where the ship is along the racetrack. In the implementation, we represent our own ship in red and all others in blue. The mini-map implementation is quite simple. The mini-map appears at the top of the screen during the race. The complete listing is shown below: package race.game { import flash.display.Sprite; import pulseui.util.Map; import race.game.SpaceShip; public class MiniMap extends Sprite { // The width of the mini-map private const MAP_LEN:int = 300; // A hash map to store all the ship sprites private var m_map:Map = new Map(); public function MiniMap() { super(); // Center the display x = (RaceTrack.SCREEN_W/2) - (MAP_LEN/2); // Give it a gray background graphics.beginFill(0xAAAAAA, 0.5); graphics.drawRoundRect(0, 0, MAP_LEN, 10, 1, 1); graphics.endFill(); } public function update(ship:SpaceShip, remote:Boolean):void { var s:Sprite; s = m_map.getValue(ship.getMask()); if ( s == null ) { // Create a sprite for each ship s = new Sprite(); m_map.put(ship.getMask(), s); if ( remote ) s.graphics.beginFill(0x0000FF, 0.8); //blue else s.graphics.beginFill(0xFF0000, 0.8); //red s.graphics.drawEllipse(0, 0, 7, 4); s.graphics.endFill();
[ 215 ]
Real-time Racing Game Tutorial s.y = 4; addChild(s); } var var l = var var s.x
c:Number = ship.getCoord(); //universe coord l:Number; RaceTrack.QUADRANT_LENGTH*RaceTrack.QUADRANT_COUNT; p:Number = c/l; // how much the ship has covered pos:Number = p*MAP_LEN; //scale it to map = pos; // update the sprite's x
} } }
The method update is repeatedly called within the main loop in RaceTrack object for each ship currently in race.
The Radar class
Radar, like the mini-map, is a simple implementation to show the coming asteroids. The player will be able to see them before they are visible on the screen enabling players to dodge them better. The radar class also has an update method that gets called from within the main game loop. Here is the listing for the class: package race.game { import flash.display.Sprite; import pulseui.util.Map; import race.AstroRace; import race.game.Astroid; public class Radar extends Sprite { // Size of the radar display private const WIDTH:Number = 200; private const HEIGHT:Number = 35; // Hashmap of items currently displayed private var m_map:Map = new Map(); public function Radar() { super(); // make a green eclipse [ 216 ]
Chapter 9 graphics.beginFill(0x00FF00, 0.4); graphics.drawEllipse(0, 0, WIDTH, HEIGHT); graphics.endFill(); // Center it and put it at the bottom // of the screen x = (RaceTrack.SCREEN_W/2 - WIDTH/2); y = RaceTrack.SCREEN_H - HEIGHT; } public // // if
function update(a:Astroid):void { avoid drawing stuff thats still far away from the ship ( a.x > RaceTrack.SCREEN_W*2 ) return; // remove the ones that have passed if ( a.x < 0 ) { s = m_map.getValue(a.getKey()); if ( s != null ) m_map.remove(a.getKey()); return; } var s:Sprite; s = m_map.getValue(a.getKey()); if ( s == null ) { // new astroid s = new Sprite(); m_map.put(a.getKey(), s); // simple black circle s.graphics.beginFill(0x000000, 0.7); s.graphics.drawCircle(0, 0, 3); s.graphics.endFill(); addChild(s); } // draw it at a proportional // x and y within the radar screen var c:Number = a.x; var l:Number = RaceTrack.SCREEN_W*2; var p:Number = c/l; s.x = p*WIDTH; c = a.y; l = RaceTrack.SCREEN_H; p = c/l; s.y = p*HEIGHT;
} } }
[ 217 ]
Real-time Racing Game Tutorial
Implementing items
Let's now look at the implementation of items in the game. There are three kinds of items in the game; visually you will see them as either black holes, mushrooms, or asteroids. It's quite easy to extend them as per your game design. Currently, except for asteroids there is no effect on the ship when it collides with the items. The two items, black holes and mushrooms, are typed instead of being subclassed, as they are quite simple. Asteroids, on the other hand, have the effect of cutting down the speed of the ship down to a minimum, unless the shields are active on the ship.
The constructor of the Item class is as follows: public function Item(index:int, q:int, type:int) { super(); m_index = index; m_quadrant = q; m_type = type; var bma:BitmapAsset = null; var bmd:BitmapData = null; switch (type) { case BLACK_HOLE: bma = new BHClass() as BitmapAsset; bmd = bma.bitmapData; break; case MUSHROOM: bma = new MushClass() as BitmapAsset; bmd = bma.bitmapData; break; case SPACE_STATION: bma = new EndClass() as BitmapAsset; bmd = bma.bitmapData; break;
[ 218 ]
Chapter 9 } if ( bmd != null ) { graphics.beginBitmapFill(bmd); graphics.drawRect(0, 0, bmd.width, bmd.height); graphics.endFill(); } }
The space station is the sprite that gets displayed when the ships reach the finish line. The items' update method simply updates the x coordinate of the item, depending on the speed of the ship. This is what gives the illusion that the ship is moving and with speed. The following update method is called on each item's object that's loaded by its quadrant. The speed passed in is the current speed of the ship. public function update(speed:Number):void { x -= speed; }
The items are created when the quadrant is loaded. Here is the load method of the quadrant and the initItems method: public function load(p:GameClient):Boolean { if ( m_loaded ) return false; m_loaded = true; if ( m_objects.length == 0 ) initItems(p); trace("Loaded Q:" + m_id); return true; } public function initItems(p:GameClient):void { var i:int; if ( m_objects.length > 0 ) return; // another client has already loaded it for ( i=0; i<(m_id+1); i++ ) { var ic:ItemClient; var item:Item; var sx:Number, sy:Number; item = new Item(m_id, m_itemId, Item.BLACK_HOLE); sx = ((Math.random()*1000000)%RaceTrack.QUADRANT_ LENGTH)+800; sy = (Math.random()*1000000)%600; [ 219 ]
Real-time Racing Game Tutorial ic = new ItemClient(); ic.setQuadrant(m_id); ic.setIndex(m_itemId); ic.setItemType(Item.BLACK_HOLE); ic.setPosX(sx); ic.setPosY(sy); ic.setStateKey(item.getKey()); p.addGameState(ic); m_itemId++; item = new Item(m_id, m_itemId, Item.MUSHROOM); sx = ((Math.random()*1000000)%RaceTrack.QUADRANT_ LENGTH)+800; sy = (Math.random()*1000000)%600; ic = new ItemClient(); ic.setQuadrant(m_id); ic.setIndex(m_itemId); ic.setItemType(Item.MUSHROOM); ic.setPosX(sx); ic.setPosY(sy); ic.setStateKey(item.getKey()); p.addGameState(ic); m_itemId++; var a:Astroid = new Astroid(m_id, m_itemId); sx = ((Math.random()*1000000)%RaceTrack.QUADRANT_ LENGTH)+800; sy = (Math.random()*1000000)%600; ic = new ItemClient(); ic.setQuadrant(m_id); ic.setIndex(m_itemId); ic.setItemType(Item.ASTROID); ic.setPosX(sx); ic.setPosY(sy); ic.setStateKey(a.getKey()); p.addGameState(ic); m_itemId++; } }
The items are created and added to the array (m_objects). The number of items created in each quadrant is proportional to the ID of the quadrant. Notice the loop runs as many times as the ID (plus 1) of the quadrant. This makes the quadrant with a higher ID (towards the end of race) have more items or obstacles than the quadrants in the initial part of the race. [ 220 ]
Chapter 9
The x coordinate is a random number between 800 (screen width) and 800 plus the quadrant length. The reason to add 800 is to avoid the items from popping up on the screen rather than loading when it's still not visible and brought into view as the ship moves forward.
Detecting collisions
In this implementation, the collision is tested for ships colliding with asteroids only, and not for other items. It should not be difficult to extend the behavior to all items. The moveItems method responsible for moving the items at the ship's current speed is called within the main game loop. private function moveItems(q:Quadrant):void { var items:Array = null; var item:Item = null; var i:int; if ( q == null ) return; items = q.getItems(); for ( i=0; i
The collision detection is simple; we get the sprite rectangle for the ship and each of the asteroids and check for an intersection. If they intersect, we call the collide method on the ship.
[ 221 ]
Real-time Racing Game Tutorial
The collision reaction is to cut the speed of the ship to a minimum, or if the shields are up, we bounce off the asteroids. The listing for the collide method on the asteroid and on the spaceship is as follows: public override function collide(ship:SpaceShip):void { if ( ship.isShieldOn() ) { var speed:Number = ship.getSpeed(); if ( y > ship.y ) m_coly = speed; else m_coly = -speed; } ship.collide(this); } public function collide(a:Astroid):void { if ( !m_shieldOn ) updateSpeed(-1*m_maxSpeed); }
Implementing the shield
Recall that we trap the s key on the keyboard. If it was down, we call the shieldReq method. When the shield is on, we add a glow filter to the ship sprite for a period of m_maxShieldTime, after which we remove the glow effect. During this state, when the ship collides with an asteroid, we bounce it off. The implementation of the shield is made easy with the main game loop design and calling the update method from within. The following are the methods that deal with shield handling: public function isShieldOn():Boolean { return m_shieldOn; }
shieldReq is called when we detect that the s key was hit. We check if the shield is already on; if so, we ignore it. public function shieldReq():void { if ( m_shieldOn ) return; shieldUp(); }
[ 222 ]
Chapter 9
To indicate that the ship's shields are up, we add a filter to the ship sprite. private function shieldUp():void { m_shieldOn = true; var filts:Array = this.filters; filts.push(m_shieldFilter); this.filters = filts; m_shieldTime = m_maxShieldTime; }
When the update is called from the main game loop, we decrement the remaining shield time, and when it goes down to zero, we turn off the shield. Here again is a great scope for powering the ship's shield-up time, depending on the ship and if it has acquired any items that increase the shield time. public function update(timeLenMilli:int):void { m_trackPosX += m_speed;//*timeLenMilli; if ( m_shieldOn ) { m_shieldTime -= timeLenMilli; if ( m_shieldTime <= 0 ) { shieldDown(); } } } private function shieldDown():void { m_shieldOn = false; var filts:Array = this.filters; filts.pop(); this.filters = filts; }
Finishing the race
The race is finished when the last quadrant is loaded. Whoever loads it up, wins the race. When the main game loop detects the end, we unload all quadrants and turn on the Boolean variable to indicate that the race is over. We will also slap on the finish screen sprite. After the race is over, we stop calling any update methods and simply return from the game loop. private function gameLoop(event:TimerEvent):void { processUserInput(); if ( m_gameEnd ) return; m_map.update(m_ship, false); var cqi:int = m_ship.getCoord()/QUADRANT_LENGTH; [ 223 ]
Real-time Racing Game Tutorial if ( cqi >= QUADRANT_COUNT ) { // Reached the end of race, unload // all quadrants unloadQuadrant(m_quads[cqi-1] as Quadrant); unloadQuadrant(m_quads[cqi] as Quadrant); unloadQuadrant(m_quads[cqi+1] as Quadrant); var win:ShipWinClient; win = new ShipWinClient(); win.setStateKey("I Win"); m_pulse.addGameState(win); var end:Item = new Item(0, 0, Item.SPACE_STATION); end.x = 0; end.y = 0; addChild(end); removeChild(m_ship); addChild(m_ship); m_gameEnd = true; return; } … … }
Multiplayer implementation
So far we have seen the implementation of different parts of the game. We will now see how we can make this into a multiplayer game. For the multiplayer game, we will go through the implementation for the following: 1. Getting the unique mask for the ship in the race. 2. Loading the asteroids in the same position in all clients. 3. Synchronizing each player's ship position on all player's screen. 4. Figuring out who won the race.
[ 224 ]
Chapter 9
Designing the schema
After knowing what information we need to share among the clients, we start out with the schema file. The schema file for the implementation is as follows: <property name="maskId" type="byte"/> <property name="sid" type="int" /> <property name="posX" type="float"/> <property name="posY" type="float"/> <property name="speed" type="float"/> <property name="quadrant" type="byte"/> <property name="index" type="byte" /> <property name="itemType" type="byte"/> <property name="posX" type="float"/> <property name="posY" type="float"/>
[ 225 ]
Real-time Racing Game Tutorial
The ShipMask class
We define the ShipMask game state to claim a mask for the ship. In this implementation, the maskId could carry a value of 0, 1, or 2. This game state is added as a unique game state, so if a client is able to successfully add the state, then it is said to have acquired the mask. The assignment of the mask is only done once the race starts the first time.
The ShipPos class
The ship's position is broadcast by sending this game state as action to all the other clients in the race. Each client will continually send the position of the ship from within the main game loop once every 200 milliseconds, in other words, five times a second. The property sid is the ship ID, which we set to the mask for the ship. We will also send the current coordinates and the ship's current speed. The posX property will carry the universe X coordinate of the ship and not the screen x. The posY property will be the screen's y position.
The item class
The item class is used to synchronize the positions of all the asteroids. There are two ways to lay out the asteroids. One way is to statically define the positions they will be placed on the track in each quadrant, then every client simply loads them and places them appropriately. This method does not need any synchronization among clients. The second method is what the implementation does in which each asteroid is placed in a random position. This presents two issues—first, the position of each asteroid should be the same on each client, and this leads to the second issue that only one client should succeed in announcing the initial position of an asteroid. More than one client may succeed in announcing the initial positions, but for different asteroids. So we will need to add this game state as a unique game state.
The ShipWin class
This game state is used to claim victory by a client finishing the race first. In a typical server-based implementation, the server would be able to determine the winner. To determine the winner without writing specific server programming, we will use the unique game state technique to figure out the winner of the race.
Assigning ship color
We will use the ShipMask game state to manage the unique assignment of the ship's mask. For a real game implementation, we would probably need to have the players choose their ships' masks from a large selection. [ 226 ]
Chapter 9
In the current implementation, we will simply assign the mask in the order the server receives the request and each client requests the mask starting from 0. The implementation only has three masks defined, but it should be straightforward to create more masks and have the player choose a mask of his or her choice rather than having one assigned to them. Let's now visit the familiar game (Game.as) screen class, which is subclassed from the PulseUI framework. Let's see the parts of the code that get the ship's mask. We make use of the following class properties to implement getting the mask for the ship: private var m_mask:int=0; private var m_myMask:int; private var m_maskCount:int=0;
The m_mask property is the current mask that the client will attempt to get assigned. The m_myMask property is the successful mask value for the ship. Lastly, the m_maskCount property is the number of assignments that have been currently made. When the assignments are equal to the number of clients in the room, the game host will start the race. public override function startGame():void { if ( m_firstTime ) { super.startGame(); requestShip(); m_firstTime = false; addChild(m_track); } else { addChild(m_track); m_track.startRace(); } }
When the game host starts the game, before the actual race begins, we first assign the mask. Note that we only make the mask assignment once; the same masked ship will be used for all the races within the room. The requestShip method is listed below: private function requestShip() :void { var mask:ShipMaskClient; mask = new ShipMaskClient(); mask.setMaskId(m_mask); mask.setStateKey("Mask-" + m_mask); [ 227 ]
Real-time Racing Game Tutorial m_pulse.addGameState(mask); m_mask++; }
In this method, we create a ShipMaskClient game state with a game state key m_mask and with a "Mask" prefix. The add operation may either succeed or fail if another client has already successfully added it. The PulseClient callback is implemented in the main class of the project; the onNewGameState and onGameStateError are simply redirected to this game screen class. In either case, we call the following assignShip method: private function assignShip(mask:ShipMaskClient, error:Boolean):void { if ( error ) { requestShip(); } else { m_maskCount++; if ( mask.getSenderId() == m_myId ) { m_myMask = mask.getMaskId(); m_track.setShipMask(m_myMask, m_pulse.getMyAvatar()); } } if ( m_maskCount == m_players.getPlayers().length ) { m_track.startRace(); } }
The error parameter will be true. In the case of an error, we will make another request with the next mask value. If it was successful, there are two cases to handle. First, if it was one's own add operation that succeeded, we assign the m_myMask property to the value and initialize the ship. The second case is that another player's add operation succeeded. In either case, we increment the m_maskCount property. Finally, start the race if the condition to start was satisfied, which is when m_maskCount is equal to the number of players in the room.
[ 228 ]
Chapter 9
Putting items on the map
In order to have the items on the racetrack appear at the same position in all clients and to have only the add operation succeed for each item, we will make use of the unique game state. We will add one unique game state for each item on the racetrack. Further, we will not add all the items on the race at once; we will do so when it is actually needed, that is, when the quadrant is loaded. Each client during the quadrant load adds a unique game state for each of the items. Only one client will succeed in adding a unique game state because the key would be the same for a particular item. The items are held in a private array m_objects property. The items are added to the array only after the client receives a successful (add) notification from the server. The initItems is called during the load method of the quadrant. The initItems goes into the loop of creating items, and before it does, it checks if m_objects has at least one item. If there is, it means another client has already added the items or is in the process of adding them—in this case, we simply return from the method. When the new game state notification is received for the item objects, they are delegated to the corresponding quadrant instance. The following illustration shows the delegation from the main class AstroRace, which inherits from PulseGame of PulseUI class:
Let's examine the onAddItem method of the RaceTrack object. After it calls the onAddItem on the corresponding quadrant, the method checks if the quadrant is already loaded; if so, the item is also added to the screen. public function onAddItem(ic:ItemClient):void { var q:int; q = ic.getQuadrant(); var quadrant:Quadrant; quadrant = m_quads[q] as Quadrant; [ 229 ]
Real-time Racing Game Tutorial var item:Item = quadrant.onAddItem(ic); if ( quadrant.isLoaded() ) { addChild(item); } }
If the quadrant is not loaded in the client that receives the notification, we wait until it is loaded and add all the items belonging to the quadrant are loaded at once onto the screen. For the first two quadrants, all clients will attempt to add items to the server. Even if more than one client gets into the add items loop, the unique game state key will prevent the same two items from being added. For subsequent quadrants, the client who is leading the race will succeed in adding the items for the new quadrants.
Ship prediction and interpolation
We will now see how we render other ships (remote) on our screen. At the beginning of the race, we will see all other players' ships, and as the game progresses, some may race ahead while others may slow down because they hit an asteroid. Other players' ships will be visible only if they are close to the ship that is being driven by the player. In this implementation, the ship's position is broadcast to every other player at the rate of five times per second. The following is code snippet that sends up the position update for the ship: private const TIMER_LEN_MILLI:int = 10; private const NET_UPDATE_FREQ:int = 200; private function gameLoop(event:TimerEvent):void { … … m_lastNetUpdate += TIMER_LEN_MILLI; if ( m_lastNetUpdate > NET_UPDATE_FREQ ) { m_lastNetUpdate = 0; var u:ShipPosClient; u = new ShipPosClient(); u.setSid(m_mask); u.setPosX(m_ship.getCoord()); u.setPosY(m_ship.y); u.setSpeed(m_ship.getSpeed()); m_pulse.sendGameStateAction(u); } // update remote ships var remotes:Array; remotes = m_ships.values(); var s:SpaceShip; [ 230 ]
Chapter 9 for each ( s in remotes ) { s.updateRemote(m_ship.x, m_ship.getCoord()); m_map.update(s, true); } }
The position is broadcast as an action, so it will not be cached on the server for the game session, and is not required anyway. The game loop is called after every 10 milliseconds and the network update frequency is set to 200 milliseconds. We send up the ships' IDs (masks), universe X coordinates, Y screen positions, and current speed. We will create and maintain a SpaceShip instance for each remote ship. We will update the ships' x and y based on the last received update. There are two issues we need to deal with. First, we receive the updates of the other ships at the rate of five times per second, while we update everything else on the screen every 10 milliseconds. This calls for interpolating others ships' positions based on the last received update. We can do so since we have the last known position and the speed. And there is another catch: the position we received was via the network and the server. This means that even the last update we received of the ship is not of the present moment. All network packets going over the server suffer from this lag. This calls for prediction. We will simply add the time it took for the packet because it was created and sent over the network. However, it is quite impossible to determine the exact time taken even with a sophisticated time synchronization algorithms. We can at best approximate the average lag the packet suffered. In Pulse or other client-serverbased implementation, the packet has to travel from the originating client to the server and then to the destination client. The lag would not be the same for all clients connected to the server and to make matters worse, the lag does not remain constant between clients and the server over time.
[ 231 ]
Real-time Racing Game Tutorial
The following illustration shows the lag for the updates coming over the network. Observe that the lag is not a constant, even though the originating client is sending the updates at a uniform time interval. The receiving client does not get it at the same interval. The illustration also shows the interpolation of the remote ship position between the update intervals.
Pulse SDK eases the issue by keeping track of the lag for each client and is recomputed quite often based on the server load. To find the lag between a client and the server, we simply get the value from the corresponding GameAvatarClient instance. The lag value we want to take into account is the average lag originating from client to server and the average lag from the server to our client. The following lists the method on SpaceShip that processes the incoming update: public function onUpdateRemote(posX:Number, posY:Number, speed:Number, remoteLag:int):void { m_rc = posX; m_ry = posY; m_speed = speed; // As this came over the network, we // will predict the ship's position at this // moment. For this we assume that there was // no change in speed! var myLag:int; myLag = m_av.getAveLag(); var totalLag:int; totalLag = myLag + remoteLag; trace("Total Packet Lag: " + totalLag); [ 232 ]
Chapter 9 m_trackPosX = m_rc+(m_speed*totalLag); }
The interpolation of points between the network updates is taken care of by the main loop calling the following method on all the remote ship object instances: public function updateRemote(myShipX:Number, myShipCoord:Number):void { m_trackPosX += m_speed; if ( Math.abs(m_ry-y) > 1 ) { if ( m_ry < y ) y -= m_vt; else y += m_vt; } x = myShipX + (m_trackPosX-myShipCoord); }
We will observe some jerkiness even when all the clients and the server are in the same LAN. Because the lag information is only an approximation, when the update is received, we update it to the new predicted point, which could be off by a factor. Moreover, if the ship makes sudden changes in speed, this will also add to the jerkiness of the remote ship rendering.
Winning the race
Winning the race is determined in the main game loop when we find that there are no more quadrants to load. At this point, we will attempt to add a unique game state, ShipWin. It could be that more than one client sent this message at the same instant; however, the server will only process the first game state and rejects any subsequent game state add operation. This ensures there is only one clear winner of the race. Once the race is won by a player, we display the win or lose sprite on all players' screens, and after a brief period, we resume the race again. The following are the steps that will take players to the next race: 1. When a client receives the ShipWin game state, depending on the sender, we display either the win sprite or the lose sprite. Calling finishGame API is important as it cleans up all the game states accumulated from the race, including the unique game states. public function onWin(win:ShipWinClient):void { if ( win.getSenderId() == m_myId ) { displayResult(true); } [ 233 ]
Real-time Racing Game Tutorial else { displayResult(false); } if ( m_pulse.isGameHost() ) m_pulse.finishGame(); }
2. We will apply the fade effect to the result sprite and set up the onRestartGame callback to be called after the fade effect is done. private function displayResult(win:Boolean):void { var r:WinLoose; r = new WinLoose(win); r.x = 400-(r.width/2); r.y = 300-(r.height/2); addChild(r); var sd:SpriteDissolve; sd = SpriteDissolve.createSpriteDissolve(this, r); sd.play(); sd.addEventListener(SpriteDissolveEvent.DONE, onRestartGame); m_pulse.playerReady(false); }
3. Once the fade effect is done, we will start the game. We will remove the current track from the screen, create a new racetrack, and call the start game. private function onRestartGame(e:Event):void { removeChild(m_track); m_track.stopRace(); m_track = new RaceTrack(m_stage, m_pulse); m_track.setShipMask(m_myMask, m_pulse.getMyAvatar()); addChild(m_track); m_pulse.playerReady(true); if ( m_pulse.isGameHost() ) m_pulse.startGame(); }
4. The race begins for everyone once the server broadcasts the game start message. This time around, we simply add the track to the screen and go! public override function startGame():void { if ( m_firstTime ) { super.startGame(); [ 234 ]
Chapter 9 requestShip(); m_firstTime = false; addChild(m_track); } else { addChild(m_track); m_track.startRace(); } }
The implementation is no way close to what you can deploy as a real game. There are a lot of improvements that can be made, especially for players owning and upgrading their ships. Also, a complete item system should be implemented with interesting effects on the ship and the gameplay as a whole. But the implementation presented here takes care of all the basic functionality of a typical, fast-action racing game.
Summary
We explored useful techniques such as quadrants that allow us to have arbitrary, long racetracks and also the masking technique for us to quickly offer a wide variety of spaceships. You should now be familiar with controlling the ships' movements, adding items on the track, and moving them at the ships' speed. We also learnt how having a main game loop and calling update methods on objects from within the loop simplifies the overall implementation. We looked at the challenges regarding the lag and updating the position of others ships' positions on our own screen. We also saw a simple implementation of a mini-map that not only gives players an idea of how far along the racetrack they have progressed, but also how near or far the other ships are with respect to their own ship. Furthermore, we got acquainted with the radar object that shows oncoming asteroids before they appear on the screen.
[ 235 ]
Introduction to FlashBuilder and AS3 Installing Flash Builder 4
What you need to start out is to get yourself the trial version from the Adobe site at http://www.adobe.com/products/flashbuilder/. That's pretty much all you need to start writing your games. In order to write multiplayer games, we will also need a server that allows the exchange of information between players. For this, we will use the Pulse SDK framework. The Pulse SDK not only includes the server that connects all players within a game instance, but also offers all the standard feature set required by any multi-player game, such as friends, chat, sharing of game state information among players and further helps us manage the lobby and rooms.
AS2 versus AS3
If you are already a long-time Flash developer working with AS2, this chapter is great to get your feet wet in AS3 programming. We will go through the AS3 syntax that enables us to write structured code by the use of classes and inheritance techniques. These concepts may seem like an overhead, but will make our programming task much easier, allowing us to create large, complex projects with ease. Also, programming in an object-oriented way lets us reuse the code. No, copy and paste is not one of them! My simple suggestion is not to try and seek all the advantages of an object-oriented design and programming from day one. It does take some practice and working with a few projects to realize that you are doing something similar to what you did in another project, and you will automatically start building your own set of classes that you can reuse in all your future projects.
Introduction to FlashBuilder and AS3
Exploring Flash Builder 4
For all our multiplayer development in this book, we will be using Flash Builder 4 to do our coding. It is worthwhile spending some time to familiarize ourselves with the IDE. Let us now explore the various features offered by the program.
In the default layout, on the left side (#1) you will see the Package Explorer view. This view shows all the projects and its files that you will be creating and managing. The central large area (#2) is where the text editor comes up when you click on an editable file in the explorer and is where you will be coding for the most part. It is quite simple compared to the Flash Designer Product (CS series), but usually that's all you need when you are doing everything in code. At the bottom of the Flash Builder window, notice the Problems (#3) tab view—this is where the compiler will display errors and warnings if there are any with the code you wrote. Go ahead, choose the Console menu item under the Window menu bar; this will add a Console tab view (#4) along with the Problems tab. The console is where your program can log things to the screen, mostly for debugging purposes. Remember that only when the program is running in debug mode, can your program log things onto the console.
[ 238 ]
Appendix A
You will also notice the Outline table view (#5) on the bottom-left corner. This view shows the contents of a class that is currently being edited or selected in the File Navigator. You will also notice at top-right corner (#6) of the window, the perspective chooser, which allows you to be in several perspectives, each one suited for your current task at hand. For example, if you are doing development, you could be in the development perspective, meaning you will have a larger area to do your coding, etc. You may also customize the perspective to suite your own needs. The development perspective is what the Flash Builder will start out with. When you start to debug your program, Flash Builder will automatically prompt you to switch the perspective to one that is convenient for debugging, which contains more tab views to let you control (#1) the program execution such as pausing, stepping, etc.
This perspective also shows view (#2) for break points and for examining the values of variables, etc. The console view (#3) appears at the bottom where the program log appears.
[ 239 ]
Introduction to FlashBuilder and AS3
There is much to explore in Flash Builder. We will learn parts of the IDE as we progress through the book. You are also encouraged to explore yourself or find detailed information via Adobe's documentation and online help.
Hello World!
As a first step to creating multiplayer games using Flash Builder, we need to first familiarize ourselves with creating and organizing our code files within a project. In the spirit of the Hello World tradition, let's go ahead and create a project and have it print Hello World!.We may do so by choosing the menu item New under File menu and then choosing ActionScript Project. You can also get to this menu item by rightclicking on the blank area in the Files Navigator tab.
We will then be asked to enter the name of the project. Simply type HelloWorld as shown in the following screenshot and click on the Finish button:
[ 240 ]
Appendix A
You will see that the Flash Builder creates a file automatically for you with the name HelloWorld.as. The extension .as stands for, you guessed it, ActionScript! The contents of the file simply define the class called HelloWorld. We will explore the class in detail in just a bit. On the left-hand side you will see that there is a bunch of directories also created for you, as shown next:
The bin-debug folder where the end result files are saved includes the SWF and HTML files. The html-template folder contains the template files for creating the end result HTML for your flash content. At this point, let's not deal with these two folders; instead, focus on the HelloWorld.as.
[ 241 ]
Introduction to FlashBuilder and AS3
Defining a class
The contents of the HelloWorld.as file should be as follows: package { import flash.display.Sprite; public class HelloWorld extends Sprite { public function HelloWorld() { } } }
Flash Builder always creates a main class with the same name as the project name, in this case, HelloWorld. The main class is what gets loaded first when the program starts the execution. Let us go ahead and add a statement that will output the string "Hello World" like the following: public function HelloWorld() { trace("Hello World"); }
Notice that we are using a global function called trace offered by the Flash library, which takes in a string as a parameter and prints to the console view of the Flash Builder. Note that in order to view the output, we should run our program in debug mode.
[ 242 ]
Appendix A
When we run the program in debug mode, the browser window comes up with a blank view. It's blank because we are not drawing anything yet. We will need to switch back to the Flash Builder window and notice the console view and there it is—our string "Hello World" printed. Now that we have test run our Hello World program, let us examine the code. As you can see, there are 10 lines in all. Let's explore each line. The class definition starts out with a package statement (package { … }) that surrounds the rest of the file contents with the curly brackets, implying that this class belongs to this package. Actually, the keyword package is usually followed by the name of the package. As this class is at root level, the package name can be left blank. However, Flash Builder expects you to have a package name when you want to store them into various folders in whatever fashion that suits your needs. In this case, the package name must follow the directory structure in a dot format. For example, you could create a folder called game at the same level as HelloWorld. as and then specify a package name as game for all the class files that you wish to store in that folder. If you have created a folder called util under the game folder, then the package name for all the class files must be named game.util. Next are the import statements: import flash.display.Sprite;
Whenever you want to make use of a class defined elsewhere that is not in the same folder as the class itself, you need to add an import statement for the class. In this example, the HelloWorld class is being extended from Sprite and so we need to import the class Sprite. Note that the import statement must include the full package name of where it's found. As you progress through the book, you will start to instinctively remember what classes are in what package and what you would need to import. The Flash Builder has a good auto-completion feature. When you type the dot (.), it will offer you a list of possible completions, be they package names, class names, properties, or method names. You can also bring up the auto-completion by holding down the Ctrl key and pressing the space bar. The class definition itself starts with the public class HelloWorld extends Sprite statement. The class name itself is HelloWorld because it follows immediately after the keyword class. It extends from a class called Sprite, which is our friend that we will learn all about in the following chapters. The keyword public tells the compiler that this class can be referenced from other packages.
[ 243 ]
Introduction to FlashBuilder and AS3
Classes—defining game objects
Encapsulation is a big object-oriented word! But what exactly is it? As the word suggests, software is best designed when you have reusable components whose functionality is well defined and exposes a bunch of interface for its use by other components. Further, you don't need to care much for how its functionality is implemented. Classes are a way to achieve encapsulation in object-oriented programming. Take for example an Array class. You don't care how the elements are actually stored in it as long as you can tell the class that you want to add, retrieve, delete items in the array. A class exposes it's functionality via a bunch of methods, also called the interface for the class. A method is simply a function that can take parameters, does something useful, and optionally return a value. The difference between a function and the method in this case is that the method is attached to the class. You can only refer to the method as part of the class and not independently of the class. A class can store its internal state in what are called properties, for example, an internal property to keep track of the current number of elements in the array. A class may define as many properties and methods as required, and each property and method may also be marked as private, public, or protected. A private property or method can be referenced only from within the class as opposed to the public properties that may be accessed from any other class, which has access to the class. The class itself may be restricted by specifying whether it is private or public. A private class is only available to another class in its own package.
Creating game objects
Simply put, an object is an instance of a class. You can define an array class. This only means that you have specified the blue print for this class, but during runtime you need to create an instance that is usable, and you are free to create as many instances of the same array class, for example. Further, only after you have created an instance can you call the methods or use the properties defined on them. Let's see how all this looks in actual code. Let us create a new project and call it HelloAvatar and create a class of our own. Once the project is created for us, we can get organized and create a folder under which we will stuff our class. Go ahead and right-click on the project icon HelloAvatar and choose Folder under the New menu item. In the dialog window that comes up, enter the word world for the Folder name. Right-click on the newly created world folder and choose ActionScript Class under New menu item. Type Avatar for the Name field and click on Finish. [ 244 ]
Appendix A
You will see that the Flash Builder creates a new file called Avatar.as under the world folder, and it's even nice enough to get us started with the following: package world { public class Avatar { public function Avatar() { } } }
Notice that since the class is defined under the world folder, the package name was automatically set to world. The class name is Avatar and it is defined as a public class, which means that this class may be referenced by other classes in other packages. It also creates a method with the same name as the class. This is a special method called the constructor, which means if you want to create a new object of this class, you do so via this method. Also notice that there is no return value for the constructor; it always returns a newly created object of class Avatar. Now, how do we create an instance or an object of class Avatar? We will modify the main class HelloAvatar as follows: package { import flash.display.Sprite; import world.Avatar; public class HelloWorld extends Sprite { public function HelloWorld() { var anAvatar:Avatar; anAvatar = new Avatar(); } } }
You will need to first import the class with an import statement, so the compiler knows exactly what you mean by an Avatar. You then create a variable that points to and gives it a name, in this case, anAvatar. Finally, you create a new object that is an instance of Avatar using the new operator followed by the constructor. There, you are now in the object-oriented world! [ 245 ]
Introduction to FlashBuilder and AS3
Variables and properties
The above example shows a few more things about the ActionScript 3 language elements, such as defining a variable You first need to tell the compiler that you are defining a variable via the var keyword, followed by a name of your choice, followed by a colon (:), and then the type of the variable. Here are some examples: var anInteger:int = 3; var anArray:Array = new Array(); var yesOrNo:Boolean = false;
A class is also referred to as a user-defined type, meaning that by creating a new class you are also creating a new data type that can be assigned to a variable just like the ones you may have used such as int, float, and so on. However, we still make a distinction between a primitive datatype such as an int and/or a complex datatype such as a class. Primitive datatypes such as an int and float do not offer any methods like a class, but only a placeholder to store the value of the type. But Actionscript 3 is very different from the other programming languages you may have come across. In AS3, everything is a class! To store and manipulate numbers, AS3 provides int and Number classes. To store strings, it offers the String class. More specifically, an int class lets us manipulate signed 32-bit integers and uint lets us manipulate unsigned 32-bit integers. The Number class lets us manipulate a floating point number. Let's see the variables in action, assign values to the variables, and have them print it. Here is the listing: package { import flash.display.Sprite; import world.Avatar; public class HelloWorld extends Sprite { public function HelloWorld() { // This is a comment. /* This is a comment as well and is multi-line we can declare the variable and assign an initial value!*/ var anInteger:int = 3; var aNumber:Number = 3.1415; var aString:String = "This is a string." [ 246 ]
Appendix A var yesOrNo:Boolean = false; trace("Value trace("Value trace("Value trace("Value
of of of of
anInteger: aNumber: " aString: " yesOrNo: "
" + + +
+ anInteger); aNumber); aString); yesOrNo);
} } }
To print the values of variables, we can use the global function trace. The trace takes a string as a parameter. AS3 also allows you to concatenate the string and other types using the + operator as you can see in the listing. To run the project in debug mode, click on the little icon with the green bug in the tool bar. Remember, you can see that the output of trace function calls only when you run the project in debug mode. When the project is run in debug mode, the browser is launched with an empty gray window. This is because we are not drawing anything to the screen yet. You will need to switch back to the Flash Builder and inspect the Console tab view, which appears at the bottom of the Flash Builder window. You should see the following: Value Value Value Value
of of of of
anInteger: 3 aNumber: 3.1415 aString: This is a string. yesOrNo: false
Here is a code snippet to work with an Array datatype: var anArray:Array = new Array(); anArray.push("Element 1"); anArray.push("Element 2"); anArray.push("Element 3"); var i:int; for ( i=0; i
This will produce the following output: Element in the array at index 0 has a value of Element 1 Element in the array at index 1 has a value of Element 2 Element in the array at index 2 has a value of Element 3 [ 247 ]
Introduction to FlashBuilder and AS3
Properties are nothing but variables that belong to the class and that all methods have access to. Another way to look at it is that the variables (also called local variables) are only accessible within the method they are defined in, while the class properties are accessible to the entire class. You may fine-tune the access of the properties even beyond the class it is defined in. Here is a general syntax to define a property: [public|private|protected] var name:type;
The declaration starts with either the public, private, or protected keyword followed by the var keyword, which is short for variable, followed by a name of your choice and ending with the type for the variable. Here is an example that illustrates the class property declaration and its usage. We will take the previous example of variables and turn them into class properties. package { import flash.display.Sprite; import world.Avatar; public class HelloWorld extends Sprite { private var anInteger:int = 3; private var aNumber:Number = 3.1415; private var aString:String = "This is a string." private var yesOrNo:Boolean = false; public function HelloWorld() { trace("Value of anInteger: trace("Value of aNumber: " trace("Value of aString: " trace("Value of yesOrNo: " }
" + + +
+ anInteger); aNumber); aString); yesOrNo);
} }
Running this sample in debug mode would produce the exact same results as the previous version. When do you decide that something should be a method variable or a class property? The answer is easy. It simply depends on how you want to use it, and if you need access to a variable that needs to be accessed by multiple methods and need to save its value over the life of the object. In this case, it is a good idea for it to be a class property. An example would be x and y coordinates in a display class. [ 248 ]
Appendix A
An example of how class properties may be used is illustrated in the following class: package { import flash.display.Sprite; public class HelloWorld extends Sprite { private const DELTA:int = 10; private var m_posX:Number; private var m_posY:Number; public function HelloWorld() { } public function moveRight():void { m_posX += DELTA; } public function moveLeft():void { m_posX -= DELTA; } public function moveDown():void { m_posY += DELTA; } public function moveUp():void { m_posY -= DELTA; } } }
Magic numbers and constants
You also observe a declaration statement for DELTA. As opposed to a variable, a constant may not change its value after being declared with an initial value. The use of constants is a great programming habit that cuts down on the magic numbers all over the code. This also helps if you want to fine-tune the value from 10 (in the sample) to something else—one change results in changes everywhere.
[ 249 ]
Introduction to FlashBuilder and AS3
Methods
Methods are nothing but functions that are part of the class definition. You interact with objects of a class by calling various methods. The method definitions follow the structure as shown: [public|private|protected] function name(parameter:type, …):return type { body }
Each method can be defined as private, public, or protected. A public method is a method that can be accessed by the class itself or from outside of a class. The private method is only accessible from within the class. You sometimes need a private method to factor commonly used code into a function and when you prefer not to expose this to other classes. We will see how we can take advantage of a protected method when we discuss inheritance. Following the access keyword comes the keyword function, followed by the name of the method. The method can take multiple parameters with a specific type defined for each. Following the parameter list comes a colon and a return type. The return type is what kind of datatype is returned by the function; if none is returned, void many be specified. Now let's see some examples. An example constructor method that takes a number of parameters is as follows: public function Monster(attackPower:Number, maxLife:Number, respawnRate:Number, lifeUpdateRate:Number) { … }
An example of a public method that accepts a few parameters and returns nothing is as follows: public function causeDamange(damage:Number, avatar:Avatar):void { … }
[ 250 ]
Appendix A
An example of a private method: private function respawn(delta:Number):void { … }
A public method that returns a value: public function isAlive():Boolean { return ( m_life > 0); }
Property and method access
We will learn a great deal about inheritance in the next section, but here is a pretty picture that shows how private, protected, and public keyword usage on properties and methods helps you control their access across classes. Class A: private var a: int; publicvar b: int; protected var c: int; May access public & protected properties and methods
May access public properties and methods
private function f1():void {} publicfunction f2():void {} protected function f3():void {}
Class B inherita A
Class C: ... var instanceofA:A -= new A(); instanceofA.a = 10; ... var instanceofB.b = new B(); instanceofB.a = 99;
function func():void { c=100; }
In the previous figure, Class A defines three properties a, b, and c, which are private, public, and protected, respectively. Class B, which is a subclass of A, may freely access and modify the public and protected variables of Class A as if it were its own, though it may not access Class A's private properties. Class C, which is a not related to Class A or B, may only access A's or B's public properties. Note that the inherited public properties, such as A may be accessed via an instance of B even though it is defined in A. This is because of the subclass definition that B inherits all public and protected of its parent class. These access rules apply exactly the same way for methods as it does for properties.
[ 251 ]
Introduction to FlashBuilder and AS3
Taming the inheritance monster
Now that we have seen the basic building blocks of a class, let us write a whole class and even go over an example of how inheritance can help us manage complexity. Imagine we are writing an MMO RPG (Massively Multiplayer Online Role Playing Game). If you have played one, you would have definitely come across all kinds of monsters that basically have the same behavior, that is, you engage them in a battle and there is a fight to the death, unless you have taken on something more than you can handle, in which case you would make a break for it! Monsters may vary in strength based on their level, basically how much you can hurt them or how much they can hurt you. A more interesting difference would be in the way they behave. For example, do they chase after you when you try to run, do they attack you as soon as you get near, or wait for you to attack first? And in the case of multiple players slaying a monster, does it attack back only the first attacker or switch to attack the player that attacked last? Or it could be smart enough to figure out the weakest of the players first, and so on. How can we model this in our game implementation? Let us go through a simple example of implementing this in an object-oriented way. The first thing we would do is take all the commonality of a monster and create what is called a base class. For any specific kind of monster we would like to create, we subclass from the base and code the specific behavior into it. Sounds easy, but you don't actually get how to go about it, do you? Well, let's jump right into the example. Let's say we want to implement two kinds of monsters, one called PassiveMonster and another called ActiveMonster. The following illustrates the specific behavior: Monster
PassiveMonster
ActiveMonster
Waits for someone to attack first Does not chase Attacks only the first attacker
Starts attacking anyone thats close Chase the attacker Attacks the last attacker
[ 252 ]
Appendix A
Let us see how we would implement a real-world monster class. The example monster described here will have the following properties. Different monsters can have a different attack power or cause more damage to the player when in battle. Game designers would typically place monsters with a higher attack power at higher levels within the game. In this example, we will keep it simple—each monster will have a specific attack power, but in real-world games, things can get quite complex. For example, the damage caused may depend on the type of monster and the player type in a battle, whether a monster is a magical kind that can hurt a warrior more than a wizard class avatar. Further, it could also depend on the defensive items that the avatar wears or possesses, and other factors. We will also give the monster the ability to heal itself over time. A variety of monster objects can be created with different values. For example, a higher level monster could heal itself quickly, making the player really fight hard to defeat it, or it might take multiple players to slay it. Finally, we will also give the monster a respawn capability, meaning that it could come back to life a certain time after the monster had died. Again, the rate at which they respawn gives an opportunity to create a variety of monster objects. Now, for all the different types of monsters, would we make a subclass for each? We could have a thousand different varieties and a subclass for each! Nah! What we do is let the monster object accept these values during their creation via the constructors. The values are then stored inside the class that will never change. Thus each monster could be different based on what values were passed in during its instantiation. So first, let's examine the base monster class and then illustrate how we can create passive and active monsters by subclassing from the parent monster class. Here is a complete listing for the monster class. There are comments embedded in the code that you are encouraged to read and an added explanation follows the listing. package world { public class Monster { /** * Stores the amount of damage the monster * can cause an avatar. */ protected var m_attackPower:Number; /**
[ 253 ]
Introduction to FlashBuilder and AS3 * The remaining life of the monster. A value * equal to or below zero is considered as dead. */ protected var m_life:Number; /** * The monster maximum life value when * it comes to life. Higher level monsters * would have a higher value. */ protected var m_maxLife:Number; /** * A variable to store the player (Avatar) that * its currently engaged in a battle. */ protected var m_avatar:Avatar; /** * The time value in milli-seconds that the * last update was called. This is enable us * to determine the actual time elapsed between * the current and previous update call */ protected var m_lastUpdate:Number; /** * A value that determines how quickly the * monster will re-spawn after it dies. */ protected var m_respawnRate:Number; /** * A count down timer that starts after the monster dies. * When the value reaches zero or below, it is respawned. */ protected var m_timeToRespawn:Number; /** * During the course of battle, the monster may * rejuvinate its life. This value determines how * quick. Higher level monsters may have higher value. */ protected var m_lifeRestoreRate:Number; /** * A count down timer, when this reaches zero or below * the monster gets more life. */ protected var m_lifeRestore:Number; /** * Constrctor * * @param attackPower Amount of damage * @param maxLife * @param respawnRate [ 254 ]
Appendix A * @param lifeUpdateRate * */ public function Monster(attackPower:Number, maxLife:Number, respawnRate:Number, lifeUpdateRate:Number) { // Simply initialize the class properties // with what is passed in m_attackPower = attackPower; m_maxLife = maxLife; m_life = m_maxLife; m_respawnRate = respawnRate; m_lifeRestoreRate = lifeUpdateRate; } /** * This method could be called by another place * in the game code to inflict damage to the monster * by an avatar * * @param damage Amount of damage * @param avatar The attacking avatar * */ public function causeDamange(damage:Number, avatar:Avatar):void { // If alive decrease life, check for death if ( isAlive() ) { m_avatar = avatar; m_life -= damage; if ( isDead() ) { death(); } } } /** * A private method that will reset the * count down timer to respawn. * */ protected function death():void { if ( isDead() ) { m_avatar = null; // forget the attacker m_timeToRespawn = m_respawnRate; } } /** * A Handy method to to check if monster is dead [ 255 ]
Introduction to FlashBuilder and AS3 * * @return true if dead, false otherwise * */ public function isDead():Boolean { return ( m_life <= 0); } /** * A Handy method to to check if monster is alive * * @return true if alive, false otherwise * */ public function isAlive():Boolean { return ( m_life > 0); } /** * Must be called from within the main game loop * */ public function update():void { var now:Number = new Date().getMilliseconds(); var delta:Number = (now - m_lastUpdate); respawn(delta); heal(delta); if ( m_life > 0 ) { if ( m_avatar != null ) { if ( m_avatar.isAlive() ) { m_avatar.causeDamage(m_attackPower); } } } m_lastUpdate = now; } /** * A private method to update the respawn * called from within the update method * * @param delta Time difference * between this and the previous * update call * */ protected function respawn(delta:Number):void { // If alive, do nothing // if the count down goes to zero or below // restore the life if ( isDead() ) { m_timeToRespawn -= delta; [ 256 ]
Appendix A if ( m_timeToRespawn < 0 ) { m_life = m_maxLife; } } } /** * A private method to heal the monster * Called from within the update method * * @param delta Time difference * between this and the previous * update call * */ protected function heal(delta:Number):void { // If monster has already full life, do nothing // else, if the countdown has reached zero // award a life point. if ( m_life < m_maxLife ) { m_lifeRestore -= delta; if ( m_lifeRestore < 0 ) { m_life++; // Assume a life unit is 1 !! // avoid going over the max life. if ( m_life > m_maxLife ) m_life = m_maxLife; m_lifeRestore = m_lifeRestoreRate; } } } } }
We see that in the properties section, we define all the needed properties and they are initialized in the class constructor. Note that when you create your army of monsters, you are creating a number of objects of this class similar to the next code: var m:Monster = new Monster(10, 10, 10, 10);
Each of the objects has its own copy of properties and any modification that happens to these properties is independent of other monster objects, so each monster can be at different levels of life, attacker, etc.
[ 257 ]
Introduction to FlashBuilder and AS3
We also use a class datatype called Avatar in the causeDamage method for the purpose of explaining the monster class inheritance. We will simply assume Avatar is another class that holds properties required for an avatar and that just like any other class, it too offers a set of methods to interact with. For example, you will see that in the attack method, the avatar can respond to isAlive and causeDamage methods. Although the method names are identical to that of the monster, they are completely unrelated and have their own implementation and are not listed here. The update method is special in that it is called from a top level game loop; the game loop is explained in more detail in the next section. With just the Monster class, you can imagine the number of different kinds of monsters you can create. Although they could be different in their strength, healing rate, etc., how can we leverage the code in this class to create another class of monster? (No pun intended!) Let's say we want to create the passive monster from our initial specification and want to keep attacking the first attacker even if another player attacks until either the monster or the player dies or runs away. Closely examine the causeDamageMethod repeated as follows: public function causeDamange(damage:Number, avatar:Avatar):void { // If alive decrease life, check for death if ( isAlive() ) { m_avatar = avatar; m_life -= damage; if ( isDead() ) { death(); } } }
Notice that the m_attacker is updated every time a player attacks the monster, which means the monster starts to attack the last player that attacked it. If we want to remember the first attacker, we need to modify the method to something like the following: public function causeDamange(damage:Number, avatar:Avatar):void { // If alive decrease life, check for death if ( isAlive() ) { if ( m_avatar != null ) { m_avatar = avatar; } [ 258 ]
Appendix A m_life -= damage; if ( isDead() ) { death(); } } }
We simply add an if condition to check whether we are already engaged in a battle with an attacker; in that case, we don't update the attacker. Note that with a simple change like shown previously, we are able to create a monster with different behavior and reuse most of the code in our base Monster class. So it's time to create the subclass for the passive monster. To do so, we right-click on the folder that we want the file to be placed under and select ActionScript Class under the New menu item. Enter PassiveMonster for the Name: and also Monster for Superclass: as shown next:
[ 259 ]
Introduction to FlashBuilder and AS3
This will generate our PassiveMonster and the listing looks like the following after inserting its own version of the causeDamage method: package world { public class PassiveMonster extends Monster { public function PassiveMonster( attackPower:Number, attackRate:Number, maxLife:Number, respawnRate:Number, lifeUpdateRate:Number) { super(attackPower, attackRate, maxLife, respawnRate, lifeUpdateRate); } /** * This method could be called by another place * in the game code to inflict damage to the monster * by an avatar * * @param damage Amount of damage * @param avatar The attacking avatar * */ public override function causeDamange( damage:Number, avatar:Avatar):void { // If alive decrease life, check for death if ( isAlive() ) { // If we don't have an attacker set // then set the attacker // If the avatar is dead or ran away // or left the game, the logic to reset the // avatar is handled in the attack method if ( m_avatar != null ) { m_avatar = avatar; } m_life -= damage; [ 260 ]
Appendix A if ( isDead() ) { death(); } } } } }
Notice that the Flash Builder was nice enough to create the constructor that matches with the one in its parent class. Also, notice the keyword override for the causeDamage method. Here we are telling the compiler that we are overriding the behavior of the causeDamage method from the super class. Omitting the override keyword produces a compiler error. Creating a new passive monster object would look something like the following: var pm:PassiveMonster = new PassiveMonster(10, 10, 10, 10, 10);
Note that via inheritance, the objects of our new class responds to all the methods defined in the super class, such as update, isAlive, isDead, etc. Now how about that ActiveMonster? From our specification, we want it to attack back the last player that attacked, so we may leave the causeDamage as is without overriding it in the ActiveMonster subclass. We also want the monster to actively attack when some player (avatar) is close by. For this we invent a new method called scan. Also, we will invent a chase method for chasing a player, in case the player tries to run. The implementation is not shown here, as it would be heavily dependent on the game specifics and more so because we want to focus on illustrating inheritance rather than an actual game implementation at this point. One way to implement this is to update our base Monster class as follows (notice the bolded code and the two new methods): public function update():void { var now:Number = new Date().getMilliseconds(); var delta:Number = (now - m_lastUpdate); scan(delta); // scan for a new attacker chase(delta); // chase the attacker respawn(delta); heal(delta); if ( isAlive() ) { attack(delta); } [ 261 ]
Introduction to FlashBuilder and AS3 m_lastUpdate = now; } /** * Scans for any player nearby. * Called from within the update method * */ protected function scan(delta:Number):void { } /** * Chase the current attacker, if not close enough. * Called from within the update method * */ protected function chase(delta:Number):void { }
In the base Monster class, we leave the implementation empty and implement the methods in the ActiveMonster class by overriding the two methods. As the passive monster also inherits what goes into the Monster class, we leave the scan and chase methods empty so that we don't change the behavior of the passive monster. Here is a listing of our ActiveMonster: package world { public class ActiveMonster extends Monster { public function ActiveMonster( attackPower:Number, attackRate:Number, maxLife:Number, respawnRate:Number, lifeUpdateRate:Number) { super(attackPower, attackRate, maxLife, respawnRate, lifeUpdateRate); } /** * Scans for any player nearby. [ 262 ]
Appendix A * Called from within the update method * */ protected override function scan(delta:Number):void { if ( m_avatar == null ) { // scan the area with a radius // set m_avatar to the nearest one } // else we already are in engagement with // an opponent } /** * Chase the current attacker, if not close enough. * Called from within the update method * */ protected override function chase(delta:Number):void { if ( m_avatar != null ) { // Check if the avatar is still accessible // or even in the game // If we determine the avatar is 'far' but still // within range, we move closer to the avatar. } } } }
To create one of these monsters would be something like: var am:ActiveMonster = new ActiveMonster(10, 10, 10, 10, 10);
To highlight an inheritance magic that you should be aware of is that of calling the update method on an active monster object. The execution goes to the parent class update method: am.update();
But the execution drops to the ActiveMonster's implementation of scan and chase methods because they are overridden and the object is of type ActiveMonster. After finishing the scan or chase method, the update method continues to execute normally in the parent's class implementation.
[ 263 ]
Introduction to FlashBuilder and AS3
Suppose you wanted to override a method and still call the parent's method; you could do something like the following: protected override function scan(delta:Number):void { // Do something different here... and // ... call the parent's method super.scan(delta); }
The super is another keyword and it always points to the current object's super class. So you can call any method of the super class, but by invoking the method on the super. The reader is encouraged to create their own monster type by modifying the previous code and using the subclassing technique.
Interface class
Another great concept from the object-oriented design is what is called the interface. An interface is like a class, but with no variables. Instead is has only methods with no implementation in them. You would simply define a bunch of method signatures and give it a name similar to a class. Flash Builder allows you to quickly create an interface similar to how you would create a class. Right-click on the package that you want to create the interface in and choose ActionScript Interface under the New menu item. Here is an example declaration: package world { public interface PhysicsItem { // ... function getWeight():Number; function updateVelocity(delta:Number):void; function getSpeed():Number; // ... } }
Note that all methods are by default public; you may not apply scoping attributes. In order to have a class implement the interface, you must specify that the class implements the interface and also provide concrete implementation for each of the methods in the interface. [ 264 ]
Appendix A
Here is an example to specify that a class implements certain interface: public class Vehicle extends Sprite implements PhysicsItem
Now why would you want to define a class that has no properties and no method implementations except its signature? Here is a good example as to why. Imagine you have created a class that simulates physics in your game. It means depending on the physical attributes of objects in your game; your physics engine will apply gravity, collision reactions among objects, and so forth. Now, the various objects may have its own inheritance structure while your physics engine should be generic enough to deal with objects no matter what the specific object's inheritance looks like. But your physics engine demands certain properties and methods to exist in each of the object that it has to deal with. You can say all objects must conform to an interface so that the physics engine can interact with it. The interface would be to get mass or weight of an object, get frictional properties, adjust the velocity and acceleration on these objects and so on. A clean object-oriented way to handle this would be to create an interface and then have all the objects that need to be controlled by the physics engine implement the interface. A class can inherit only from one parent class, but can implement several interfaces.
Static properties and methods
ActionScript 3 also allows us to define a static method or a class property by using the static keyword in its declaration. The static allows you to use or access the properties or methods without having to create an object for a class. Below are some sample declarations: private static const DELTA:int = 10; protected static var MAX_SPEED:Number = 100.0; public static var DEFAULT_NAME:String = "Guest";
Note that just like regular (non-static) properties, static properties and methods may be used in conjunction with scoping attributes and used for constants and variables. However, there is one restriction that static properties and methods may not be overridden by a subclass.
[ 265 ]
Introduction to FlashBuilder and AS3
An example static method declaration is shown next: public static function getDefaultName():String { return DEFAULT_NAME; }
Important to note here is that static methods may access their own class properties, provided they are also static. Accessing static properties and methods from outside the class requires specifying the class name instead of the variable (instance) of a class. For example: Avatar.DEFAULT_NAME; Vehicle.MAX_SPEED;
[ 266 ]
Graphics Programming in AS3 Flash programming is easy. By far, Flash delivers the easiest and most intuitive programming environments to quickly and efficiently develop stunning multimedia 2D applications compared to other platforms in the market. We will learn some cool Flash graphics programming basics, and its tips and tricks. We will cover the basic building block of Flash graphics—the sprite, learn how to draw stuff using them on a Flash stage, and interact with it via mouse and keyboard. We will also see how we can embed images and put them on stage. This chapter is a must for programmers who have not programmed Flash in AS3.
Flash object hierarchy
The following is a subset of object hierarchy offered by Flash. There is no need to be a master of each and every class, but the ones we will be dealing with a lot are Sprite, TextField, SimpleButton, and label. Readers are encouraged to get more detailed information about them from Adobe documentation.
Graphics Programming in AS3
Object
The class Object is a root class for every object that you define in the AS3 world. If you were to define your own object with no parent class, AS3 assumes that the class is Object's subclass. Object enables us to create new objects using the new operator. For example, in the following code snippet, an instance of Sprite object is created and assigned to the variable s: var s:Sprite = new Sprite();
EventDispatcher
This class mainly adds the event handling capabilities to all the classes underneath it. This is done by implementing the IEventDispatcher interface. Thereby your own subclasses of a Sprite allow you to add and remove event handlers, as you will see later in this chapter.
DisplayObject and DisplayObjectContainer
DisplayObject provides the functionality whereby an instance of any of its subclass
may be part of the display list. A display list is an ordered set of objects within an instance of a DisplayObjectContainer. The display objects contained within a display object container are termed as children. Thus, from the previous hierarchy, you can see that a sprite is both a DisplayObject and a DisplayObjectContainer, which means that sprite objects may contain other sprite objects forming a tree of object, which is ordered. As you will see in the detailed section on sprite, programmatically, you may manipulate this tree of objects and see how Flash makes it all so very intuitive.
InteractiveObject
InteractiveObject encapsulates objects that the user interacts with, such as TextField and SimpleButton. Also, notice that because they do not inherit from DisplayObjectContainer, the subclasses of InteractiveObjects cannot contain
other objects as children; they may only be used as the leaf objects in the display object tree.
[ 268 ]
Appendix B
Sprite, in detail
Let us start with explaining what a sprite is. A sprite is a commonly used basic term in 2D graphics, which usually refers to any visual element on the stage such as images, or buttons, text field, etc. Sprite does not usually have a strict definition. Sprites are used anywhere from drawing your game's characters, to splash screens to a score display, etc. Sprites in AS3 can usually be moved around the screen, responding to mouse events as well as keyboard events.
Which way is up?
The following illustrates the stage of any Flash application. Notice the origin is at the top left-hand corner and how values for X and Y increase. The larger the value of X farther to the right of the screen larger is the value of Y lower to the bottom of the screen.
A sprite is drawn onto a stage. The stage is your area where you are allowed to draw and receive mouse and keyboard events. Each sprite among numerous other properties have x, y, width, height, and children. In AS3, there could be hundreds of sprites being displayed at any given moment. However, it is good to know that more the number of sprites on the stage and moving lower is the performance.
[ 269 ]
Graphics Programming in AS3
Each sprite must have a parent if it is being displayed on the screen. A sprite may have as many children as needed. The hierarchy of parent-child relations may be arbitrarily deep. The hierarchy of the sprites is as follows:
If the previous image shows the ordering of a group of sprites, the following diagram is an example of how the sprites may be arranged front to back on the screen:
The sprite can be removed from its parent and added as a child of another sprite. In this case, the children of the sprite that was moved will remain as children and thus would also be moved effectively along with its parent. Furthermore, the children are kept in an ordered array. The order in which they are stored is the same order the sprites are drawn on the stage by the Flash runtime. For example, the root sprite is drawn first, and then its children are drawn in the order that they are sorted. The order is controlled by the programmer, as you will see shortly.
[ 270 ]
Appendix B
When a sprite moves, all the children of that sprite will also move. Also, changing the size of a sprite changes all of its children's sizes as well. Every sprite has an x and y. The origin of the coordinate space of a sprite is relative to that of its parent.
In the previous figure, you see that the root sprite is drawn on the stage at X=10; Y=10. The sprite has one child and is also with X=10; Y=10. Notice that even the x and y values of parent and child are identical and are not drawn in the same location. The reason is the parent sprite is drawn in the stage coordinate system whose origin is at the top right-hand corner and the child sprite is drawn in the coordinate system of its parent sprite. The parent coordinates system has its origin just like that of a stage at its top right-hand corner. It is important to understand these basic concepts, as we will be drawing all the sprites on the stage programmatically without the help of any designer.
Let the fun begin
Let us make sprites to display on the screen. But first, let us see how we can create a project in Flash Builder 4. Create a new project by opening Flash Builder and under the File menu, choose New and then choose ActionScript Project.
[ 271 ]
Graphics Programming in AS3
In the dialog that pops up, give your project a name as follows:
And then click on Finish. The following illustrates the code to draw your first sprite on the stage: public function SpriteTest() { var s:Sprite = new Sprite(); s.graphics.beginFill(0xFF0000); s.graphics.drawCircle(0, 0, 10); s.graphics.endFill(); s.x = 25; s.y = 25; addChild(s); }
The above is a simple example to create a sprite, which in this example is a red circle at position x=25 and y=25. Notice the call to addChild, which you must call in order for Flash to put it on the stage. The main class SpriteTest, in this case, is a sprite and is added to the stage automatically to the stage by Flash runtime. Flash Builder automatically creates the main class with the same name as the name we specify for the ActionScript project name. When you run the project, the program execution starts in this main class's constructor. [ 272 ]
Appendix B
Before you run the project, right-click on the project, choose Properties, and then select ActionScript compiler. For the additional compiler arguments, enter the following: -default-size 100 100 -default-background-color 0xFFFFFF
The previous code asks the compiler to make the stage 100 by 100 pixels and make the background for the HTML page white.
Events
You may be thinking that if the program execution has started in the constructor of the main class, what happens after completing the execution of the constructor? How can we add more code for more behavior? Flash has something called the event system. An event system is a clean, simple, yet powerful paradigm to create large, complex, interactive applications, especially suited for game programming.
Simply put, the event system consists of three parts: the event itself, an event generator, and an event listener. Any part of the code may generate events; there can be any number of listeners for the same event and you may define your own events. There is, however, a great number of events that the Flash runtime has already defined. For example, there are events based on mouse clicks and movements, keyboard events that are generated when a key is pressed and released, and so on. We will explore each of these in detail in the following sections, specifically, timers, keyboard events, and mouse. After having explored the events that are defined in Flash, we will see how we can define our own events. In order to listen to the events, we need to do two things. First off, we need to let Flash runtime know that you are interested in a specific event and provide a callback function. When any other part of the system generates the event you have set up a listener to, the callback function is called and the event object is passed in. The callback function then does something useful. For example, you can set up a listener when the mouse button is clicked, and in the callback function, you handle the event.
[ 273 ]
Graphics Programming in AS3
Timers
It is one of the most useful objects in game development and Flash programming makes it all very easy to set up. Basically, you create a timer object and tell it to call your function often. Let us add a timer to our previous sprite test example, and every time the timer is fired, we will make the sprite move in a random direction. We will also take care to check if the sprite is off the stage to reverse its course so that it stays on the stage, that is, it is visible. package { import flash.display.Sprite; import flash.events.TimerEvent; import flash.utils.Timer; public class SpriteTest extends Sprite { private const HEIGHT:int = 100; private const WIDTH:int = 100; private private private private
var var var var
m_sprite:Sprite; m_timer:Timer; m_dx:Number = 1; m_dy:Number = 2;
public function SpriteTest() { m_sprite = new Sprite(); m_sprite.graphics.beginFill(0xFF0000); m_sprite.graphics.drawCircle(0, 0, 3); m_sprite.graphics.endFill(); m_sprite.x = 25; m_sprite.y = 25; addChild(m_sprite); // Set up the timer and fire it m_timer = new Timer(10, 0); m_timer.addEventListener(TimerEvent.TIMER, onTimer); m_timer.start(); } private function onTimer(event:TimerEvent):void { moveSprite(); } private function moveSprite():void { // Check the bounds for x if ( m_sprite.x < 0 || m_sprite.x > WIDTH ) [ 274 ]
Appendix B m_dx *= -1; // Check the bounds for y if ( m_sprite.y < 0 || m_sprite.y > HEIGHT ) m_dy *= -1; // affect the change to the sprite m_sprite.x += m_dx; m_sprite.y += m_dy; } } }
What you just saw was the listing for the entire program that makes a red ball bounce off the walls, continuously. You will notice that we define the HEIGHT and WIDTH simply to avoid magic numbers all over the code. We also define the sprite as a class member or property so that we can access it from different methods of the class and the timer object itself. We also define the dx and dy, which determine the direction and speed of the ball. In this example, we keep the speed constant to 1 and simply reverse the direction when the ball reaches any of the borders in the moveSprite function. In the constructor, we create a new timer, whose constructor takes two parameters first being the interval in milliseconds and the second being the number of times to fire the callback. Setting it to zero will cause the timer to fire infinite number of times until the program is terminated. In this example, the interval is defined to be 10, which means after every 10 milliseconds the callback will be fired for us to move the ball. You may experiment with this interval to see how fast or slow the timer callbacks are fired. Notice that the callback is set up to call the onTimer method that you define as a member function of the class. The callback in this example simply calls the moveSprite function. The timer also supports a stop function and many other useful methods that the reader is encouraged to fully explore and experiment.
Trace
Although Flash Builder provides powerful debugging controls, it is often useful to print something to the console. Flash provides a simple method called trace that will help you do just that.
[ 275 ]
Graphics Programming in AS3
The following prints Hello World to the console: trace("Hello World");
The following prints numbers from 0 to 99: for ( var i:int=0; i<100; i++ ) trace("Value of i: " + i);
Embedding pictures
Before we continue onto mouse and keyboard events, let's look at how we can include images as part of the project as well as read and use them in our program. Let us embed two pictures, one for the background and another in place of a simple red circle. Flash Builder lets us simply drag a PNG or a JPG file into the project, but before we drag it in, let us be organized about it and make a directory for all the assets. To do this, you may right-click on the project folder and choose folder under new menu item:
In the previous screenshot, you see that there are two PNG files (I personally like .png because you can have transparency) under the rsrc folder. Below is how you create a class that represents the image: public class SpriteTest extends Sprite { [Embed(source="rsrc\\back.png")] private var Back:Class; private const HEIGHT:int = 100; private const WIDTH:int = 100;
[ 276 ]
Appendix B
The following is a private function that is called from the constructor of the SpriteTest class: private function addBackground():void { var bma:BitmapAsset = new Back() as BitmapAsset; var back:Sprite; back = new Sprite(); back.graphics.beginBitmapFill(bma.bitmapData); back.graphics.drawRect(0, 0, WIDTH, HEIGHT); back.graphics.endFill(); addChild(back); }
The result of the earlier changes looks like what's shown next:
Notice that the background is added first into the stage and then the bee is. The order of adding the sprites is important. By reversing the order, you will not be able to put the bee on top of the background. Here is the full listing of our program so far: package { import flash.display.Sprite; import flash.events.TimerEvent; import flash.utils.Timer; import mx.core.BitmapAsset; public class SpriteTest extends Sprite { [Embed(source="rsrc\\back.png")] [ 277 ]
Graphics Programming in AS3 private var Back:Class; [Embed(source="rsrc\\bee.png")] private var Bee:Class; private const BEE_HEIGHT:int = 50; private const BEE_WIDTH:int = 42; private const HEIGHT:int = 288; private const WIDTH:int = 288; private private private private
var var var var
m_sprite:Sprite; m_timer:Timer; m_dx:Number = 1; m_dy:Number = 2;
public function SpriteTest() { addBackground(); m_sprite = createBee(); addChild(m_sprite); // Set up the timer and fire it m_timer = new Timer(50, 0); m_timer.addEventListener(TimerEvent.TIMER, onTimer); m_timer.start(); } private function addBackground():void { var bma:BitmapAsset = new Back() as BitmapAsset; var back:Sprite; back = new Sprite(); back.graphics.beginBitmapFill(bma.bitmapData); back.graphics.drawRect(0, 0, WIDTH, HEIGHT); back.graphics.endFill(); addChild(back); } private function createBee():Sprite { var bma:BitmapAsset = new Bee() as BitmapAsset; var s:Sprite; s = new Sprite(); s.graphics.beginBitmapFill(bma.bitmapData); s.graphics.drawRect(0, 0, BEE_WIDTH, BEE_HEIGHT); s.graphics.endFill(); return s; } private function onTimer(event:TimerEvent):void { moveSprite(); [ 278 ]
Appendix B } private function moveSprite():void { // Check the bounds for x if ( m_sprite.x < 0 || m_sprite.x > WIDTH ) m_dx *= -1; // Check the bounds for y if ( m_sprite.y < 0 || m_sprite.y > HEIGHT ) m_dy *= -1; // affect the change to the sprite m_sprite.x += m_dx; m_sprite.y += m_dy; } } }
Mouse events
Every game user interface needs to handle mouse events. The mouse is an integral part of any application, especially a game. You must have a very good understanding of Flash mouse events that you can trap and take action for.
What do we need to handle mouse events for?
In a multiplayer game, mouse handling starts at the login panel. For example, clicking on an OK button, the user expects the interface to log into the server; alternatively, clicking on a Cancel button, the user expects to quit the game. Although Flash does a lot of mouse handling for you, there are numerous places that you need something done specific when the user operates the mouse within your game. Flash, for example, does all the work when the user clicks inside a text field to show the cursor indicating the user that it is ready to accept the keyboard input. Handling mouse clicks is one thing. Often in your game you may need to track the mouse position; you may also need to handle events when the mouse enters or leaves a specific area on the screen.
[ 279 ]
Graphics Programming in AS3
How to register for a mouse event
Flash lets you handle the mouse event in an elegant way. Even if you had to handle mouse events on 50-odd objects on the screen, with the combination of object-oriented design, things can be kept clean and manageable. First off, you need to determine which sprite, called the target, you want to trap the mouse events in. Then you simply need to add event listener, like we saw for the timers in the previous section. First we create a simple sprite and then we register a listener for the mouse event. We also write a listener callback function that simply prints the log to the console. package { import import import import
flash.display.Sprite; flash.events.Event; flash.events.MouseEvent; flash.ui.Mouse;
public class Main extends Sprite { public function Main():void { if (stage) init(); else addEventListener(Event.ADDED_TO_STAGE, init); } private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); // entry point var s:Sprite = new Sprite(); s.graphics.beginFill(0x0000FF); s.graphics.drawCircle(0, 0, 30); s.graphics.endFill(); s.addEventListener(MouseEvent.CLICK, onClick); s.x = 50; s.y = 50; addChild(s); } public function onClick(e:MouseEvent):void { trace("Mouse Click Detected."); } } } [ 280 ]
Appendix B
What are the events we can handle in Flash? Among plenty of mouse events that Flash offers, let us talk about a few important ones here: Event
Description
CLICK
When the mouse was clicked on a sprite
MOUSE_OVER
When the mouse moves over a sprite
MOUSE_OUT
When the mouse moves out of bounds of a sprite
MOUSE_MOVE
When the mouse moves around within the bounds of a sprite
MOUSE_DOWN
When primary mouse button is held down within the bounds of a sprite
MOUSE_UP
When the primary mouse button is released within the bounds of a sprite
For a complete list of mouse events, please refer to Flash framework documentation. The event definitions are defined in the MouseEvent class in flash.events. Try the following example and experiment how and when Flash fires those callbacks for the previous mouse events. You are encouraged to add more callbacks to other mouse events that are not mentioned here: package { import flash.display.Sprite; import flash.events.MouseEvent; public class MouseEvents extends Sprite { public function MouseEvents() { var s:Sprite = new Sprite(); s.graphics.beginFill(0xFFFF00); s.graphics.drawCircle(0, 0, 10); s.graphics.endFill(); s.addEventListener(MouseEvent.CLICK, onMouseClick); s.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown); s.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove); s.addEventListener(MouseEvent.MOUSE_OUT, onMouseOut); s.addEventListener(MouseEvent.MOUSE_OVER, onMouseOver); s.addEventListener(MouseEvent.MOUSE_UP, onMouseUp); addChild(s); } private function onMouseClick(e:MouseEvent):void { trace("onMouseClick"); [ 281 ]
Graphics Programming in AS3 } private function onMouseMove(e:MouseEvent):void { trace("onMouseMove"); } private function onMouseOver(e:MouseEvent):void { trace("onMouseOver"); } private function onMouseUp(e:MouseEvent):void { trace("onMouseUp"); } private function onMouseDown(e:MouseEvent):void { trace("onMouseDown"); } private function onMouseOut(e:MouseEvent):void { trace("onMouseOut"); } } }
Handling mouse events in many objects
Consider a screen with many sprite buttons. How do we know what sprite was clicked on? Do we add one listener callback for all the sprites? You could, but there is a better way: an object-oriented way to handle it makes things elegant. Let's start out with our own class that is a subclass of Sprite. Call it MouseTrap, which does three things: draws itself, registers for mouse clicks, and prints when it gets a click. To distinguish among different MouseTrap objects, we also give it an ID, which is printed when the mouse is clicked. We will also add code in the main class that creates these MouseTrap objects and put it on the screen by calling addChild. The project created for this example has the following compiler settings: -default-size 300 300 -default-background-color 0xFFFFFF
Here is the listing: MainClass: package { import flash.display.Sprite; public class SpriteMouseClick extends Sprite [ 282 ]
Appendix B { public function SpriteMouseClick() { for ( var i:int=0; i<5; i++ ) { var mt:MouseTrap = new MouseTrap(i); mt.x = 20*i + 10; mt.y = 20*i + 10; addChild(mt); } } } }
MouseTrap: package { import flash.display.Sprite; import flash.events.MouseEvent; public class MouseTrap extends Sprite { private var m_id:int; public function MouseTrap(id:int) { super(); m_id = id; graphics.beginFill(0x000000); graphics.drawCircle(0, 0, 10); graphics.endFill(); addEventListener(MouseEvent.CLICK, onMouseClick); } private function onMouseClick(e:MouseEvent):void { trace("onMouseClick ID: " + m_id); } } }
[ 283 ]
Graphics Programming in AS3
Where is the Mouse?
Often we would like to know the exact position of where the mouse was clicked. Luckily, you don't have to look far; the MouseEvent object that is passed into the callback carries this information for you. Further, it carries two sets of x and y. The first set is called local coordinates and the other the global or the stage coordinates. Yes, you guessed it right—local x and y is with respect to the sprite's coordinates and the stage x and y is relative to the stage coordinates. private function onMouseClick(e:MouseEvent):void { trace("onMouseClick ID: " + m_id); trace("Local Location X: " + e.localX + " Y: " + e.localY); trace("Stage Location X: " + e.stageX + " Y: " + e.stageY); }
Drag-and-drop
Not all games need drag-and-drop, but it is a useful trick to keep under your hat. Let's see how easy it is to drag a sprite around on the stage with the mouse. As you are now already proficient in handling mouse events, there are just two methods that you need to know about: startDrag() stopDrag()
These two methods are defined in the Sprite class. When you call startDrag() on the sprite, the Flash runtime will move the sprite with the mouse position. The stopDrag() will make the Flash runtime let go of the sprite that is being dragged. It will also let go if startDrag() is called on another sprite. StartDrag() takes two parameters: lockCenter, if true (false by default) locks the center of the sprite to the mouse cursor; the second parameter, called the bounds, specifies a rectangular area that the sprite is not allowed to move out of. Note that the bounds must be expressed relative to the sprite's parent coordinates.
So, what is a good place to call startDrag and stopDrag? Since we can detect the mouse down and mouse up events on a given sprite, we can simply add it there. A full listing of the example is as follows. The first example uses default values during the startDrag and the second one will constrain the dragging to specified bounds.
[ 284 ]
Appendix B
The first example, which you should be able to grasp easily by now, will create a simple sprite and add it to the stage. Also, we register the mouse down and mouse up events, and in the callbacks, we make calls to start and stop dragging the sprite.
Note the compiler options: -default-size 300 300 -default-background-color 0xFFFFFF package { import flash.display.Sprite; import flash.events.MouseEvent; public class MouseDrag extends Sprite { // The sprite that will be moved around private var m_s:Sprite = new Sprite(); public function MouseDrag() { m_s.graphics.beginFill(0x555555); m_s.graphics.drawCircle(0, 0, 25); m_s.graphics.endFill(); addChild(m_s); m_s.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown); m_s.addEventListener(MouseEvent.MOUSE_UP, onMouseUp); } private function onMouseDown(e:MouseEvent):void { m_s.startDrag(); } private function onMouseUp(e:MouseEvent):void { m_s.stopDrag(); } } }
Here is the listing for constraining the drag to the bounds. In this example, we first create a rectangle called m_bounds, which we pass into the startDrag() method. We also put another sprite b to simply highlight the bound area. Note that something different happens when you move the mouse within the circle. The cursor icon is changed and becomes a hand. There are two lines of code that does that. Can you figure out which ones?
[ 285 ]
Graphics Programming in AS3
Compiler options: -default-size 300 300 -default-background-color 0xFFFFFF package { import flash.display.Sprite; import flash.events.MouseEvent; import flash.geom.Rectangle; public class DragMouseInBounds extends Sprite { // Bounds within which the sprite may not be dragged out private var m_bounds:Rectangle = new Rectangle(0, 0, 100,100); // The sprite that will be moved around private var m_sprite:Sprite = new Sprite(); public function DragMouseInBounds() { var b:Sprite = new Sprite(); b.graphics.beginFill(0x999999); b.graphics.drawRect(0, 0, 100, 100); b.graphics.endFill(); addChild(b); m_sprite.graphics.beginFill(0x555555); m_sprite.graphics.drawCircle(0, 0, 10); m_sprite.graphics.endFill(); m_sprite.x = 50; m_sprite.y = 50; addChild(m_sprite); m_sprite.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown); m_sprite.addEventListener(MouseEvent.MOUSE_UP, onMouseUp); m_sprite.buttonMode = true; m_sprite.useHandCursor = true; } private function onMouseDown(e:MouseEvent):void { m_sprite.startDrag(true, m_bounds); } private function onMouseUp(e:MouseEvent):void { m_sprite.stopDrag(); } } }
[ 286 ]
Appendix B
Keyboard events
Similar to handling other events such as mouse, timers, and others, keyboard key handling is no different. Note that in most cases you don't need to handle it. For example, in case of text field, text area, scroll bars, and more, you don't need to handle the key strokes explicitly, as Flash does that for you. However, for key handling to move your character on the screen, pick stuff up in the game, or fire a weapon, you do need to handle it on your own. The following is a simple example on handling keys on a sprite: package { import flash.display.Sprite; import flash.events.KeyboardEvent; import flash.ui.Keyboard; public class Keyboard extends Sprite { public function Keyboard() { // Set up the listener for the key board events stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp); } // The callback fired when the key is pressed down private function onKeyDown(e:KeyboardEvent):void { trace("Event Key Down. Key Code: " + e.keyCode + " Char Code: " + e.charCode); } // The callback fired when the key is let go private function onKeyUp(e:KeyboardEvent):void { trace("Event Key Up. Key Code: " + e.keyCode + " Char Code: " + e.charCode); } } }
When you try the previous example, you will see that the console prints the trace logs every time a key is pressed down and every time the key is let go. To help determine what key is pressed, you may examine the property of the event that is passed into the callback. There are two codes that get passed in—keyCode and charCode. [ 287 ]
Graphics Programming in AS3
With keyCode, you can find out which physical key was pressed on the keyboard, and charCode is the ASCII code, if there is one. For example: When key a is pressed, you get keyCode of 65 and charCode of 97. If the same a key pressed with Caps Lock on or with the Shift key down, you get a keyCode of 65 and charCode of 65. As is the case for key 4 in the top row and $, the physical key is the same. So in both cases, you will get a keyCode of 52, while the charCode is different and is the corresponding ASCII value of 52 and 36. For those keys pressed for which there is no ASCII equivalent, for example, function keys, arrow keys, etc, you get a charKey of 0 and keyCode as defined in the keyword class found in flash.ui package. The event also passes in things that are handy such as altKey, ctrlKey, and shiftKey, which are Boolean values that determine if the corresponding keys were held down or not when the event was generated. Note that you still get the keyDown and keyUp events for Shift, Ctrl, and Alt keys just like any other key press.
Arrow key handling: The basics
Often in game programming you handle the movements of a character, vehicle, or weapon using the arrow keys. The following is a listing to handle the arrow keys that allows your player to control the corresponding sprite. The example will create a small circle sprite and put it on the stage. It will set up the keyboard event handling as demonstrated in the previous example, only that the callback implementation will handle the arrow keys and discard everything else. We simply change the x and y of the sprite depending on which arrow key was pressed. We will also perform some boundary checks so that the sprite does not run off the stage. We will also define a constant property called SPEED. The value determines how fast the sprite should move for each key down. You might think about how you can make speed a variable and in a game you could change the speed, depending on whether the player has a certain item to go faster and among other factors. The following is a full listing, and note the compiler option: -default-size 500 500 -default-background-color 0xFFFFFF package { import flash.display.Sprite; import flash.events.KeyboardEvent; import flash.ui.Keyboard; [ 288 ]
Appendix B public class SpriteMove extends Sprite { // Width and Height of the stage must be equal // of less than what is defined in the compiler options private static const WIDTH:int = 500; private static const HEIGHT:int = 500; // Determines how much to move the sprite, for // every key down event private static const SPEED:int = 10; // The sprite that will be moved around private var m_sprite:Sprite = new Sprite(); public function SpriteMove() { m_sprite.graphics.beginFill(0x555555); m_sprite.graphics.drawCircle(0, 0, 25); m_sprite.graphics.endFill(); addChild(m_sprite); stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); } // The callback fired when the key is pressed down private function onKeyDown(e:KeyboardEvent):void { var key:uint = e.keyCode; switch (key) { case Keyboard.UP: m_sprite.y -= SPEED; // make sure it does not go negative if ( m_sprite.y < 0 ) m_sprite.y = 0; break; case Keyboard.DOWN: m_sprite.y += SPEED; // does not go out of stage if ( m_sprite.y > HEIGHT ) m_sprite.y = HEIGHT; break; case Keyboard.LEFT: m_sprite.x -= SPEED; // make sure it does not go negative if ( m_sprite.x < 0 ) m_sprite.x = 0; break; case Keyboard.RIGHT: // does not go out of stage m_sprite.x += SPEED; if ( m_sprite.x > WIDTH ) m_sprite.x = WIDTH;
[ 289 ]
Graphics Programming in AS3 break; } } } }
Arrow key handling: The professional way
If you compiled and tried the previous example, you will have noticed one thing that is annoying. The sprite does not start to move as soon as you hit the key or takes a second or so to change directions when you start to hold down a different key. Frustrating! You can that see most of the Flash games out there do not have this issue. Let's see how we can deal with this issue. Another issue that needs to be dealt with is multiple key downs. For example, when your player is controlling your spaceship, they may have more than one arrow key held down furiously at the same time. The previous is too simple of an implementation to handle the spaceship through a sea of asteroids, but does give you the basic of keyboard handling. So what's a better way to implement the same? The following describes one possible implementation that takes care of the shortcomings of the previous simple implementation. We will still retain the arrow key handling except that we do not move the sprite in the callbacks, but we maintain the state of keys, whether they are held down. So we need to maintain four Boolean values one for each arrow key. We also will use a timer, and in the callback for which we examine the Boolean key state values and move the sprite accordingly. The following is a visualization of the mechanics:
[ 290 ]
Appendix B
So we add the key state variables and timer as shown next: // Timer to move the sprite private var m_timer:Timer; // Booleans private var private var private var private var
to keep track of the key state (held down or not) m_keyUP:Boolean = false; m_keyDOWN:Boolean = false; m_keyLEFT:Boolean = false; m_keyRIGHT:Boolean = false;
In addition to the key down handling, we add the key up handling and a timer in the constructor: stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp); m_timer = new Timer(100); m_timer.addEventListener(TimerEvent.TIMER, onTimer); m_timer.start();
Here is the complete listing for the fast-moving sprite, with the same compiler options for the previous implementation: package { import import import import import
flash.display.Sprite; flash.events.KeyboardEvent; flash.events.TimerEvent; flash.ui.Keyboard; flash.utils.Timer;
public class FastSpriteMove extends Sprite { // Width and Height of the stage must be equal // of less than what is defined in the compiler options private static const WIDTH:int = 500; private static const HEIGHT:int = 500; // Determines how much to move the sprite, for // every key down event private static const SPEED:int = 10; // The sprite that will be moved around private var m_sprite:Sprite = new Sprite(); // Timer to move the sprite private var m_timer:Timer; // Booleans to keep track of the key state (held down or not) private var m_keyUP:Boolean = false; private var m_keyDOWN:Boolean = false; [ 291 ]
Graphics Programming in AS3 private var m_keyLEFT:Boolean = false; private var m_keyRIGHT:Boolean = false; public function FastSpriteMove() { m_sprite.graphics.beginFill(0x555555); m_sprite.graphics.drawCircle(0, 0, 25); m_sprite.graphics.endFill(); addChild(m_sprite); stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); stage.addEventListener(KeyboardEvent.KEY_UP, onKeyUp); m_timer = new Timer(100); m_timer.addEventListener(TimerEvent.TIMER, onTimer); m_timer.start(); } // The callback fired when the key is pressed down private function onKeyDown(e:KeyboardEvent):void { var key:uint = e.keyCode; switch (key) { case Keyboard.UP: m_keyUP = true; break; case Keyboard.DOWN: m_keyDOWN = true; break; case Keyboard.LEFT: m_keyLEFT = true; break; case Keyboard.RIGHT: m_keyRIGHT = true; break; } } // The callback fired when the key is let go private function onKeyUp(e:KeyboardEvent):void { var key:uint = e.keyCode; switch (key) { case Keyboard.UP: m_keyUP = false; break; case Keyboard.DOWN: m_keyDOWN = false;
[ 292 ]
Appendix B break; case Keyboard.LEFT: m_keyLEFT = false; break; case Keyboard.RIGHT: m_keyRIGHT = false; break; } } // The callback fired when timer fires, move the sprite private function onTimer(e:TimerEvent):void { if ( m_keyUP ) { m_sprite.y -= SPEED; // make sure it does not go negative if ( m_sprite.y < 0 ) m_sprite.y = 0; } if ( m_keyDOWN ) { m_sprite.y += SPEED; // does not go out of stage if ( m_sprite.y > HEIGHT ) m_sprite.y = HEIGHT; } if ( m_keyLEFT ) { m_sprite.x -= SPEED; // make sure it does not go negative if ( m_sprite.x < 0 ) m_sprite.x = 0; } if ( m_keyRIGHT ) { // does not go out of stage m_sprite.x += SPEED; if ( m_sprite.x > WIDTH ) m_sprite.x = WIDTH; } } } }
[ 293 ]
Graphics Programming in AS3
Labels, text fields, and sprite buttons
A must have feature for any game, especially right at the beginning if you are writing a multiplayer game! How else would you have players log in? Here is the listing of a sample login screen with compiler options: -default-size 800 600 -default-background-color 0x000000 package { import fl.controls.TextInput; import import import import
flash.display.Sprite; flash.events.MouseEvent; flash.text.TextField; flash.text.TextFormat;
import mx.core.BitmapAsset; public class LoginScreen extends Sprite { // Create access for the button images [Embed(source="ok.png")] private var OK:Class; [Embed(source="exit.png")] private var Exit:Class; // Text format for all labels and text input private var m_format:TextFormat = new TextFormat(); // Label and text input for the username private var m_userLabel:TextField = new TextField(); private var m_username:TextInput = new TextInput(); // Label and text input for the password private var m_passLabel:TextField = new TextField(); private var m_password:TextInput = new TextInput(); public function LoginScreen() { // Initialize the format m_format.font = "arial"; m_format.size = "10"; m_format.color = 0xaaaaaa; // Initialize and add username label m_userLabel.defaultTextFormat = m_format; m_userLabel.x = 172; m_userLabel.y = 100; m_userLabel.text = "Username:"; addChild(m_userLabel); [ 294 ]
Appendix B // Initialize and add username input m_username.setStyle("textFormat", m_format); m_username.x = 230; m_username.y = 100; m_username.width = 120; m_username.height = 20; addChild(m_username); // Initialize and add password label m_passLabel.defaultTextFormat = m_format; m_passLabel.x = 172; m_passLabel.y = 130; m_passLabel.text = "Password:"; addChild(m_passLabel); // Initialize and add password input m_password.setStyle("textFormat", m_format); // Neat trick to hide the typed characters m_password.displayAsPassword = true; m_password.x = 230; m_password.y = 130; m_password.width = 120; m_password.height = 20; addChild(m_password); // Add the buttons addOK(); addExit(); } private function addOK():void { var bma:BitmapAsset = new OK() as BitmapAsset; var s:Sprite; s = new Sprite(); s.graphics.beginBitmapFill(bma.bitmapData); s.graphics.drawRect(0, 0, bma.width, bma.height); s.graphics.endFill(); s.x = 230; s.y = 160; addChild(s); s.addEventListener(MouseEvent.CLICK, onOK); } private function onOK(event:MouseEvent):void { trace("Clicked on OK. Username: " + m_username.text + " Password: " + m_password.text); } private function addExit():void { var bma:BitmapAsset = new Exit() as BitmapAsset; var s:Sprite; s = new Sprite(); s.graphics.beginBitmapFill(bma.bitmapData); [ 295 ]
Graphics Programming in AS3 s.graphics.drawRect(0, 0, bma.width, bma.height); s.graphics.endFill(); s.x = 280; s.y = 160; addChild(s); s.addEventListener(MouseEvent.CLICK, onExit); } private function onExit(event:MouseEvent):void { trace("Clicked on Exit"); } } }
The resulting screen looks something like the following:
Filters: Adding effects to sprites
Time to learn about adding some cool effects to those sprites! Adding effects to sprites is as easy as setting what are called filters to the sprite. Filters can be added to any instance of DisplayObject class and to any of its subclasses. Remember, Sprite is one of the subclasses, which is what you usually play with, and makes it all perfect. DisplayObject has a property called filters, which is an array. It means that we can
add more than one filter to a sprite enabling us to combine more than one effect to a particular sprite to get the cool effect we want.
Flash provides a wide variety of filters under the package flash.filters. All filters are a subclass of the common base class known as BitmapFilter.
[ 296 ]
Appendix B
Here is a list of the ones available to us: Filter Class BevelFilter
Description
BlurFilter
The BlurFilter class lets you apply a blur visual effect.
ColorMatrixFilter
The ColorMatrixFilter class lets you apply a 4 x 5 matrix transformation on the RGBA color and alpha values of every pixel in the input image to produce a result with a new set of RGBA color and alpha values.
ConvolutionFilter
The ConvolutionFilter class applies a matrix convolution filter effect.
DisplacementFilter
The DisplacementMapFilter class uses the pixel values from the specified BitmapData object (called the displacement map image) to perform a displacement of an object.
DropShadowFilter
The DropShadowFilter class lets you add a drop shadow to display objects.
GlowFilter
The GlowFilter class lets you apply a glow effect.
GradientBevelFilter
The GradientBevelFilter class lets you apply a gradient bevel effect.
GradientGlowFilter
The GradientGlowFilter class lets you apply a gradient glow effect.
The BevelFilter class lets you add a bevel effect.
Modifying the filters of a display object or sprite is a three-step process: 1. Assign the sprite's filter to a temporary array. 2. Modify the temporary array the way you want, either remove an existing filter or add an additional filter. 3. Assign the temporary filter back to the sprite's filter property, assuming that m_filter was defined as follows: private var m_filter:DropShadowFilter = new DropShadowFilter();
The following is the code to add a filter to the sprite m_sprite: var filters:Array; filters = m_sprite.filters; filters.push(m_filter); m_sprite.filters = filters;
[ 297 ]
Graphics Programming in AS3
Next is a listing that demonstrates adding a drop shadow filter when the mouse is over the sprite and removing the filter when the mouse moves out of the sprite's bounds: package { import flash.display.Sprite; import flash.events.MouseEvent; import flash.filters.DropShadowFilter; public class FilterTest extends Sprite { private var m_sprite:Sprite; private var m_filter:DropShadowFilter = new DropShadowFilter(); public function FilterTest() { m_sprite = new Sprite(); m_sprite.graphics.beginFill(0xFFFF00); m_sprite.graphics.drawCircle(0, 0, 10); m_sprite.graphics.endFill(); m_sprite.addEventListener(MouseEvent.MOUSE_OUT, onMouseOut); m_sprite.addEventListener(MouseEvent.MOUSE_OVER, onMouseOver); m_sprite.x = m_sprite.y = 20; addChild(m_sprite); } private function onMouseOver(e:MouseEvent):void { var filters:Array; filters = m_sprite.filters; filters.push(m_filter); m_sprite.filters = filters; } private function onMouseOut(e:MouseEvent):void { var filters:Array; filters = m_sprite.filters; filters.splice(0, 1); m_sprite.filters = filters; } } }
[ 298 ]
Appendix B
Take your time and play with the rest of the filters and see how you can apply them. Choosing the right filters or a combination of them is what makes a game visually more appealing. Further, note that each filter comes with a bunch of little knobs (properties) that lets you create unique filters as your game demands.
Transparency: Playing with the alpha channel
With Flash and AS3, one can easily control the transparency of an image and UI elements like buttons and text fields. This is a great way to create a cool-looking interface for your game. In the graphics world, transparency is usually controlled by what is called the alpha channel. The alpha channel value in Flash is a number and its value may range from 0 to 1. 0. Being completely transparent, you would not see the sprite with an alpha of 0 and a value of 1 is opaque, which means that anything directly behind the sprite will be invisible. A value of 0.5 is 50 percent transparent or opaque. The following example shows how you can create one. It is quite simple. During the creation of a sprite in the method call beginFill, you simply specify the value for alpha (default value 1) something other than 1. package { import flash.display.Sprite; public class AlphaTest extends Sprite { public function AlphaTest() { var s:Sprite = new Sprite(); s.graphics.beginFill(0xFFFF00, 1); s.graphics.drawCircle(0, 0, 10); s.graphics.endFill(); s.x = s.y = 20; addChild(s); var s1:Sprite = new Sprite(); s1.graphics.beginFill(0xFF0000, 0.5); s1.graphics.drawCircle(0, 0, 10); s1.graphics.endFill(); s1.x = s1.y = 25; addChild(s1); } } }
[ 299 ]
Graphics Programming in AS3
The previous example creates two circles that are overlapping, the first one has an alpha value of 1, which means it is opaque, and the second sprite (s1) has an alpha value of 0.5, which is 50 percent transparent. s1 is placed on the stage so that it overlaps the first sprite. Here is what the stage looks like when you run the previous example:
Cool fading screens
Now that you know how to control the transparency in a sprite, how can we do that with an arbitrary image? Well, we will go a step further. Let us load an arbitrary image and have it fade out. In order to create transparency for an image, one way is to add a filter to the sprite representing the image. If we then add a timer and for each timer event callback we tweaked the alpha value by a delta starting from 1 (fully opaque) to 0(fully transparent), we can achieve the fading-out effect. A fade-in effect can be similarly achieved by updating the alpha value from 0 to 1. Fading images are not only useful for splash screens, but also finds its uses in game playing. For example, when the player kills an enemy such as a monster, you can either simply remove the dead enemy by removing the sprite (removeChild) or you can fade it out and then remove the sprite, increasing your general visual appeal for your game. The following is a generic class that takes in a bunch of sprites; you could also call it with just one sprite that you want to dissolve. When the sprite's transparency reaches zero, the sprites are automatically removed from the parent and that is the reason the constructor of the class takes in the parent as one of the parameter. Lastly, it also throws an event when the sprites have dissolved and removed from its parent. Let us examine the various parts of this class now. In order to generate an event from the class, we will have it inherit from EventsDispatcher, as shown next: public class SpriteDissolve extends EventDispatcher
We define the start and end values of alpha, which are essentially 1 and 0, respectively. We also allow for some flexibility of how fast the sprites should be dissolved—this is controlled by the steps' (m_maxSteps) private variable. [ 300 ]
Appendix B // We do the fading in 200 steps private static var m_maxSteps:int = 200; // starting alpha from 1.0 = opaque private static const s_alphaFrom:Number = 1.0; // ending alpha to 0 = 100% transparent private static const s_alphaTo:Number = 0.0; // determine what the change in alpha should be at each step. private static const s_alphaStep:Number = (s_alphaFrom - s_alphaTo)/m_maxSteps;
You also can find an external interface to modify the m_maxSteps. There are two ways to construct the SpriteDissolve object: a direct constructor, which takes in a parent sprite object, and an array of sprite objects all belonging to the parent. It is optional to construct the object with parent set to null; in this case, the sprites will not be removed from the parent. public function SpriteDissolve(parent:Sprite, sprites:Array)
Oftentimes, there is only one sprite to dissolve, in which case the caller may call the convenience factory method that creates and returns a SpriteDissolve instance. public static function createSpriteDissolve(parent:Sprite, sprite:Sprite):SpriteDissolve
The play method is where the timer is started and the callback method dissolve is called every 10 milliseconds. public function play():void { m_count = 0; m_alpha = s_alphaFrom; m_timer.start(); }
The dissolve method simply increases the counter, checks if it has reached the max number of steps; if so, the sprites are removed (only if the parent was specified) from the parent. If the max count has not been reached, we update the current alpha values to be applied; we create the filter with it. Finally, we apply it to each sprite. Also, note that we define an event class called SpriteDissolveEvent that is generated and thrown when the dissolve is complete. Other objects may listen to this event to take further actions. However, adding an event listener is optional. m_isDone = true; // Fire the Dissolve done event dispatchEvent(new SpriteDissolveEvent(SpriteDissolveEvent.DONE));
[ 301 ]
Graphics Programming in AS3
Here is the complete listing for the simple event class: package pulseui.util { import flash.events.Event; public class SpriteDissolveEvent extends Event { public static const DONE:String = "done"; public function SpriteDissolveEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false) { super(type, bubbles, cancelable); } } }
Here is the complete listing for the SpriteDissolve class: package pulseui.util { import flash.display.Sprite; import flash.events.Event; import flash.events.EventDispatcher; import flash.events.TimerEvent; import flash.filters.ColorMatrixFilter; import flash.utils.Timer; public class SpriteDissolve extends EventDispatcher { // We do the fading in 200 steps private static var m_maxSteps:int = 200; // starting alpha from 1.0 = opaque private static const s_alphaFrom:Number = 1.0; // ending alpha to 0 = 100% transparent private static const s_alphaTo:Number = 0.0; // determine what the change in alpha should be at each step. private static const s_alphaStep:Number = (s_alphaFrom - s_alphaTo)/m_maxSteps; // store the passed in paramters private var m_parent:Sprite; private var m_targets:Array; private var m_timer:Timer; [ 302 ]
Appendix B // per play vars // Keep track of the count the timer has fired private var m_count:int; // Current alpha value being applied to the sprites private var m_alpha:Number; // For external query private var m_isDone:Boolean; /** * * @param parent Parent of the sprites, could be null * @param sprites Sprites that need to be dissolved * */ public function SpriteDissolve(parent:Sprite, sprites:Array) { // Initialze m_parent = parent; m_targets = sprites; m_isDone = false; // set up the timer m_timer = new Timer(10); m_timer.addEventListener(TimerEvent.TIMER, dissolve); } /** * A convenience factory method to create sprite * dissolve instance, when there is only one sprite to * dissolve. * * @param parent Parent of the sprite, could be null * @param sprite Sprite to dissolve * */ public static function createSpriteDissolve(parent:Sprite, sprite:Sprite): SpriteDissolve { var a:Array = new Array(); a.push(sprite); return new SpriteDissolve(parent, a); } /** * External interface to control the dissolve speed * @param steps default is 200 * */ [ 303 ]
Graphics Programming in AS3 public function setSteps(steps:int):void { m_maxSteps = steps; } /** * External interface to start the dissolve * */ public function play():void { m_count = 0; m_alpha = s_alphaFrom; m_timer.start(); } /** * Timer callback method to update the alpha on sprites * @param event * */ private function dissolve(event:Event):void { m_count++; // If steps are completed, remove the sprite from the// parent if ( m_count == m_maxSteps ) { m_timer.stop(); for each ( var s:Sprite in m_targets ) { if ( s.parent != null && s.parent.contains(s) ) s.parent.removeChild(s); //remove the filter var filters:Array = s.filters; if ( filters.length > 0 ) { filters.pop(); s.filters = filters; } } m_isDone = true; // Fire the Dissolve done event dispatchEvent( new SpriteDissolveEvent( SpriteDissolveEvent.DONE)); } else { // update the alpha value m_alpha -= s_alphaStep; //trace(m_alpha); // call to update the filter on the sprites [ 304 ]
Appendix B updateDissolve(); } } /** * External interface to query if dissolve is done * @return * */ public function isDone():Boolean { return m_isDone; } /** * Private method to create the filter. * */ private function updateDissolve():void { var i:int; var matrix:Array = new Array(); matrix = matrix.concat([1, 0, 0, 0, 0]); // red matrix = matrix.concat([0, 1, 0, 0, 0]); // green matrix = matrix.concat([0, 0, 1, 0, 0]); // blue matrix = matrix.concat([0, 0, 0, m_alpha, 0]); // alpha var filter:ColorMatrixFilter; filter = new ColorMatrixFilter(matrix); for ( i=0; i<m_targets.length; i++ ) { var s:Sprite = m_targets[i]; applyFilter(s, filter); } } /** * Convenience method to apply the filter. * This method assumes that there are no other * filters applied to the sprites * during the dissolve process. * * @param s the sprite to apply the filter to * @param filter The filter itself * */ private function applyFilter(s:Sprite, filter:ColorMatrixFilter):void { var filters:Array = s.filters; if ( m_count > 1 ) { [ 305 ]
Graphics Programming in AS3 filters.pop(); } filters.push(filter); s.filters = filters; } } }
Cutting up assets
A typical game contains a plethora of art assets numbering in the hundreds for medium-sized games. One obvious way is to have an image file for each, but just managing such a huge number of files may become cumbersome. One trick is to put more than just one asset into a single .jpg or .png file. The assets in a single file are shown as follows:
Now, how do we make it useable in the game? Do we need to cut up the asset file and then use them? Exactly! How do we do that? There are essentially three steps: 1. Load the graphics file as a BitmapAssetData object. 2. For each asset you want to cut out, create a bitmap data by providing the specific x and y (top left) and width and height. 3. Create the sprite from the cut out bitmap data. [ 306 ]
Appendix B
The following is the listing of a class that loads a .png file like the one shown previously; it then cuts out the GO button from the file and puts it on the state. The result of running the program is as follows:
package { import import import import
flash.display.BitmapData; flash.display.Sprite; flash.geom.Point; flash.geom.Rectangle;
import mx.core.BitmapAsset; public class AssetSplicer extends Sprite { // Define the graphics file that contains // all the ui you need [Embed(source="ui.png")] private var UIClass:Class; // A placeholder that the file can be accessed // as a bitmap data private var m_ui:BitmapData; public function AssetSplicer() { // Read the graphics file var bma:BitmapAsset = new UIClass() as BitmapAsset; m_ui = bma.bitmapData; // Add a sprite to the stage made with // cut out part of the start game ("go") asset addChild(getStartBtn()); } /** * A handy method to create a sprite from m_ui BitmapData * * @param r The rectangle to specify the x, y, [ 307 ]
Graphics Programming in AS3 * width and height within the bitmap. * * @return A sprite with the image found in the ui bitmap. * */ protected function _createSprite(r:Rectangle):Sprite { var bmd:BitmapData; bmd = new BitmapData(r.width, r.height, true, 0); bmd.copyPixels(m_ui, r, new Point(0, 0)); var sprite:Sprite = new Sprite(); sprite.graphics.beginBitmapFill(bmd, null, false, true); sprite.graphics.drawRect(0, 0, r.width, r.height); sprite.graphics.endFill(); return sprite; } public function getStartBtn():Sprite { // It is required that we know the // exact top-left co-ordinates // of the asset we want to cut // as well as the width and height. return _createSprite(new Rectangle(150, 200, 100, 100)); } } }
[ 308 ]
Index Symbols _checkMatch method 165 _sendPieceMatch method 166, 167 tag 57 tag 57 tag 57 .jar file 9 .jpg file 306 .png file 307 GNetClientObjectFactory 38
A ActiveMonster about 252 creating 261 listing 262 addFrog method 179 addGameState API 110 addGameState method 114 allToDeck method 185 alphaBitmap parameter 158 alpha channel about 299 creating 299 fading-in effect 300 fading-out effect 300 alphaPoint parameter 158 altKey 288 AS2 versus AS3 237 AS3 versus AS2 237 asset file cutting up 306, 307
assignShip method 228 Astrorace about 205 class organization 206 coordinates, mapping 212 finishing 223, 224 game design 206 game folder 206 game loop 207 Item class 218 items, implementing 218 mini-map 215 quadrants, loading 212-214 racetrack module 212 radar class 216 shield, implementing 222, 223 spaceship class 207 ui folder 206 avatar manager, game server module about 21 lobby 21 room manager 21 avatar-related APIs PulseGame client APIs 66, 67
B BevelFilter class 297 BitmapAssetData object 306 BitmapFilter 296 BlurFilter class 297 Button Effect class about 116 listing 116, 117 logic screen class example 117 byte, data types 58
C Card class 183 CardManager class 185, 186 cash property 60 causeDamage method about 258 examining 258 charCode 287 charKey 288 chars, data types 58 chat feature chat API 73 chat display, customizing 74, 75 in Hello World 74 private chat 73 public chat 73 room chat 73 system chat 73 team chat 73 chat, tic-tac-toe game 146 checkGameStatus method 141 class about 244 BevelFilter class 297 BlurFilter class 297 Button Effect class Card class 183 CardManager class 185, 186 ColorMatrixFilter class 297 ConvolutionFilter class 297 DisplacementFilter class 297 DisplayObject class 268, 296 DropShadowFilter class 297 frog class 181 GameStateSprite class 111 GlowFilter class 297 GradientBevelFilter class 297 GradientGlowFilter class 297 HelloGame class 44 HelloGameStateClient class 109 HelloWorld class interface class 264, 265 jib.game.ShapeGen class 154 JigsawGame class JigsawGameScreen class JigsawSkinner class
methods 244 NewGame class 192 NewJigsawGameScreen class 173 Number class 246 private class 244 properties 244 RaceTrack class 207 CLICK event 281 ColorMatrixFilter class 297 ConvolutionFilter class 297 copyPixel method 158 createChat method 75 createGameRoom API 95 createNewGameRoom method 93 createNewPlayerDisplay method 66 ctrlKey 288 cut method 159
D dir property 166 DisplacementFilter class 297 DisplayObject class 268, 296 DisplayObjectContainer 268 double, data types 59 downloading Pulse SDK 9 drag-and-drop using 284-286 drawMe function 62 drawMe method 63 DropShadowFilter class 297
E encapsulation 244 enterLobby API 89 enterLobby method 92 enterprise deployment architecture balancer process 17 diagram 16 game server programming 17 session server 17 zero server-side programming 17 events 273 exp property 60
[ 310 ]
F filters about 296-298 modifying 297, 298 finishGame API 233 Flash mouse events 281 flash-based multiplayer requirements 8 Flash Builder 4 exploring 238-240 installing 9, 237 float, data types 59 friends friends API 68 friends display, customizing 69-72 in Hello World 68 making, steps 67 frog class 181
G GAMANTRA 12 game avatar design about 59 avatar, displaying in Hello World 60 modeling 60 PlayerDisplay class, customizing 62-66 GameClient 30 GameClientCallback 30 game client module about 24, 25 chat 25 friends 25 Game State Manager 24 message handler 24 object serialization 24 game objects creating 244, 245 HelloAvatar class, modifying 245 game play implementing 103 game play implementation game, playing 202, 203 game screen, properties 197 initial frog positions, determining 200 initial three cards, obtaining 201
player colors, assigning 197-199 game room audience 89 managing 85 modeling 84 player, kicking out 87 player states, not ready 87 player states, ready 86 properties 89 seating order 85 states, play 86 states, waiting 86 types, open 88 types, password protected 88 types, turn-based 88 GameRoomClient class 95 game screen about 96 customizing 102 implementing 97-101 game server functionality, exploring guest logins 54 login 52 multiple logins, dealing with 53, 54 registration 51 game server module avatar manager 21 chat 21 connection management 22 friends 21 message 23 message, chat message body 24 message dispatcher 22 message, login message body 24 object serialization 22 object synchronization 21 persistence 20 security 22 session 21 session manager 20 game state developer usable property 104 in Hello World 107 types 105 game state, in Hello World API 114 code, exploring 107-109 [ 311 ]
GameStateSprite class 108 general flow of events 108, 109 new game state, adding 110 removing 113 schema file 109 screenshot 107 updating 111, 112 GameStateSprite class 111 game states, types action game state 106 initial game states 105 normal game states 105 unique game states 106 gen method about 155 top level pseudo code 155 getAvatarPix method 67 getCard method 201 getFormat method 50, 77 getGameId method 44, 130, 170 getSplash method 41 GlowFilter class 297 GNET_JAVA 12 GNetMetaDataMgr 38 GradientBevelFilter class 297 GradientGlowFilter class 297
project, setting up 31 schema file, examining 36 specification 35, 36 Hello World sample, exploring HelloGame.as 43, 45 login screen 46 outline class 49 player, registering 50 screen class 46, 47 skinner class 47, 48 hide method 97 high-level architecture, multiplayer games client-server model 19 client-to-client interaction 19 enterprise deployment architecture 16 network programming paradigm 18 peer-to-peer model 19 simple deployment architecture 14, 15 UDP protocol 18 high scores about 76 GameRank object and Avatar relationship 76 in Hello World 77 HostID property 84 host name property 84
H
I
handleMsg method 27 HelloGame class 44 HelloGameStateClient class 109 HelloWorld.as file class, defining 242, 243 code, examining 243 contents 242 HelloWorld class class, defining 243 Hello World sample ActionScript Project 240 ActionScript project, setting up 32-35 bin-debug folder 241 code generator 37, 38 creating, steps 31-38 directory structure 38 exploring 43 html-template folder 241
IEventDispatcher interface 268 inheritance monster ActiveMonster 252 ActiveMonster, creating 261 ActiveMonster, listing 262 Avatar, using 258 class listing 253-257 implementing 252 modeling, in game implementation 252 PassiveMonster 252 PassiveMonster, creating 258-260 initGameScreen method 102 initItems method 219, 220 init method 43, 97, 135 initOutline method 43, 74 initPlayersDisplay method 66 installing Flash Builder 4 9, 237 [ 312 ]
Pulse SDK 10 int, data types 59 InteractiveObject 268 interface class 264, 265 isHost property 60 items, Astrorace collisions, detecting 221, 222 implementing 218-220
J jib.game.ShapeGen class 154 jigsaw game graphics 151 project, files 150 project, setting up 150 two player screens, viewing side-by-side 150 JigsawGame class about 170 constructor, overriding 170 initNetClient method, overriding 170 jigsaw game, graphics DisplayManager 152 matches, checking 163-165 piece, creating 153-161 pieces, dragging 161, 162 pieces managing, group used 152 PieceSprite class 152 JigsawGameScreen class about 174, 175 JigsawSkinner class about 172 JJF class 191 Jump Jump Frog game about 177, 178 implementing 178 Jump Jump Frog game implementation card, managing 183-189 frog class 181-183 frog movement 180 graphics 178, 179 JJF class 191 map class 179 NewGame class 192
screen, managing 190 Skin class 190, 191 step class 181
K keys, handling arrow key 288 arrow key, handling 291, 293 arrow key, multiple key downs 290 example 287
L labels 294, 295 Lag 104 lazy authentication 53 level property 60 load() method 78 lobbyHit method 138 lobby management about 81 game room, modeling 84 joining a room 82 lobby screen 83 rooms 83 lobby screen available rooms, displaying 91 customizing 90 game room display, customizing 90, 91 implementing, in Hello World 89 room-related API 92, 93 lobby screen, tic-tac-toe game 145 login, game server functionality about 52, 53 guest logins 54 multiple logins, dealing with 53, 54
M magic numbers 249 map class 179 Massively Multiplayer Online Role Playing. See MMO RPG max player count property 84 m_colors property 196 m_discard property 196 mergeAlpha property 158 [ 313 ]
method accessing 251 addFrog method 179 addGameState method 114 allToDeck method 185 assignShip method 228 causeDamage method checkGameStatus method 141 copyPixel method 158 constructor method 250 createChat method 75 createNewGameRoom method 93 createNewPlayerDisplay method 66 cut method 159 defining 250 drawMe method 63 enterLobby method 92 gen method getAvatarPix method 67 getCard method 201 getFormat method 50, 77 getGameId method 44, 130, 170 getSplash method 41 handleMsg method 27 hide method 97 initGameScreen method 102 initItems method 219, 220 init method 43, 97, 135 initOutline method 43, 74 initPlayersDisplay method 66 load() method 78 lobbyHit method 138 moveItems method 221 move method 162 newGameState method 100 onAddGS method 111 onAddItem method 229 private method 250 protected method 250 public method 250 m_frogs property 196 m_maskCount property 227 m_mask property 227 MMO RPG 252 m_myColor property 196 m_myHand array 185 m_myMask property 227
m_netClient property 131, 171 m_objects property 229 MOUSE_DOWN event 281 mouse events CLICK 281 handling, in Flash 281, 282 handling, in many objects 282, 283 handling, need for 279 MOUSE_DOWN 281 MOUSE_MOVE 281 MOUSE_OUT 281 MOUSE_OVER 281 MOUSE_UP 281 position, determining 284 registering for 280 MOUSE_MOVE event 281 MOUSE_OUT event 281 MOUSE_OVER event 281 MOUSE_UP event 281 moveItems method 221 move method 162 moveSprite function 275 m_position property 181 m_selected property 101 m_spot property 181 multiplayer games about 7 avatar, designing 55 chat feature 73 high-level architecture 14 Lag 104 lobby management 81 overall structure 25 room management 81 tic-tac-toe game, example 125 multiplayer games, overall structure main game loop 25, 26 message, processing from server 27 programming API 27 multiplayer implementation interpolation 232 items, adding on map 229 race winner, determining 233, 234 schema, designing 225 ship position updates 230, 231 multiplayer implementation design card, distributing 193, 194 [ 314 ]
client-server implementation 192 frog position, setting 194 games states, defining in schema file 194, 195 player color, assigning 194 Pulse-based games 192
N networking about 166 code, generating 169 game screen, preparing 169 game states, designing 166-168 JigsawGame class 170 Schema file 166 NewGame class 192 NewGameScreen createGameRoom API, calling 95 customizing 94 implementing, in Helllo World 93 newGameState method 100 NewJigsawGameScreen class 173 Number class 246
O object hierarchy, Flash DisplayObject 268 DisplayObjectContainer 268 IEventDispatcher interface 268 InteractiveObject 268 Object class 268 object hierarchy, Flash 267 onAddGS method 111 onAddItem method 229 onGameStateError 199 onHostAlert method 96 onLogin method 42 onNewGameState implementation 198 onNewGameState method 198 onPieceMatch method 167 onPlayerMoved method 140 onRemoveGS method 100 onSplashDone method 41 onTimer method 275 onUpdateGS method 112 Outline.png 47
P password property 84 pictures embedding 276-279 PieceCutter class 159 pieceId property 166 PieceSprite class 152, 164 playCard method 203 player count property 84 PlayerDisplay class customizing 62-66 pos property 60 posX property 226 posY property 226 processUserInput method 209 programming API, multiplayer games about 27 PulseCallback 28 PulseClient 27, 28 types 28 property about 248 accessing 251 cash property 60 class property, using 248, 249 defining 248 dir property 166 exp property 60 HostID property 84 host name property 84 isHost property 60 level property 60 max player count property 84 m_colors property 196 m_discard property 196 mergeAlpha property 158 m_frogs property 196 m_maskCount property 227 m_mask property 227 m_myColor property 196 m_myMask property 227 m_netClient property 131, 171 m_objects property 229 m_position property 181 m_selected property 101 m_spot property 181 [ 315 ]
password property 84 score property 60 Pulse API design GameClient 30 GameClientCallback 30 PulseGame class, PulseUI about 40 default implementation 42 GameClient 40 GameClientCallback interface 40 MyGame, subclass 40 onSplashDone 41 showLogin method 42 splash method 41 Pulse library, components diagram 30 Pulse.swc 29 PulseUI.swc 29 Pulse modeler about 55 arguments 56 classes 56 Pdata types 58 example schema 56, 58 Pulse SDK about 8 avatar-related APIs 66 downloading 9 features 8 installing 10 Pulse SDK installation bin folder 12 doc folder 12 lib folder 12 post-installation checks 12 samples, running 13 steps 10, 11 Pulse.swc 29 PulseUI classes 39 PulseGame class 40 screens, managing 39 Pulse UI framework about 126 project, setting up 127 PulseUI.swc 29
R RaceTrack class 207 radar class listing 216, 217 ready property 60 registration screen, tic-tac-toe game 148 Remote Procedure Call. See RPC removeGameState method 114 requestShip method 227 room name property 84 room status property 84 room type property 84 RPC 18 rsrc directory Frame.png file 78 Outline.png file 78 Ui.png file 78
S samples, Pulse SDK installation client, starting 13 running 13 server, starting up 13 schema, designing about 225 item class 226 ship color, assigning 226, 228 ShipMask class 226 ShipPos class 226 ShipWin class 226 Schildkroten-Rennen. See Jump Jump Frog game score property 60 sendGameStateAction method 115 sendGameState method 132 server modules about 19 game server module 20 setSelected method 113 ShakeEffect class 121-123 shieldReq method 222 shiftKey 288 ShipMask class 226 short, data types 58 shouldShowRoom method 91 [ 316 ]
showEndGame method 144 showIP method 45 showLogin method 42 show method 97 showRanks method 77 Skin class 190, 191 Slider class about 117-119 listing 120 SliderEvent class 117 spaceship class about 207 movement, controlling 207, 208 hip, skinning 210, 211 splash method 41 sprite about 269 arranging, front to back 270 concepts 271 effects, adding to 296 filters 296 Flash application, stage 269 hierarchy 270 moving 271 project, creating on Flash Builder 4 271-273 removing 270 transparency 299 sprite buttons 294-296 SpriteDissolve class 115, 302-305 SpriteDissolveEvent 301 SpriteTest class 272 startDrag() method 285 startGame method 96, 137, 174 start method 42 static keyword 265 static method example 266 static property accessing 266 step class 181 stopDrag() method 284
T text fields 294-296 tic-tac-toe game chat 146 constructor, overriding 130 game screens, preparing 130 initNetClient method, overriding 131 lobby screen 145 modeling 128 player actions, receiving 132 player actions, sending 132 registration screen 148 running, from sample directory 126 TictactoeGameScreen 134 TictactoeNewGameScreen 133, 134 TictactoeSkinner 133 top-ten 147 turn-based game, implementing 132 tic-tac-toe game, modeling about 128 project, directory structure 129 steps 128 TictactoeGameScreen 134 game screen, initializing 135, 136, 138 player's move, allowing 139 player's turn, displaying 138 player's win, determining 140, 142, 143 winner, searching 143, 144 TictactoeGameStatus 134 TictactoeHotspot 134 TictactoeNewGameScreen 133, 134 TictactoeSkinner 133 timers 274, 275 timestamp, data types 59 top-ten, tic-tac-toe game 147 trace method 275
U UI.png 48 unique game state feature 192 updateGameState API 111 updateGameState method 114 updateMakeFriend method 63
[ 317 ]
updateSpeed method 210 user interface skinning 78 skinning, for Hello World 78
V variable defining 246
[ 318 ]
Thank you for buying
Flash 10 Multiplayer Game Essentials
About Packt Publishing
Packt, pronounced 'packed', published its first book "Mastering phpMyAdmin for Effective MySQL Management" in April 2004 and subsequently continued to specialize in publishing highly focused books on specific technologies and solutions. Our books and publications share the experiences of your fellow IT professionals in adapting and customizing today's systems, applications, and frameworks. Our solution based books give you the knowledge and power to customize the software and technologies you're using to get the job done. Packt books are more specific and less general than the IT books you have seen in the past. Our unique business model allows us to bring you more focused information, giving you more of what you need to know, and less of what you don't. Packt is a modern, yet unique publishing company, which focuses on producing quality, cutting-edge books for communities of developers, administrators, and newbies alike. For more information, please visit our website: www.packtpub.com.
Writing for Packt
We welcome all inquiries from people who are interested in authoring. Book proposals should be sent to [email protected]. If your book idea is still at an early stage and you would like to discuss it first before writing a formal book proposal, contact us; one of our commissioning editors will get in touch with you. We're not just looking for published authors; if you have strong technical skills but no writing experience, our experienced editors can help you develop a writing career, or simply get some additional reward for your expertise.
Unity Game Development Essentials ISBN: 978-1-847198-18-1
Paperback: 316 pages
Build fully functional, professional 3D games with realistic environments, sound, dynamic effects, and more! 1.
Kick start game development, and build readyto-play 3D games with ease
2.
Understand key concepts in game design including scripting, physics, instantiation, particle effects, and more
3.
Test & optimize your game to perfection with essential tips-and-tricks
3D Game Development with Microsoft Silverlight 3: Beginner's Guide ISBN: 978-1-847198-92-1
Paperback: 452 pages
A practical guide to creating real-time responsive online 3D games in Silverlight 3 using C#, XBAP WPF, XAML, Balder, and Farseer Physics Engine 1.
Develop online interactive 3D games and scenes in Microsoft Silverlight 3 and XBAP WPF
2.
Integrate Balder 3D engine 1.0, Farseer Physics Engine 2.1, and advanced object-oriented techniques to simplify the game development process
Please check www.PacktPub.com for information on our titles
Blender 3D 2.49 Incredible Machines ISBN: 978-1-847197-46-7
Paperback: 316 pages
Modeling, rendering, and animating realistic machines with Blender 3D 1.
Walk through the complete process of building amazing machines
2.
Model and create mechanical models and vehicles with detailed designs
3.
Add advanced global illumination options to the renders created in Blender 3D using YafaRay and LuxRender
Papervision3D Essentials ISBN: 978-1-847195-72-2
Paperback: 428 pages
Create interactive Papervision 3D applications with stunning effects and powerful animations 1.
Build stunning, interactive Papervision3D applications from scratch
2.
Export and import 3D models from Autodesk 3ds Max, SketchUp and Blender to Papervision3D
3.
In-depth coverage of important 3D concepts with demo applications, screenshots and example code.
4.
Step-by-step guide for beginners and professionals with tips and tricks based on the authors’ practical experience
Please check www.PacktPub.com for information on our titles