Sei sulla pagina 1di 39

Designing Isometric Adventure Games

Version 1.0 August 15, 2006

S60

p l a t f o r m

Designing Isometric Adventure Games | 2

Legal notice
Copyright 2006 Nokia Corporation. All rights reserved. Nokia and Forum Nokia are registered trademarks of Nokia Corporation. Java and all Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. Other product and company names mentioned herein may be trademarks or trade names of their respective owners. Disclaimer The information in this document is provided as is, with no warranties whatsoever, including any warranty of merchantability, fitness for any particular purpose, or any warranty otherwise arising out of any proposal, specification, or sample. Furthermore, information provided in this document is preliminary, and may be changed substantially prior to final release. This document is provided for informational purposes only. Nokia Corporation disclaims all liability, including liability for infringement of any proprietary rights, relating to implementation of information presented in this document. Nokia Corporation does not warrant or represent that such use will not infringe such rights. Nokia Corporation retains the right to make changes to this specification at any time, without notice. License A license is hereby granted to download and print a copy of this specification for personal use only. No other license to any other intellectual property rights is granted herein.

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 3

Contents
1.
1.1

Overview .............................................................................................................. 6
Target audience and platform-related considerations ....................................................6

2.
2.1

Content creation.................................................................................................. 8
Creating an isometric game level....................................................................................8 2.1.1 2.1.2 2.1.3 2.1.4 2.2 2.2.1 2.2.2 2.3 2.4 Designing................................................................................................................9 Creating the 3D environment ...............................................................................10 Cutting out and post-processing for Mappy..........................................................11 Creating a map in Mappy .....................................................................................13 Designing and sketching ......................................................................................13 Modeling and texturing .........................................................................................14

Model.............................................................................................................................13

Animation creation ........................................................................................................14 Rendering and preparing for game ...............................................................................14

3.
3.1

Programming ..................................................................................................... 16
Structure........................................................................................................................16 3.1.1 3.1.2 3.1.3 3.2 3.2.1 3.2.2 3.3 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.4 3.4.1 3.4.2 3.4.3 3.4.4 3.4.5 3.4.6 3.5 3.5.1 3.5.2 Application framework ..........................................................................................16 Game architecture ................................................................................................17 Reusable components..........................................................................................19 Example: Player ...................................................................................................20 Example: Enemy ..................................................................................................20 Introduction to Lua................................................................................................22 Lua and C++ integration .......................................................................................24 Data scripting........................................................................................................27 Object scripting.....................................................................................................28 Off-line scripting....................................................................................................28 How triggers work.................................................................................................29 Integrating triggers into game...............................................................................30 Triggers and inventory..........................................................................................31 Level scripting example ........................................................................................32 Triggers and story telling ......................................................................................32 Trigger script error handling .................................................................................33 Estimating memory usage ....................................................................................33 Designing for effective memory usage .................................................................33
Version 1.0 | August 15, 2006

Central game objects ....................................................................................................20

Scripting ........................................................................................................................21

Scripting with triggers....................................................................................................29

Memory management ...................................................................................................33

Designing Isometric Adventure Games | 4

3.6

Animation system and tools ..........................................................................................34 3.6.1 3.6.2 Animation generation from bitmaps......................................................................34 Example: Surface .................................................................................................34 Rendering example ..............................................................................................35

3.7 3.8 3.9

Isometric rendering .......................................................................................................35 3.7.1 Different screen resolutions and modes .......................................................................35 Debugging .....................................................................................................................36

4. 5. 6.

Summary ............................................................................................................ 37 References ......................................................................................................... 38 Evaluate this resource ...................................................................................... 39

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 5

Change history
August 15, 2006 Version 1.0 Initial document release

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 6

1.

Overview

This document discusses game development, specifically the designing of an isometric adventure game, from mobile perspective. The document first gives an overview of the design process, then provides more detailed information about content creation (graphics, mostly), and finally deals with programming issues. The discussion is on a general level, but experiences and best practices from creating an isometric example game (S60 Platform: Isometric Adventure Game Example, available at www.forum.nokia.com [2]) are provided. Isometric projection is a method for visual representation of 3D objects in two dimensions so that the angles between the projection of the three axes (x,y,z) are identical (120). All lines on each axis are parallel to each other, and objects do not appear to be larger when they are "closer" to the viewer, as they would in perspective projection. This is advantageous when portraying large 3D game levels on systems with limited 3D graphics capabilities, such as mobile devices. Objects can be easily represented as 2D sprites and game environments can be constructed using tile-based graphics. Because everything appears to have a constant size, there is no need to do scaling or calculate the effects of perspective. Isometric projection has been popular since the old 8-bit and 16-bit game systems. More recent examples of computer games using isometric projection include the city-building simulation game SimCity (2000 and 3000), and a multitude of role-playing games (and their sequels) such as Baldur's Gate, Fallout, and Diablo. [1]

1.1

Target audience and platform-related considerations

The key audience for this document is considered to be C++ game developers who have developed games for desktop computers and are willing to expand also to mobile platforms/devices, such as S60. Also those who have previously developed games for Palm or Windows Mobile, and are considering migrating to the S60 platform, can find the presented techniques useful. However, the isometric content creation part is considered applicable to all game developers interested in developing for that genre, regardless of the platform or the programming language in question. Similarly, the generic game architecture design considerations for mobile platforms are considered applicable to all game developers, or those interested in becoming (mobile) game developers. The document describes the usage of standard C/C++ libraries, which has certain advantages if the same game is to be ported from desktop to mobile devices. Note that the usage of Symbian C++ APIs provides potentially many other advantages, such as better resource optimization and access to key mobile technologies and low-level device features (such as camera, vibra, backlight, telephony) that are not achieved in ANSI C/C++. Thus, for experienced mobile developers and for those who wish to provide unique mobile game experience, Symbian C++ programming is often a better option. This document does not, however, discuss Symbian C++ game programming in detail; refer to the S60 Platform: Programming Games In C++ document [3] for specific usage of Symbian C++ APIs in game development. In addition, the approach in this document is rather to describe how to utilize development tools and techniques in desktop environment for mobile game development; for further pointers to the specific S60 and Symbian C++ resources and development tools, the above-mentioned S60 Platform: Programming Games In C++ document should be studied in addition to this document. Although the document focuses on designing C++ games, much of the content of this document can be applied to Java ME projects as well: The content creation process, (whole Chapter 2) could be the same in a Java ME game. In fact, many commercial Java
Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 7

ME games in the market have been created using the same level editor (Mappy) as used in this project. Most of the content in Chapter 3, "Programming, could be applied in Java as well. The main part which is not directly applicable is related to scripting, mainly Section 3.3 and scripting integration in Section 3.4, and more low-level development tips, that is, Section 3.5, Memory management, and Section 3.9, Debugging. However, note that even though Section 3.4, Scripting with triggers, is quite Lua/C++ oriented, it still contains useful conceptual information about the usage of triggers and might, therefore, be good to read if you are interested in using a trigger-based system in your Java/MIDP application. On the other hand, if you are interested in developing a game of some other genre than an isometric adventure game, it is best to skip the whole content creation process in Chapter 2. Most of the text in Chapter 3, "Programming, is directly usable in other genres as well, even though an isometric game is used as an example. Tools and libraries used in this document, Lua scripting language, and the Mappy tilemap editor are useful outside mobile content creation as well. Nowadays 2D games are almost obsolete in PC and console world since 3D is dominating game development, but scripting is becoming a more and more integral part of the game development as the games become more content intensive. Making content creation more independent of the game programming enables a more efficient workflow for level designers, game artists, and programmers alike. Lua is also a very popular choice as a scripting language in PC/console games due to good portability and low memory footprint. In addition, many of the programming techniques used in this document can be leveraged in the console world as well even if their usage is not needed in PC games. In consoles, memory is a limited resource, so memory buffer and object reuse are also important techniques. Memory fragmentation is also an issue in console games, and object/buffer reuse helps to avoid fragmentation as well.

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 8

2.

Content creation

Isometric game graphic content was created by leveraging 3D-rendering, mainly to avoid manual pixel-level drawing of the 2D art. This makes using lighting and shading easier to create a nice mood for the game levels. In practice this approach works so that you render images in 3D content creation software (for example, Blender), and then use them as construction blocks in a 2D level editor, in this case Mappy, which is used in Section 2.1.4. Character and other item animations are created in a similar way, by creating the animation first in a 3D content creation program and then rendering an image sequence from it. The animation creation process is described in more detail in Section 2.3, "Animation creation. The following sections take a closer look to how content, mainly graphics, is created for an isometric adventure game.

2.1

Creating an isometric game level

When creating a game level for an isometric 2D game, the first thing to think of is the perspective. Isometric projection is a form of graphical projection, a method of representing three-dimensional objects in two dimensions. See Figure 1 and Figure 2 for an example of an isometric world and tiles that can be used to create it.

Figure 1: Example of isometric representation

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 9

Figure 2: Tiles for creating an isometric world As you can see in Figure 1, all of the perspective lines (red) are facing only two different directions and the vertical lines (green) are always facing towards the sky. This means that there are no vanishing points for the lines so the environment can continue forever. It gives the opportunity to make the environment look more like 3D even though it is simple 2D graphics. An isometric world's limitation is that because the lines representing each dimension are parallel on the page, objects do not become larger or smaller as they extend closer to the viewer so there is not really any visible depth in the environment. These things need to be remembered so you can avoid unnecessary mistakes while creating an isometric game level. There are a lot of different phases to consider while working on a map for an isometric game. In the example game, 3D environments were used to create the map blocks so the blocks could simply be cut out and used in Mappy. The phases were the following: 1. Designing 2. Creating the 3D environment 3. Cutting out and post-processing for Mappy 4. Creating a map in Mappy 2.1.1 Designing

First of all, there needs to be a map design, preferably for the whole environment. For example, if you need to make three different environments as maps, it is best first to lay out a few rough sketches of the environments from the top view in a 2D program (for example, Photoshop). In Figure 3, you can see an example of a dungeon design. The design is a simple image that gives some clue of how the rooms will lay out, which makes the map creation itself much easier, because you already know where to put everything (for example, doors, windows, torches, and so on).

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 10

Figure 3: Design sketch 2.1.2 Creating the 3D environment

After the design has been made, it is time to start the fun part. There are a few rules that you should follow before you render the final content. The most important part is the perspective. You should angle and set up the camera to act as an isometric camera, which in simple terms means that the angle on the horizontal and vertical alignment should be exactly 45. Then you automatically get the perspective to be ready for rendering tiles out and to be set up in the map. One rule is that all the blocks need to be of the same size. This is mostly because Mappy can handle only single-sized blocks when creating a map. When you create the 3D environment, you should pay much more attention to texturing than to modeling. That is mostly because the forms of your models and textures have to be tileable in the 2D environment; otherwise it will look like the environment is actually collected of pieces (which simply looks bad). In Figure 4, you can see how the castle interior would look in 3D with and without textures.

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 11

Figure 4: Interior example As you can see, the model is very simple. When you texture and map your models, keep in mind the isometric law when you make tileable blocks for the ground, walls, and so on. The objects need to be rendered and then cut out separately. You cannot, for example, render only the window without the wall, but you need to render them together. This is mostly because Mappy does not layer two bitmaps on the same block so that one would still lie in the background. When you create details (for example, windows, doors, statues, or trees) you can and you should use as many polygons as possible to make it look good. In 2D art the polygon count does not matter. You can also use whatever shaders and illumination you like. The point is to make it to look good and map-like. 2.1.3 Cutting out and post-processing for Mappy

This part is tricky and requires patience. Mappy can handle only single-sized blocks. In the example game the size was 128px * 90px, so it was required to cut all of the necessary parts to this size, and then collect them into one bitmap in Photoshop so Mappy knows how to read them. See Figure 5 for an example of a block bitmap of the castle (the final version had at least 50 per cent more blocks in it). As you can see, a lot of blocks need to be created to build a good-looking environment, and all of them need to be of the same size. See Figure 6 for an example of how the bitmaps are cut out of a 3D-rendered image.

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 12

Figure 5: Block set example

Figure 6: Block set creation After the cutting is done, the post-processing begins. As you can see from some of the rendered pictures, they do not match with the final blocks. This is because after the cutting different colors, details, fake global illumination, and so on were added. Mappy does not support mirroring yet, so you need to create separate mirrored blocks for your bitmap if you want something to be mirrored. Some tips to make it look more interesting: 1. Make at least five variations of the ground blocks that are at least somehow tileable. If you use only one, it will look very dull and meaningless.

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 13

2. Make a couple of different kinds of details (in 3D or in 2D). For example statues, bushes, trees, carpets, windows, or roads. You can place them here and there and check where they look good. 3. Fake the lighting. Usually 3D renderers have options to create global illumination. 2.1.4 Creating a map in Mappy

Mappy is a 2D-level 'tile-map' editor. Tile-map is a system for storing a rectangular area of graphics using less memory than a whole picture would take if stored as is. A tile-map editor allows you to reuse parts of the bitmap, that is, blocks, without extra storage cost. This method of creating maps is especially good for mobile devices, since memory is a very limited resource. Mappy uses a fairly flexible file format called FMP. All the versions of Mappy and playback libraries use the same FMP files and are backwards compatible. The first thing to do when you start creating a map in Mappy is to set up the scene. With the block size 128px * 90px, you need to set up the staggering to half (64px * 45px) so you can assemble the blocks correctly. This, however, in some cases leads to a tiny bug in Mappy. It causes your cursor automatically to snap to every other block there is in the invisible grid and it makes your workflow more difficult and much more time consuming. You also need to set the transparency color, import the block bitmap you created, and so on in order to start compiling the map. Now that everything is set up, you need to take out the designs you created earlier. Every map in the design has an approximate value of width and height. The first map usually determines the scale of the other maps too. When creating the first map, keep in mind the speed of the character movement so you do not get too short or too long, too boring, or too confusing maps. Then just lay out the ground of the map and start adding the required details. Remember that every map (that is, different rooms, different scenarios) needs to be in its own file, not in the same FMP file with others. Collisions need to be set up by hand for every block separately (though, of course, not when there is no collision). You can set the collision to any of the four sides of a block, or multiple sides as well.

2.2

Model

In general, it is easier to make more smooth-looking animations with 3D than 2D. That is because when you create a character in 3D, you can have all directions and frames you need just by rendering them out. However, in 2D you need to draw every frame by hand. In the example game, there are altogether approximately 675 frames. You would not want to draw them by hand, would you? This is one reason for using 3D content to make a 2D game. Creating a model to be animated is the first step. 2.2.1 Designing and sketching

Before starting to model a character, you should think what it is used for. Is it the player character, an ally, an enemy, or something else? Sketching the character also helps greatly when designing a 3D character. While sketching, you can decide whether it is a female or male, the coloring, elements, necessary details, and so on.

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 14

2.2.2

Modeling and texturing

When modeling and texturing the character, you should think about the target resolution of the game. If the target resolution is as small as 176*208, you should not pay attention to the smallest details of the character model. However, you should still create some smaller elements so you can recognize the character easily and there is something different to other characters. And of course, those two pixels of detail can still be shown from time to time. Basically only the coloring, anatomy, and shading are important when creating a 3D character for a small resolution. At this stage of creating the character, you should also think about the topology of the mesh for the animation. It is commonly known that the better the topology, the easier is animating.

2.3

Animation creation

In the example game, there are four different characters and every animation of them needs to be rendered in five different directions (north, north-east, east, south-east, and south). You can use mirroring for n-e, e, and s-e frames to save some space in the final build, but in general it is not recommended to mirror them because there are a few things that will look a bit weird if not hidden. For example, if your character has a gun in his hand, he will change hand when walking to different directions. Also the lighting can be made to look more dramatic and realistic when mirroring is not used, because then you can have an actual light source. In the example game there is no real light source; the light just comes from somewhere, so the mirrored frames in animations worked. After the modeling and texturing parts are done, the animation process begins. When animating, there are no limits. In this case, you do not need to think about any isometric or other projection-related things anymore. Just keep in mind that longer animations require more frames to be rendered, which means a larger file size. The approximate and optimal frame counts in the example are about five frames. Figure 7 presents an example of the player walking (four frames per direction).

Figure 7: Animation frames

2.4

Rendering and preparing for game

Now that models are animated, it is time to render them out to be integrated into the game. The perspective settings are similar to the ones used when rendering the maps. The only difference here is that you need to render the model separately for all directions, the number of them depending on how many directions the character can move. Also, you must decide the height of the character when rendering out the first frames. The character has to maintain its size all the way through the animation, regardless of how many frames there are. The actual frame size can vary between animations as long as the size remains the same for all the frames of a single animation. The following is an example: 1 character 1 animation

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 15

4 directions (north, east, south, west) 4 frames per animation. You render north animation and your frame size is 50px * 80px for all of the four frames of that animation. Then you render south animation, but your frame size is 50px * 85px for all of the four frames of that animation, and so on. Generally the background of the frames should be of the same color. In the animation system used in the example game, the first pixel, that is, the pixel in the upper-left corner, determines the transparent color of the frame; semi-transparency was not supported. You should pick a color that is not used in the character. Otherwise parts of the character get transparent.

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 16

3.

Programming

Mobile game programming is very similar to any programming. There are only a few restrictions and peculiarities because the game that is created needs to be sufficiently fast and user-friendly, and because the platform is more restricted than, say, a generic PC. This chapter examines game programming from the mobile platform perspective, concentrating on the creation of an isometric adventure game, and discusses some of the more important issues specific to mobile platforms.

3.1

Structure

As soon as a preliminary game design has been created, the actual program architecture can be designed. The common cycle of coding, testing, and fixing may be a good way to practice, but in an actual project it is better to leave the keyboard alone for a while and proceed with the design. Common software engineering wisdom applies. The special requirement of a mobile platform is that you want to minimize the number of memory allocations and preferably not allocate memory at all while the game is running. Objects should not be created and destroyed during a typical game state update. In mobile devices, efficiency is also an issue. The impact of implementation (efficiency, reliability) is greater the lower the abstraction level is, meaning that if you optimize a routine that is run 1000 times in the main loop, it matters much more than some other routine which is run twice in high-level game logic. Refactoring, reorganizing code architecture, is always a good practice. It helps to make things clearer and simpler, and may enable performance improvements as well. There may also be a time and place for an optimization that reduces code simplicity and readability, a desperate situation, but the decision to go for it should never be made lightly or at an early phase of the project anyway. Mostly, you should stick to the kind of optimization that makes things clearer and simpler. Simple is good. 3.1.1 Application framework

If you plan to target multiple mobile platforms with your game development, it is advisable to create a multi-platform framework for mobile devices from the start. You have a generic framework, and then you have platform-specific components, which handle all the tasks that need to be done differently on each platform. This way it is easier to branch your project to cover possible new platforms and systems. It also potentially enables development on other, non-mobile platforms as well. Naturally the game has to be tested, extensively, on the target platform, and there can be problems that manifest themselves on the target platform only. So eventually it is probable that you need to debug code on a phone, which has traditionally been difficult and to which there have not been too many solutions available until Carbide.c++ Professional. One advantage of desktop development worth mentioning is that artists and designers can use a desktop application for testing, which might turn out easier and more efficient for this purpose than using device emulators. Also, you can have more debugging information on a desktop version, debugging is easier in general, and you can emulate all different target platforms, their screen sizes and so on, and switch between these easily. Generic testing is also faster, due to superiorly faster loading and execution, not to mention the possibility of loading and reloading your scripts at run time (if you create the support for it).
Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 17

On the downside, the desktop development approach provides a potential risk that an application tested to run on the desktop PC might eventually not work as expected on a mobile device with limited resources. Supporting the S60 scalable UI with different screen sizes and orientations might also turn out more tedious than with specific S60 C++ APIs. Thus, if only Symbian OS devices are targeted, IDEs and SDKs for platforms based on Symbian OS, such as the S60 platform, might be a better choice. So pros and cons have to be carefully studied before choosing the development approach. Abstract the underlying platform, so you can create a separate version for each platform. Also, prepare for the possibility to actually create a different build for each of your supported platforms, because mobile platforms indeed do differ from each other. 3.1.2 Game architecture

What you lay upon the framework is the game itself. Any game, as any piece of software in general, has an architecture, whether developers know it or not, or intend it or not. It is always better to know your architecture beforehand, and it is always better to control the architecture, not let the architecture control you. The definition of software architecture varies. The following definition is used here: the concept and blueprint of all things related to how the application works, information of its components, and the interaction of these components. The better the project is prepared, the better are its chances of success, and designing the high-level architecture beforehand is a big part of that preparing. At least, it is advisable to have a diagram of the main components and their relationships. And remember that more than ever, in mobile applications, simple is good. A good approach for getting started with architecture design is to make an informal textual description of the game (or a proper design document) and then explicitly cut marketing talk and unnecessary features out of it before continuing with the design. Furthermore, keep it separate from the actual design document. Design documents are often written by people with no programming knowledge whatsoever, which may result in unnecessarily complex features. After you have a very condensed description of the game, you can start gathering verbs and nouns from it. This is an old and simple approach, but it works quite well in practice. A noun in this case is a potential class or object and a verb is a potential functionality or action for some object. In practice, it is often easier to find objects first, and only then think about responsibilities and how they should be assigned to the objects. There are a number of relationships to consider, for example: Does the object consist of other objects? Are the lifetimes of two objects dependent on each other? Does the object use some other objects? Which other objects use this object? Is this object type a special case of some other, more general type?

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 18

Figure 8: Example of a high-level game architecture In the example game the architecture is built around the GameApp class, which owns Map and Player. Player and each Enemy is a special case of MapObject. Map contains each Enemy, each Trigger, and has a reference to Player. In addition, specifically the Windows build uses MapConverter, which derives from Map. See the diagram in Figure 8. The framework calls GameApp through initialization, updating, and control (in this case, specifically, key control) functions. The update function is called at a steady rate. The rate could also be irregular, which is why delta time (dt) is better to pass as an argument to the function, just so that the game can take it into account. The update function moves the game forward by one cycle. The main things that the update does are: Translate key controls as commands to Player Handle player movement, collision, state updating, and actions (like hitting an enemy) Handle enemy AI, movement, collision, state updating, and actions Handle triggers Handle screens and level changes Call sound playing Render visuals, including health bar and texts, into a buffer, from which the framework eventually flips the visual data onto the screen. So, GameApp is what keeps things running, Map is the milieu of action, and MapObjects are those who do the action. In addition, Triggers are how Player moves through the milieu, interacts with it, and alters it.

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 19

3.1.3

Reusable components

Once you create your basic framework it is almost guaranteed that you can use it in every similar game. The architecture and the game code itself, on the other hand, are unique. Thirdly, you have your generic components, which, hopefully, are reusable and designed for reuse. The general wisdom for mobile development is to keep these components as simple and efficient as possible. This is where optimization efforts mostly pay off, which is also treacherous, because these components have to be extremely reliable, too. Loaders fall into this category, since they certainly are reusable. A compression system is good to have if the game is bigger than just a demo. With a good compression system 64MB of game data can be easily compressed to half of the original size. Moreover, generic rendering utilities are usual. For example, in the example game there is a class called Surface, which basically contains images and animations and the code for handling them. One of the more important things to have is a math library the more robust the better. Remember that all low-level reusable functions require extensive testing, fine tuning, and optimization because they are the building blocks of all greater things. A bug in such a function can be extremely fatal and difficult to spot. Likewise, slowness in such a function can affect the game with tenfold magnitude compared to some other function. This applies to math functions especially well. One thing, related to math, that deserves special mentioning is the fact that unless your target platform has an FPU, it is better to stick to fixed-point math, which is infinitely faster than using emulated floating point calculations. Problems with accuracy and overflowing may arise, but usually these problems are manageable. The fixed point class in the example game uses a 24-bit integer part and an 8-bit decimal part, which suits the game's needs well. If more accuracy is needed, a 16-bit integer part and a 16-bit decimal part would be called for, or maybe even 32-bit integer and 32-bit decimal. Some basic math optimization tips: Use fixed point calculations. Use simple operations such as addition, subtraction, and logical operators as much as possible to replace more complex calculations. Avoid loops as much as you can. Avoid division if the processor of the target platform does not have the operation in its instruction set (for example, ARM). If needed, implement fast inverse square root ( iSqrt(x) = 1 / sqrt(x) ) instead of square root. You can use it directly for normalization, and: sqrt(x) = x * iSqrt(x). Series approximation (for example, Taylor series approximation) is a good way of implementing more complex functions such as sine, cosine, and arctan2 with enough accuracy. For example, sin(x) = x - ( x^3 / 3! ) + ( x^5 / 5! ) - ( x^7 / 7! ) + ( x^9 / 9! ) ... Implement what you need, not more. If you do not need much accuracy for, say, sine function, as is the case with the example game, it is perfectly ok to use nested if-elses, a simple table or a minimized Taylor series approximation, such as sin(x) = x ( x^3 / 3 ).

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 20

Use pre-calculation when possible, for example allocating memory for tables before 'main loop' is entered. Avoid doing a calculation on every update if it is not necessary.

3.2

Central game objects

The focus of any traditional game is the object that the player controls, the player object, usually a character. In the case of the example game, this is the brave hero who sets on his journey to save the lands from evil. Other central objects are the interaction counterparts of the player object. The example game concentrates mainly on fighting, so it is natural that enemies are central game objects, too. Central game objects extend the base class MapObject. 3.2.1 Example: Player

The core of Player class consists of a Finite State Machine (FSM): 'body state' machine. In addition, 'virtual body states' are used as substates, to enable simple automatic maneuvers within states. For example, when the hero changes from walking to idling, he has his weapon in his hand, so he first freezes for a short period of time (allowing the player to continue walking without a long pause), after which he puts the weapon away, and only then he 'really starts idling' (that is, idle animation is shown). FSMs are based on states, inputs, and transitions. A number of the states are finite, hence the name Finite State Machine. Automata theory usually considers FSMs as acceptors, which end up either accepting or rejecting a given input. In the example game the FSM is more like a transducer, used to control, and its final state is always accepting unless an error has occurred. In a way, every Boolean variable and the related code that changes the variable's state is a separate FSM. A nice way to implement an FSM would be to write a virtual base class 'state', on which each actual state is based, and write a generic FSM class, which contains all the states in a table, then process states and transitions through the interface of the 'state' class. Another basic method is to enumerate the states and handle them in one or more switch-case statements. The latter is what has been used in the example, and it has one distinct advantage considering mobile development (besides that it requires no class inheritance): less costly function calls. The game engine controls Player through simple commands such as drink, attack, and run. When the player attacks, a list of nearby targets is collected and those who are positioned in the attack sector (which depends on the used weapon) are chosen. If Player succeeds in a skill roll (random variable), the chosen targets get hit. In the example the skill rolling is very simple. The player's attributes are defined in the script Player.lua. It is up to the designers how complex the attribute system and underlying logic should be. Basically it could be as complex a role playing game system as ever invented. 3.2.2 Example: Enemy

Enemies are controlled by an Artificial Intelligence (AI), integrated in the Enemy class. In this case AI means, like with games in general, simulation of intelligent behavior or making entities act so as they seem sentient, as opposed to what AI means in academic research. Game developers are more interested in simulating behavior, which actually includes both intelligence and emotion. The more reaction range, the better. And though talking about intelligence, one of the

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 21

secrets is to make these intelligent entities make mistakes with flair, every now and then. The core of the Enemy class consists of two FSMs: 'behavior state' machine and 'body state' machine. You can use only one machine, since everything that can be achieved with several FSMs can be achieved with one (proving this is left as homework). However, it is a tad bit easier and clearer to use multiple nested machines in hierarchical manner. The entry state of the behavior state machine is 'patrol'. When an enemy's target (in this case always the hero, though it could be made to vary) is close enough, it attacks, that is, its behavior state changes to 'combat'. If it is victorious, it goes back to patrolling. If it gets weak, it flees. A fleeing enemy might switch back to combating if it is attacked again. The final state is 'disabled.' The 'combat' state is the most complicated state these guys have, since fighting the hero is their purpose. When an enemy attacks for the first time, it approaches its target first, then hesitates for a moment, and then resumes charging. This is meant to add some behavioral realism. When getting severely hit, an enemy backs up a bit. Also, after attacking several times there is some backing up. The entry state of the body state machine is 'idle'. The body state varies a lot more often than the behavior state. An enemy idles, runs toward the hero, backs up, attacks, runs again, gets hit, and dies in the sword of the hero. As is the case with the hero (player), the body state of an enemy is closely related to the animation that shows on the screen. To maintain a smooth animation, it is a good idea to use some mechanism to prevent states from being changed too often. This is also a good idea considering game speed and efficiency. More complex calculations can be placed on decision moments rather than every update, and when decision moments do not take place too often, it is hardly noticeable. In addition, it should be noted that an enemy does not make decisions while it is in a non-interruptible state, just like the hero (player) is not given control commands while it is non-interruptible, that is, busy. The final body state of the evil enemies in the example game is 'dead', which means the hero has done his job well. Many actions, like the before-mentioned hesitation of a combat green enemy, depend on the enemy's personal attributes (notably aggression and cowardice). The attributes are defined in the level scripts, entered as parameters to the addEnemy function. Once again, the complexity of the attribute system is up to the design. Finally, it must be stated that in the example, movements of these entities are very linear and simple. Path finding is beyond the scope of this document, but sophisticated path finding (with portals and all), entities negotiating their ways around obstacles and so on, would bring a huge load of extra realism and credibility to the game. A good way to get started with path finding (as with everything else) is to use your favorite search engine with some related words to search, for example, for A star path finding.

3.3

Scripting

Scripting can reduce your workload immensely and make the overall game development workflow much smoother. Instead of seeing the results in the game after timely building, the artists can prototype and try the new content in the game real-time while they are developing it. Programmers are freed from routine content modification work and can focus on actual features. In mobile programming, scripting is especially useful, and its importance grows together with the scale and ambitiousness of a project. Testing on the target platform can be
Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 22

difficult, and you should keep the code itself as simple as possible. It is better to move the game design out of the code, into scripts. In the example there are many places where further scripting would clear up the code and make the project more and more manageable. For example, animations could be scripted, in one way or another. Comprehensive localization would also benefit from scripting. In scripting aspect, the example game concentrates on level scripting, which is one of the more central aspects of an adventure game. The scripting engine uses Lua scripting and binary compilation of scripts for the target platform. 3.3.1 Introduction to Lua

Lua is a small language especially well suited for extending C/C++ applications to be scriptable. Its origins are in a data definition language designed in 1993 for automating the task of data input to simulations in the Brazilian oil industry, but it has been used in numerous games in recent years. Lua has a number of qualities that make it suitable for game development: Lua is very small. The whole source takes about 250 kB, and run-time overhead is less than 70 kB. Lua is very easy to learn. In practice you can learn the language and start using it effectively in less than a day. Lua is easy to integrate. With the C++ wrapper (described later), the integration is trivial. Lua is very portable. Only a few standard ANSI C library functions are used. First, the language itself is introduced below. It is also useful to know the basics of the low-level Lua interface if you run into problems, so it will be discussed quickly before getting into the high-level C++ wrapper. Luas lexical conventions closely match most other common languages. Identifiers and keywords are case sensitive. Identifiers need to begin with an alphabetic character or underscore and any alphanumeric character can follow. Only single-line comments are supported, and comments start with -- (two dashes). String literals can be created with the C-like double quote (" ") pair and contain escape sequences like '\n' (new line). Lua also supports multi-line string literals in the form of the double square bracket ([[ ]]) pair, for example: a = [[ this is a multi-line string literal ]] String literals created with the [[ ]] pair can span over multiple lines, but escape sequences are not expanded inside the pair. To conform to Unix scripting conventions, if chunk starts with the # character, the first line is skipped. A chunk is the unit of execution in Lua. Lua is a dynamically typed language, as most scripting languages are. Basic types in Lua are nil, Boolean, number, string, table, function, and userdata. Boolean and number have value semantics. Strings have internally referenced semantics, but strings are immutable, so they actually appear to the user as having value semantics. Nil is a special type that roughly matches the C NULL value. Function instantiations, or closures, are firstclass variables in Lua and they have reference copy semantics, as well as tables and userdata. Lua relies heavily on associative arrays, which can be accessed by any type except nil. Associative arrays are implemented with the table type in Lua. Tables
Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 23

can also be heterogeneous; they can contain values of all types except nil. Tables are the only data structuring mechanism in Lua. In addition to named record access using array["name"], Lua provides syntactic sugar array.name and iteration over table elements using next(array,key), which returns the key and value of the next array element. Userdata is a special data type that can only be modified from C code. This guarantees integrity when extending C applications by scripting, as the scripts cannot modify the value of userdata. Userdata has reference semantics, as well as tables and functions. Metatables can be used to define operations on userdata values, which allows Lua to be extended with custom types. Every table has a metatable, which defines the behavior of the original table. For example, for addition, Lua calls the __add member of the metatable. Lua is not an object-oriented language, but it does support object-oriented programming by the usage of tables. Lua tables can contain associations to functions and the table itself can be passed as a first parameter to the function by using a colon (:) operator. For example, a:f() calls function f of table a and passes the table as an argument to the function. Lua provides type coercion between strings and numbers at run time. This is applied so that any arithmetic operation tries to convert a string to number. Also when a string is expected, a number is converted to a string automatically. As Lua was intended for extending applications, it has no concept of a main() function. The basic unit of execution is a chunk. A chunk is a sequence of Lua statements, which is optionally followed by a semicolon. Chunks are interpreted as anonymous functions, so chunks can have local variables and can return values. In addition to the traditional assignment, Lua supports assignment of multiple values. For example a,b,c = 1,2,3 assigns a = 1, b = 2, and c = 3. Parameters to assignment are evaluated applicatively, so that x,y = y,x performs the swap operation correctly between x and y. Lua has control structures similar to other common imperative languages. For example, the following code prints odd positive numbers less than 100: local local while if n = 1 odd = true n < 100 do odd then print(n) end odd = not odd n = n + 1 end In addition to while, Lua has repeat and for loops. Repeat has the form: repeat block until exp The for loop has two forms: numerical and generic. The numeric form is: for i = first,i ncrement, last do block end The generic form is: for v1,...,vn in explist do block end

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 24

Lua also has return and break statements. Return is used to return a value from a code block, and break is used to break an iteration inside the loop. Luas return statement supports multiple return values. The other difference from C language return semantics is that return and break can only be executed as the last statement of their (inner) blocks. Lua expressions are similar to C expressions. In arithmetic operators, the main difference is the power operator ^, which is only present as the pow standard library function in C. Relational operators are also similar to C, with the difference of inequality, which is ~= in Lua. Logical operators are expressed in written form: and, not, or. Booleans are true and false, but they are a quite recent addition to Lua and so nil is still considered false in versions preceding Lua 5.0. String concatenation is done with the double period (..) operator. Due to coercion rules, numbers can be concatenated to strings with this as well. Precedence order in Lua is similar to other common languages like C. Concatenation comes after arithmetic but before relational operators. Lua functions are first-class variables with reference copy semantics. A function is defined by: function f( parameters ) ... end or f = function( parameters ) ... end 3.3.2 Lua and C++ integration

Lua can be used as a language for operating system scripting, but the main focus of Lua usage has always been enhancing applications with scripting. This section describes the basics of Lua and C/C++ integration, with an introduction to the core Lua library C language interface, C++ techniques to make Lua usage more convenient and less error prone, the usage of the C++ wrapper, using Lua scripts for data definition, and making function calls with Lua script. One of the many attractive points of Lua is that it is easy to get started using it. Lua implementation is fully reentrant, which means that it has no global variables. The Lua library state is contained in lua_State, which is initialized and deinitialized by calling lua_open and lua_close, respectively. Script compilation can be done by simply calling lua_dofile with the Lua state and name of the script file as parameters. Unfortunately, things get a bit more complicated when you start calling functions defined in Lua scripts, and even more complicated when you want Lua scripts to be able to call your functions. Calling functions defined in Lua scripts is heavily based on the manipulation of the Lua virtual stack. The Lua virtual stack is used to pass values to and from C. Assume, for example, that you want to call a Lua script function that is an entry in a Lua hash table. This is a very common scenario in Lua usage, as tables are used frequently as roughly corresponding to objects in C++, so table functions correspond to C++ methods as well. To achieve this relatively simple operation, you need to: 1. Push the table reference to the Lua virtual stack.
Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 25

2. Push the hash table key (the function name) to the Lua virtual stack with lua_pushstring. 3. Call the Lua C interface lua_gettable function with the correct Lua virtual stack index, which specifies where to find the table in which the hash table entry, defined by the previously pushed key, is located. In this case the correct index is 2, which means that the table to be used in this operation is the second element from the top of the stack backward. 4. Push any function parameters, for example a number, to the Lua virtual stack with lua_pushnumber. Arguments are passed to the stack in direct order, which means that the first argument to the function needs to be pushed to the virtual stack first as well. 5. Execute the actual script function by calling the lua_call function with the correct number of parameters. Note that function arguments are not type checked; you need to check the argument types yourself if you want to be sure that the programmer did not pass incorrect arguments to the Lua virtual stack, say, by calling lua_pushstring when he or she should have called lua_pushnumber. 6. After the function has been executed by the Lua interpreter, the parameters returned by the Lua function are returned to the Lua virtual stack. 7. Parameters are not type checked either, so the programmer needs to verify types manually, for example, by calling lua_isnumber for each returned number parameter. 8. The programmer needs to retrieve the parameters, for example, by calling lua_tonumber with the correct index to retrieve the actual return value. 9. At the end, the programmer needs to clear the returned values from the Lua virtual stack by calling lua_pop(n).

Here is the same process as C code: lua_getref(luastate, table); lua_pushstring(luastate, "myfunc"); if (!lua_istable(luastate,-2)) error("invalid table reference"); lua_gettable(luastate, -2); lua_pushnumber(luastate, x); if (!lua_istable(luastate,-2)) error("cannot call non-function"); lua_call(luastate, 1, 1); if (!lua_isnumber(luastate,-1)) error("function myfunc didn't return number"); float returnval = lua_tonumber(luastate, -1); lua_pop(luastate, 1); The mechanics of Lua usage are similar when you want to be able to allow Lua scripts to call your C functions. C functions need to have a declaration of: int scriptableCFunction(lua_State* luastate); to be able to be called from a Lua script. Arguments are again received in direct order, and return values are sent to the top of the stack in direct order as well. The actual return value integer represents the number of return values left at the top of the stack.
Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 26

Needless to say, programming Lua this way is tedious and highly error prone. For example, the programmer might accidentally call a wrong table by using an index that is off by one, which is an easy mistake to make. Even the Lua 5.0 reference manual suggests using macros or some other high-level mechanism for actual Lua library usage. Luckily, C++ provides mechanisms to make Lua usage effortless in actual applications. Initialization and script compilation is already straightforward with the Lua C API, but stack management was the problem in the examples shown earlier. Lua virtual stack management can be made considerably easier to use with modern C++ features. Obviously, the first place to start when abstracting Lua virtual stack usage is the Lua hash table type, which is in heavy use especially when using Lua tables as C++ object counterparts. In the example Lua C++ wrapper this abstraction was done with the C++ class LuaTable. LuaTable provided basic table operations like setString, setNumber, and so on, to avoid burdening the application programmer with Lua low-level stack management. This makes Lua table usage quite simple. So instead of writing the following: lua_getref( luastate, table ); lua_pushstring( luastate, "mystring" ); if ( !lua_istable( luastate,-2 ) ) error( "invalid table" ); lua_gettable( luastate, -2 ); if ( !lua_isstring( luastate,-1 ) ) error( "variable is not string" ); char mystring[ ENOUGH_SPACE ]; strcpy( mystring, lua_tostring( luastate,-1 ) ); lua_pop( luastate, 2 ); the programmer can simply write: String mystring = table.getString( "mystring" ); without having to be concerned with Lua virtual stack management details. A more difficult target for abstraction is the function calls. If you look at the calls made from C++ to Lua scripts, you see that the only things that differ between calls are: Name of the Lua script function to be called Number of parameters given to the function Type of each parameter Optional return value and its type Name-based calling of a Lua script function is easy to abstract. It can simply be done in a similar fashion as the table and string usage example shown earlier. For example, table.call("myfunc"); could be a simple call to a Lua script function without parameters or return values. The number of function arguments and their types are a bit more difficult. The argument types can be abstracted by providing overloaded functions to push values to the Lua virtual stack, so that the programmer does not need to know the actual types pushed as Lua function parameters. The number of arguments can be abstracted by providing overloaded versions of the previously introduced call abstraction for the different number of parameters, say, from zero to 10 parameters. Now you can type

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 27

table.call("myfunc",123,"hello"); without virtual stack management issues. Receiving function calls from Lua is the hardest task. The ideal case is to write a C++ function as is and then just register it to Lua by making a simple call like luastate->registerFunction( "myfunc", myfunc ); This can be achieved by making registerFunction a template function that has the function type F as a template parameter. Clearly, at the time registerFunction is called, the compiler knows the declaration of the function parameter. This type can be used to form a new template function with the same calling convention as a regular Lua C function. This function in turn knows F, and can forward the function call to the correct function. This template magic might sound a bit confusing, but luckily the usage is easy. Next, the actual C++ wrapper usage is discussed. A Lua wrapper consists of three classes (LuaState, LuaTable, LuaObject) and an exception class (LuaException) thrown when something goes wrong. You use the wrapper by deriving your own (script-enabled) classes from LuaObject. At run time you need to create LuaState and register scriptable functions in each LuaObject-derived class instance, and youre done. Every step is described in detail below. LuaState, as the name implies, is a C++ wrapper for the low-level lua_State class, which is the heart of Lua. LuaState acts as a virtual machine for the script code. For example, each LuaState has its own global namespace. LuaState is actually only a thin wrapper around lua_State; LuaStates main responsibilities are to provide exception-based error handling support when using Lua. The LuaTable and LuaObject classes are more interesting, and are discussed in the next sections. LuaTable is, as the name suggests, a handle to a Lua table. LuaTable simplifies a great deal of Lua table usage. You can query and set properties of tables, even members, which are also tables. For example, the following code shows how to create a LuaState and then a LuaTable to the state (virtual machine) and set the tables myVariable to 123.456: LuaState lua; LuaTable tab( &lua ); tab.setNumber( "myVariable", 123.456f ); LuaObject is the glue between your class and Lua scripts. You use the class by deriving your own class from it, register functions, and compile script associated with the object. After registering the functions, Lua script can call your C++ code. Script functions defined in the Lua script can also be called directly from C++ code. 3.3.3 Data scripting

The following code snippet reads Player's basic attributes: lua::LuaState* lua = new LuaState; LuaTable playertab( lua ); FileInputStream in( expandPath( "data/player.lua" ) ); lua->compile( &in, in.available(), "player", &playertab );
Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 28

m_player.setHitPoints( playertab.getNumber( "hitpoints" ) ); m_player.setSpeed( playertab.getNumber( "speed" ) ); from the script player.lua: -- player.lua: speed = 100 hitpoints = 250 Lua script is compiled using an empty tab table as a running environment for the script. In practice, it means that all assignments assign values to the table members. This way the scripts can work more or less in the same way as C++ constructors. C++ code reflects this; notice that the tab table is passed to the compile function as the last argument. 3.3.4 Object scripting

If, for example, you have a level script where you want to add an enemy: -- level.lua -- addEnemy( x, y, hitpoints ) addEnemy( 1, 1, 100 ) It is simple enough to implement the function in C++: float Converter::addEnemy( float x, float y, float hitpoints ) { Enemy o; o.setPosition( x, y ); o.setHitPoints( hitpoints ); m_enemies.add( o ); return (float)( m_enemies.size() - 1 ); } However, the actual trick is the binding: Converter::Converter( LuaState* lua ) : LuaObject( luastate ) { registerMethod( "addEnemy", (Converter*) this, &Converter::addEnemy ); } Here you register the method of the class to the Lua object, so that they can be used in the Lua script as well. No special code is needed to make the function calls. 3.3.5 Off-line scripting

Scripts are edited on a computer, not on a phone, so it makes sense to convert all the scripted data before it is transferred to the phone. Converting scripts, or compiling them beforehand, is advisable, because there is hardly loss but several advantageous aspects: Script parsing does not have to be implemented on the target platform.

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 29

Data loading is simpler and thus more likely to work. Data loading is considerably faster. The previous section discussed parsing a script where an enemy is added to a level. To save the data in binary format for mobile platform, the following code could be used: // save enemies out.write( &id, sizeof(id) ); ++id; size = m_enemies.size(); out.write( &size, sizeof(size) ); for ( int i = 0; i < m_enemies.size() ; ++i ) m_enemies[ i ].write( &out ); Likewise, the following code could be used for loading the data: // load enemies checkId( &in, okid++ ); in.read( &size, sizeof(size) ); m_enemies.resize( size ); for ( int i = 0; i < m_enemies.size() ; ++i ) { m_enemies[ i ].read( &in ); addObject( &m_enemies[ i ] ); } The saving is executed on the PC right after the script is parsed. In the final mobile build of the game, the loading procedure replaces the entire script parsing. Now the game is running efficiently on a mobile platform and the target code is kept simple, yet you have the powerful scripting in use.

3.4

Scripting with triggers

The secondary focus of the example game is simple puzzle solving. This can be achieved via the concept of Trigger. The game logic itself, proceeding in the game, moving from one level to another, puzzles, and finishing the game is, in the example, achieved by using triggers. It is a powerful way of controlling game logic and proceeding via scripting. Basically triggers are how the player can alter the milieu and move through it. In essence, puzzle solving is the player altering the world from the obstacle-ridden state, into which it was scripted by game designers, to the passage-free state, which leads to the completion of the game. 3.4.1 How triggers work

Triggers use the collision system of the game as if they were game objects. In a typical game you could, for example, open doors, pick up items, talk to nonplayer characters, use explosives to make your way through obstacles, and so on. Obviously you have numerous choices. It is clear in this context that you, as a programmer and system designer, absolutely need to move most of the game play complexity to somewhere other than in the player class. The world is a good candidate. This is where triggers come into play. A trigger, as the name suggests, is a passive object until some other game object signals the trigger, in other words activates it. Triggers usually have some volume, like a box or sphere, that defines the range in which the trigger can be activated.
Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 30

In the example game, the volume of a trigger is exactly one map block. This is convenient, since checking Players collision with a trigger equals to checking whether Player is positioned in the same block as the trigger or not. An activator object can be a player character, AI entity, bullet, camera, or any other game object. Different triggers can listen for different sets of activator objects. For example, you probably do not want a screen to be activated unless it is the players character arriving at a particular site, but a trap could be activated by anyone. In the limited-scope example game, Player is the only activator object. A trigger can be activated once when an object enters the space of the trigger, or it can be activated continuously when an object is in the range of the trigger. A one-shot trigger could be something like an item that is picked up (the item is not there anymore after it has been picked up) and a continuous trigger could be something like lava, which causes damage to the player for the entire time the player is walking on it. In the example game, one-shot triggering is not supported by code. A nice property of triggers is that it moves complexity from game objects to the game world in a context-sensitive manner. For example, when a character opens a door by walking towards it, the character itself does not necessarily need to know anything about the door being opened. (The character collides first with the open-door trigger, which opens the door, so the character never actually knows that the door is there.) Triggers do not have to be bound to some specific location or area. For example, the characters in a game could cause continuous walking sound triggers to be emitted while they walk. This sound trigger would inform enemies in the trigger area about itself. Other characters can then react to this sound trigger, which could cause a guard to be alarmed. Triggers can also be based on timers or other properties that are needed in a specific game. Basically, any event in the game can cause a trigger to be created or signaled, and often triggers provide very elegant and general-purpose design solutions. In addition to moving complexity from the game objects to small manageable scripts, triggers are efficient performance-wise, as they do not require continuous checking from the objects. Instead of player character code actively looping through all doors in the game level for nearby doors it could open, a playeraware trigger is signaled when a player is nearby and the trigger (script) opens the door in front of the player. As triggers use the same collision system as other objects, all triggers can be handled O(n) time instead of O(n2), which would be necessary if all objects were to check the triggers separately. 3.4.2 Integrating triggers into game

Triggers use the collision system of the game as if they were game objects. The difference is that even though triggers are collided against, they do not use the collision information to provide any kind of collision response to either of the objects, but they view the collision only as a potential activation signal. As triggers are very small and specialized systems (for example, one trigger could just call one damage(x) function to give damage to the player), they are ideal candidates for scripting. Like any other objects, they need an initialization function to set up their state. They also can have signal functions for different types of objects they want to be aware of. In the example game, the effect of each trigger is hard coded, for the sake of simplicity.

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 31

What different triggers do in each of these functions is specific to the game. For example, in a platformer game, a coin trigger might just increment the number of coins collected and remove the coin mesh from the game level. In the example game it is possible, for example, to create a trigger, which opens a dialog screen addScreenTrigger( "", 2, 11, "adventurer_speaks" ) which corresponds to the following code (simplified): void Map::addScreenTrigger( String name, float x, float y, String screen ) { ScreenTrigger o; String::cpy( o.name, sizeof(o.name), name ); o.block[0] = Fixf( x ); o.block[1] = Fixf( y ); o.active = true; GameApp* app = GameApp::get(); o.screenIndex = app->findItem( app->screenNames, app->screenCount, screen.c_str() ); m_screenTriggers.add( o ); }

The code that handles the trigger collision testing and the actual effect of the trigger reads as follows: if ( trigger.isCollision( player ) ) { m_map->removePlayer(); LevelTrigger lt = trigger; loadMap( trigger.filename ); m_map->setPlayerStartPosition( lt.targetblock ); m_resetTime = true; return true; } 3.4.3 Triggers and inventory

It is possible to simulate an inventory system with triggers alone. On the other hand you can also enhance triggering system with an inventory. In the example game, triggers are local to each level or room, so it is convenient to use items or metaitems to store information, which is needed in another level or room. The player finds a shotgun in a certain room of the castle, and can use it as a weapon from that point on. The player finds a key in a shed and, having the key, is able to enter the castle. These are examples of real items. In one of the castle rooms there is a drawbridge, which can be opened by pulling a lever in another room. When the player pulls the lever, that is, enters the block where the lever is, he gets a meta-item called lever. Having that item causes the player, eventually, to enter a different version of the drawbridge room, where the drawbridge does not the block the passageway anymore. In effect, the player seems to pull the lever and turns the drawbridge.

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 32

In the example game, inventory-triggers integration is implemented so that there are triggers for enabling or disabling other triggers based on whether the player has a certain item or not. 3.4.4 Level scripting example

The following is an actual level scripting example from the example game: -- monastery.lua loadMappy( "monastery.pms", "monastery_out.tga" ) setStartBlock( 7, 29 ) addLevelTrigger( "", 7, 30, "forest_ghoul", 10, 13 ) addTextTrigger("mapfound", 7, 29, "You use the map and\n find your way\n through the forest.", 3 ) addLevelTrigger( "", 14, 17, "lato01", 2, 11 ) addLevelTrigger( "", 13, 14, "lato02", 4, 8 ) addLevelTrigger( "", 5, 13, "lato03", 5, 7 ) addTextTrigger("keynotfound", 11, 9, "It is locked!", 1 ) addLevelTrigger("keyfound", 11, 9, "castle01", 3, 12 ) setTriggerByItem( "key", "keyfound" ) setTriggerReverseByItem( "key", "keynotfound" ) addEnemy( "", 11, 9, 225, "OGRE", 15, 60, 10, 40, 150, 0 ) First, the data files connected to the level are defined and the default starting block is set. A trigger for entering the previous level forest_ghoul is added. A text trigger that throws some text on the screen when the player enters the level is added. There are level triggers to lead the player to sheds (lato01, lato02, lato03). There is a text trigger for informing the player that the castle door is locked if the key is not found, and there is a level trigger to allow the player to enter the castle if the key is found. The actual key item is in the level lato01. Whether the player has the key item or not, at this stage, dictates the state of keyfound and keynotfound triggers, that is, whether these triggers are enabled or disabled. Finally, an enemy to guard the castle door is added. Every trigger's initial state is enabled, unless there is a disableTrigger call for the trigger, at the end of the script. Note that both setTriggerByItem and setTriggerReverseByItem update the defined triggers on every game cycle. 3.4.5 Triggers and story telling

Triggers are often used to give the game a more cinematic look and feel. They can be used to associate cinematic events to either game play or location. The simplest example is a screen that is triggered when the player arrives in a new area. Another example is an enemy whose behavior is altered by a trigger. Certainly you can create a friendly character, too, who gives the player a mission of sorts and, after the mission is accomplished, tells the player something, maybe even gives him something. You could have triggers affecting existing game objects, or
Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 33

you can just create an impression of a character, just as you can create a feeling of inventory and items even if they do not actually exist. In the beginning of the example game there is a sage character, who does not actually exist code-wise, as an object, except for the sprite that is rendered into a specified map block. All this makes the player really feel more like he is in the game, since the world seems to interact with his actions. 3.4.6 Trigger script error handling

How to handle scripting errors, and how to spot errors in the first place? There are several solutions to this issue, but one is to use defensive programming when providing the interface for scripts. Errors do not affect the proceeding of the game: no error message or such. Usually error messages are better choices than hiding bugs with defensive programming, but level scripts can be edited by anyone (who might have no idea of the code under the hood), so it is better to be on the safe side with error handling, at least when you ship the game. It is annoying for the user to get stuck in a game due to any error, and that includes script errors as well. This does not mean that error conditions should not still be noticed and logged for later reviewing.

3.5

Memory management

Memory is a limited resource on a mobile device, or at least it has been that way so far. Also, memory allocation in game main update is slow, and frequent allocation and freeing fragments the memory. It is important to take memory management into account in your design. 3.5.1 Estimating memory usage

Memory usage is a useful indicator of the project scope. Comparing your estimation to the limits of different target platforms gives you an idea of how realistic your design actually is and what you may have to do to get the game running on different platforms. It is better to make these changes on desk and not in reality. Graphics, animations especially, require a lot of work, and it would be catastrophic to realize in the middle of a project that they need to be redone because of memory issues. So, make your estimations well. The actual calculation is easy. For example: For each animation, the memory usage m = frame count * width * height * bytes per pixel The pixel format into which you save your files has a huge impact on the size, as well as the sound format that you use (packed, unpacked, 8-bit/16-bit, 8 kHz, 12 kHz, 16 kHz ...). Naturally the file size is not the same as the memory requirement. Most data has to exist unpacked in the memory to allow fast enough handling. Make separate estimations for best cases, average cases, and worst cases. Design by the worst case and add some extra buffer just to be sure. 3.5.2 Designing for effective memory usage

The design of an application affects memory usage greatly. It is easy to end up using a double amount of memory for insignificant differences. Decide what is
Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 34

actually needed and what is not. Maximize the reuse of common elements. It is nice if you can use same block bitmaps for several levels, right to the point until it gets too repetitive. Reusing arrays is extremely advisable. For example, if the game needs to show long sequences of images but the speed is not a problem, you could load them at run time, using one and the same buffer over and over again. A bitmap related to a new level can be loaded over the corresponding bitmap of the old level. Minimizing reallocation boosts up the performance and alleviates memory fragmentation problem. Data packing is of course one technique that can be used as long as it is possible to unpack the data fast enough. Also, so-called null data is better to be optimized out, for example in bitmaps empty regions around objects.

3.6

Animation system and tools

Animating is an important aspect of the visuals of an isometric adventure game. In the example game this is based on the Surface class; animations are multiple frames stored one upon another in a single image. It is easy enough to implement a few functions for setting animation properties like FPS (Frames Per Second) and running the animation. Another technique is to store the first frame of an animation and after that, for each frame, store what is different from the previous frame. More advanced techniques could be used, too, but it might be exaggerating for a mobile game. Usually all frames of a single animation are required to be of equal size. This makes things a lot easier. Also, it is easier if all these frames are centered around the same point, a point that is here called reference point, which is used to determine the center of each animation. For example, in the case of the hero in the example game, the reference point of each animation is positioned right under his feet. 3.6.1 Animation generation from bitmaps

Animation (and sprites and screens and fonts) are stored in a custom NAN format, which can be easily loaded into the Surface classes the game uses. Animation files were generated using a custom conversion program, which basically loads animation frame images, and combines and crops them. A BAT file was used for automating the process. An advanced animation scripting system would be a very useful improvement. 3.6.2 Example: Surface

The reusable Surface class stores an image in A4R4G4B4 format. Animations are stored as a sequence of animation frames (of equal size) stored one upon each other. Basically a function called getFrame( time ) is used to determine the current frame, based on time. Then, a function called clipFrame( frame ) is used to create a new surface of the defined animation frame. The new surface is then blitted on the screen buffer. On generation, surfaces are optimized so that empty areas around objects are cropped and an offset value is stored to enable the positioning of the image as if it was never cropped in the first place. In the example game, each animation is stored as a surface. Animation lists are stored in GameApp as well as the reference points for these animations. The reference points are used to position each animation correctly on the screen.

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 35

3.7

Isometric rendering

Isometric rendering is a rather straightforward process. Drawing the blocks, characters, and so on from back to front pretty much guarantees the correct drawing order. There is some overlap if blocks contain elements that are taller than one block. To render such blocks correctly, the routine needs to extend its scanning below the screen bottom level, just in case there is a tall column in the foreground, and the base of the column is out of sight but its top should be in the view. 3.7.1 Rendering example

Map data is stored in the pattern that the editor, Mappy, uses. The difference to a grid-like map is that advancing in x coordinates could take you upwards and downwards, and a block with a greater y coordinate still might have its upper region higher than the lower region of a block with a smaller y coordinate. The rendering is, nevertheless, straightforward. First check the rendering position and find boundaries for the area you want to show. Then it is simple back to front scanning along the map, fetching the bitmap for each block and drawing it. Sprites are also easy in the case of this game, because they always fit inside the boundaries of their base blocks. It gets tricky only in the case of map objects (player, enemies), which can be located anywhere and can be bigger than one block. In a simple system like this, every object belongs to one block. It is after drawing the base of that block when the object is drawn, too. The example code uses the following functions to determine the base block of each object: blockRenderPositionReference() blockRenderPositionExtreme() The first function returns the block where the reference point of an animation is located. The latter function returns the last block in rendering order that is located under any part of the animation. Usually the scan routine meets the reference block first. If it has more than two levels (it contains, for example, a piece of fence), it draws the object right there. Now, for example, the object is drawn correctly if it moves behind a fence. Otherwise, the extreme block is used. Using it guarantees that no part of the animation is left under the base level of the map (grass, road, floor, or such).

3.8

Different screen resolutions and modes

If an application is intended to support more than one platform, it is unlikely that supporting one target screen resolution is enough. This should be taken into account early in the design process. The first requirement is that the platform-specific code that flips the screen buffer data onto the screen scales automatically to the screen size and supports all pixel formats that are needed. 16-bit A4R4G4B4 is the main pixel format in the example game. Other formats can be supported with real-time translation. A faster way to support multiple pixel formats is to create separate blitting routines for each pixel format, maybe even have different bitmaps for each pixel format. Thus the screen buffer contains the data in the correct format, and the procedure of showing visuals on the actual screen reduces to a simple task of copying, which is of course faster than translation. Varying screen resolutions may also call for multiple sets of data. If the difference between resolutions is minimal, say, from 176x208 to 240x320, it is possible to manage
Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 36

this by aligning HUD elements by the edges of the screen and just drawing a few more map blocks, if there are more pixels. However, if the differences are greater, visuals get too small on greater resolutions or too big on lower resolutions. Only 3D rendering is easy to scale to any resolution. Scaling 2D would require a zoom option in drawing functionality, which again might require filtering to look good; either way, it is slow. A good solution is to use different sets of visual data for different resolutions. In the example game, graphics were designed for the target resolution of 480x640. For supporting lower resolutions, the game uses smaller versions of block bitmaps and automatically resamples other visuals except for the font, which remains the same. Shrinking higher-resolution graphics produces a significantly better result than enlarging lower-resolution graphics, though the data takes more space. Load-time resampling takes some time but it is faster than the loading process anyway, and it does not require much memory because the same resampling buffer can be reused for every loading.

3.9

Debugging

Debugging on a mobile device can be tricky and until Carbide.cpp Professional there simply have not been too many solutions for on-device debugging not even in development tools specifically targeted at mobile application development. If development with desktop tools has been chosen, a good enough framework should be used to minimize the need for on-device debugging; develop on another platform, like desktop. Then it is good to use the emulator, which enables stepping through the code, checking the call stack and all the convenient stuff that helps in debugging. Finally you run into problems that manifest themselves only on the target platform. If you can find the address where the problem occurs, you can check the address in the map file that you installed on the device together with the application. Another good method is to write debug messages to a log file; when a problem occurs, you can check if there is any useful information in the log. Screen output is also very practical, especially because text is immediately visible to the tester. Specifically, game initialization and loading points are places where many different problems can occur, so it makes sense to make both initialization and all loading functions phased, that is, the function is called several times. After all, in a singlethreaded application it would be nice to know how the execution of such a complex function proceeds. On every phased call the function does a task and returns. Frequent returning allows frequent screen update and printing of some debug text.

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 37

4.

Summary

This document has given you some ideas on how to approach high-level game development on a mobile platform with limited resources using development tools for desktop environment. The document used an isometric adventure game ('isometric 3D') as an example, but most concepts used here, for example, memory buffer re-cycling, offline scripting, and avoiding resolution dependencies apply to other game genres as well. The most important aspect of mobile game development is planning for the target memory budget. Memory fragmentation is also an issue which might be hard to fix in a later phase if not addressed right from the start of the design. Not only the programming but also the game content creation team needs to take memory and mobile platform considerations into account. Having too much animations or other memory space-consuming game content can be a real killer if noticed in a late phase of the development, and it can potentially waste a lot of work when you have to cut down already created content. The second important aspect is the screen resolution. Even the best-looking content on a desktop display does not necessary work on a screen with limited size and resolution. The most important aspect of a game is, nevertheless, that it is fun to play, and gameplay can severely suffer if screen layout or game controls do not take advantage of the mobile platform.

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 38

5.

References

[1] http://en.wikipedia.org/wiki/Isometric_projection [2] S60 Platform: Isometric Adventure Game Example, available at www.forum.nokia.com [3] S60 Platform: Programming Games In C++, available at www.forum.nokia.com

Version 1.0 | August 15, 2006

Designing Isometric Adventure Games | 39

6.

Evaluate this resource

Please spare a moment to help us improve documentation quality and recognize the resources you find most valuable, by rating this resource.

Version 1.0 | August 15, 2006

Potrebbero piacerti anche