5 the least you can do versus an architectls approach
Transcription
5 the least you can do versus an architectls approach
THE LEAST YOU CAN DO VERSUS AN ARCHITECT’S APPROACH 5 CHAPTER OUTLINE Basic Encapsulation: Classes and Containers 78 Store Relevant Values as Variables and Constants 79 Don’t Rely on Your Stage 80 Don’t Use Frameworks or Patterns You Don’t Understand or That Don’t Apply 81 Know When It’s Okay to Phone It In and When It Definitely Isn’t 81 Transitioning to Architecture 82 OOP Concepts 82 Encapsulation 83 Inheritance 83 Polymorphism 84 Interfaces 84 Practical OOP in Game Development 85 The Singleton: A Good Document Pattern 86 Summary 89 The subtitle of this book may be How to Follow Best Practices, but it’s only fair to cover some “worst practices” and basic pitfalls you should avoid when getting started. As such, the first half will look at the bare minimum any Flash game developer should do, regardless of the circumstances. Once you have the basics known, you can “graduate” to the second half of this chapter where we’ll examine how to look at your games like an architect from day one. One of the most common phrases I hear developers (including myself from time to time) use to justify lackluster coding is, “Well, this project just didn’t afford me the time.” The implication here is that if the developer had more time to do the work, it would have been done better. I certainly don’t disagree with that premise. Before I worked at a game company, I was employed by an interactive ad agency. Anyone who has ever worked at an ad agency knows that there is never enough time on any project, ever. Forget formalized design patterns and wireframes, we’re talking about timelines in which it’s Real-World Flash Game Development, Second Edition. DOI: 10.1016/B978-0-240-81768-2.00005-3 © 2012 Elsevier Inc. All rights reserved. 77 78 Chapter 5 THE LEAST YOU CAN DO VERSUS AN ARCHITECT’S APPROACH hard to find time to use the bathroom. I have built the core mechanics for a game in less (but not much less) than 24 hours; it wasn’t pretty but it got the job done. I believe most reasonable people could agree that a day or two turnaround for any game, regardless of complexity, is utterly absurd, and any project manager or account executive who agrees to such a timeline should be flogged publicly. Despite all of this, I do think that abandoning all sense of standards, forward thinking, or just reasonable programming principles because you were given a ridiculous schedule is not a good practice. In my experience, coding a game rigidly and badly saves no more real time than coding it in a halfway decent way, so why not strive for the higher standard? In this chapter, I’ll outline some examples of “the least you can do,” even when you don’t have much time on your hands. If you follow these basic principles when you’re in crunch time, you (and anyone else who has to look at your code) will be thanking yourself later on down the road. Basic Encapsulation: Classes and Containers I once had to make edits to a game in which the developer had, for the supposed sake of simplicity and speed, put virtually all of the codes for the game, menu screens, and results screen in the same document class. Needless to say, it was an organizational nightmare. There was absolutely nothing separating game logic from the navigational structure or the leaderboard code. I’m sure at that time, this kept the developer from switching between files, but at an ultimately very high cost. The code was an ugly step up from just having it all tossed on the first frame of the timeline. Here are the steps the developer should have taken to improve the readability and editability of his or her code, in order of importance: • Move all game logics to its own class. At the bare minimum, any code that controls the mechanics of a game should be encapsulated by itself, away from irrelevant information. This is the core of the game, and the most likely candidate for re-use—it should not be lumped in with everything else. • Move code for each discrete screen or state of the game to its respective class. If the game has a title screen, rules screen, gameplay screen, and results screen, there should be a class for each. In addition, the document class should be used to move between them and manage which one is active. This doesn’t sound unreasonable, does it? It’s hardly a formalized structure, but it can be up to far more scrutiny than the previous “structure.” Chapter 5 THE LEAST YOU CAN DO VERSUS AN ARCHITECT’S APPROACH Store Relevant Values as Variables and Constants If you work with string or numeric properties that represent a value in your code (such as the speed of a player, the value of gravity in a simulation, or the multiplier for a score bonus), store them as a variable or a constant. “Well, duh,” you’re probably thinking right now, “Who wouldn’t do that?!?” Sadly, I have to say I’ve seen a lot of codes over the years which were hurriedly thrown together, and the same numeric values were repeated all over the place instead of using a variable. Here’s an example: player.x += 10 * Math.cos(angle); player.y += 10 * Math.sin(angle); In their haste, a developer was probably testing values to determine the proper speed at which to move the player Sprite and just used the number directly in the equation. It would have been virtually no extra time to simply assign the number to a variable, speed, and then use the variable in the code instead. var speed:Number = 10; // player.x += speed * Math.cos(angle); player.y += speed * Math.sin(angle); Now if something changes in the game before it’s finished which requires a change in player speed, it will require altering only a single line of code versus how ever many places that value was used. Although this seems like a very simple exercise, a number of otherwise good developers have been guilty of this at one time or another because they were rushing. While this example is obvious, there are other instances of this phenomenon, which might not occur to developers immediately. One example that comes to mind is the names of event types. Many Flash developers with a background in ActionScript 2 are used to name events using raw strings: addEventListener("init",initMethod); In ActionScript 3, Adobe introduced constants: values that will never change but are helpful to enumerate. One of the key uses of constants is in naming event types. public static const INIT:String = "init"; addEventListener(INIT, initMethod); There are a number of reasons for following this syntax. The first is that it follows the above example: if you are going to use 79 80 Chapter 5 THE LEAST YOU CAN DO VERSUS AN ARCHITECT’S APPROACH a value more than once anywhere in your code, it should be stored in memory to change it easier. The second reason is that by declaring event types and other constants in all capital letters, they stand out in your code if someone else is looking at them. Perhaps the most important reason, however, is compile-time checking. When Flash compiles your SWF, it runs through all the codes to look for misuse of syntax and other errors. addEventListener("init", method1); addEventListener("inti", method2); If I had the previous two lines of code in different parts of the same class, Flash would not throw an error when I compiled it. public static const INIT:String = "init"; addEventLister(INIT, method1); addEventLister(INTI, method2); However, had I used a constant value from above and misspelled the name of the constant, Flash would have warned me about my mistake when I tried to compile it. This type of checking is utterly invaluable at the eleventh hour when you’re trying to get a project out the door and don’t have time to debug inexplicable errors. Don’t Rely on Your Stage When a developer is working on a game in a crunch, it is often in a vacuum. He or she can take certain things for granted, such as the size of the Stage of their SWF. However, if that SWF is loaded into another container of different dimensions, the game’s mechanic can be adversely affected. For instance, the following lines of code center an object horizontally and vertically on the stage, assuming its container lines up with the upper left-hand corner of the stage and its registration point is in its center. player.x = stage.stageWidth/2; player.y = stage.stageHeight/2; If the SWF containing this code is loaded into a larger SWF, it is unlikely it will still have the desired effect. The better option in this case is to use the less-frequently known width and height values in the LoaderInfo object for the SWF. Every SWF knows what its intended stage size should be and that information is stored in an object that is accessible to every DisplayObject in the display list. The two lines above would simply become: player.x = loaderInfo.width/2; player.y = loaderInfo.height/2; Chapter 5 THE LEAST YOU CAN DO VERSUS AN ARCHITECT’S APPROACH These values will stay consistent even if the stage does not. One exception to this is if you are working with scalable content (like a universal iPhone/iPad app) and the original size of the stage is irrelevant to how elements on the screen need to be laid out. Don’t Use Frameworks or Patterns You Don’t Understand or That Don’t Apply This may sound like an odd item in a list of bad practices to avoid when you’re pressed for time, but it is yet another very real scenario I’ve witnessed with my own eyes. It is the opposite of gross underengineering—obscene overengineering—and it is every bit as much a crime … as development crimes go. An example might be trying to apply a complex design pattern to a very simple execution. Some developers are tempted by many OOP frameworks that exist because of the generosity of the Flash community as a way to speed up development in a crunch. However, if the developer doesn’t really understand the framework and how to implement it effectively, they will have essentially added an enormous amount of bulk to their project for no reason and will often end up “rewiring” how the framework is intended to function because it should never have been used in the first place. Another project I recently had to make edits was created with a model-view-controller (MVC) framework designed to force adherence to the design pattern of the same name. However, because of the architecture of the framework, it meant that related code was scattered over at least 20 different class files. Some of the classes only had one or two methods or properties associated with it, making it a bread-crumb trail to attempt to debug. It was a classic example of overengineering; the game was not complicated or varied enough to warrant such a robust system, but the developer equated using an OOP framework with good programming, so they used it anyway. As a result, it took probably twice as long to fix bugs in the game because it was hard to track down where the logic for different parts of the game was stored. Know When It’s Okay to Phone It In and When It Definitely Isn’t If you’re producing games independently of an employer or client, either for profit or for an experiment, the stakes are much lower. Fewer people, if any, are ever going to see your code, let alone have to work with it. You can get away with some sloppier standards or rushed programming. In fact, some of the best foundations for games I’ve seen have been born out of hastily thrown 81 82 Chapter 5 THE LEAST YOU CAN DO VERSUS AN ARCHITECT’S APPROACH together “code brainstorms.” In experimentation, all you’re interested about is the “idea” behind a mechanism. However, the moment you start answering to anyone else about your code, be it a client or a coworker, it is vital to take the time to do it right. No one is perfect, and no one’s code is perfect either, but there’s a huge visible difference between someone who made a genuine effort and someone who did not. Even if you’re independent now, don’t turn a blind eye to your coding practices—you might want to get a job someday and many employers like to see code samples. Now that we’ve looked at the bare minimum, let’s look at higher ideals toward which we should strive. Transitioning to Architecture Ever since ActionScript 3 was introduced, there has been a flurry of interest regarding architecture and design patterns. If you read Chapter 1, you will know that design patterns are basically a blueprint or template for solving development problems. They are meant to provide re-usable architecture when building applications. In some areas of the programming community, design patterns are an essential part of application development. That said, more often than not, design patterns implemented in ActionScript tend to hamper development because they work against the natural grain of the language. One reason for this is that AS3 is already somewhat designed as a language to work in a certain way, specifically with events. In this chapter, we’ll explore some of the basic fundamentals of object-oriented programming to keep in mind as we develop, some programming styles and design patterns that work, and when you should ignore the hype. OOP Concepts As I mentioned in Chapter 1, object-oriented programming (OOP) is a model of software design centered around the concept of objects interacting with each other. To put it into game terms, every character on the screen in a game would be a unique object, as well as interactive elements around them. They would all have commands they accept and messages they can broadcast to each other. By having each object responsible for its own behavior, programming becomes much more modular and flexible. Abstractly, this is probably not too a difficult concept to grasp. In practice, it can be difficult to achieve without a certain amount of planning and forethought. This is where design patterns arose; by using an “approved” style of software design, planning an application became easier because the template was already designed. Note, I said application. Many of the accepted design patterns in the Chapter 5 THE LEAST YOU CAN DO VERSUS AN ARCHITECT’S APPROACH industry work extremely well for applications that perform specific tasks, such as productivity apps, utilities, design software, and so on. However, design patterns aren’t always the answer for game development, because games are meant to feel more like “experiences” than rigid, predictable business software. The best solution to develop a game engine may not follow an “accepted” pattern at all, and that’s perfectly okay. However, some basic principles should be followed when using OOP so that your code is modular and scalable. Encapsulation One of the most fundamental OOP concepts is encapsulation. Briefly, encapsulation is the notion that an object (or class, in ActionScript) should be entirely self-managed and contained. An object should not have to know anything about the environment in which it exists to carry out its functions, and it should have a prescribed list of functions (or interface) that other objects can use to tell it what to do. In order to send information to objects outside, it should send messages that can be “listened to” by other objects. You can think of a well-encapsulated object like a soda vending machine. All of the inner workings are hidden away from you, and its functionality is distilled down to the buttons you can press to select a drink and the bin in which you can “listen” to receive your purchase. There is no reason for you to know what is going on inside the machine; it might be a couple of gnomes brewing and canning the soda right there on the spot or it might just be a series of tubes. Either way, all you’re interested in is getting your tasty sugar water through an interface that is easy to understand and use. If you look at any of the built-in classes in Flash, they follow this same pattern. The only information listed about a class in the documentation is its public methods, properties, and events. There is certainly more going on “under the hood” than what we’re exposed to, but we don’t need to know about all of it. Your goal should be the same in developing your classes for games. Inheritance Say we have two classes, Chair and Sofa. Each of these classes share similar traits such as weight, size, number of legs, number of people they can seat, and so on because they both are types of sitting furniture. Instead of defining all of these traits in both classes, we could save ourselves time by creating a class called Furniture and adding the common traits to those. We could then say that Chair and Sofa inherit those properties by being (or extending) Furniture. This is the concept of inheritance; all objects in the real and virtual worlds have a hierarchy. When programming in an object-oriented style, the key to maximizing efficiency is to recognize 83 84 Chapter 5 THE LEAST YOU CAN DO VERSUS AN ARCHITECT’S APPROACH the relationships of one object to another and the features they share. Adding a property to both Chair and Sofa then becomes as simple as adding that property to Furniture. When you extend a class, the new class becomes its subclass and the original is now referred to as the superclass; in the previous example the Furniture is the superclass and the Chair and Sofa are subclasses. There are some practical limitations to pure inheritance (namely that a class can only extend one other class) that we’ll discuss shortly. Polymorphism Although it sounds like an affliction one might develop in a science fiction novel, polymorphism is basically the idea that one class can be substituted in code for another and that certain behaviors or properties of inherited objects can be changed or overridden. ActionScript only allows for a basic type of polymorhpism, so that’s all we’ll cover here. Take the Chair from the previous example on inheritance. Now, let’s say that we extend Chair to make a HighChair for an infant. Certain properties of the chair may not apply or behave differently in the HighChair versus the normal Chair. We can override the features that are different in the HighChair but continue to inherit those that are similar. In practice, this process is not as complicated as it sounds, and I will point it out when it is used. Interfaces A core principle of object-oriented programming is the separation between an interface and an implementation. An interface is simply a list of public methods and properties, including their types. An implementation would be a class that uses that interface to define what methods and properties will be publicly available to other classes. This concept can be initially confusing, so let’s look at an example. Note in this example (and throughout the rest of this book) that interface names in ActionScript start with a capital I by convention. In the section “Inheritance,” we used an example of a Chair and Sofa extending from Furniture. However, if you were to introduce another piece of furniture, a Table for instance, you would now be presented with a problem. While all three of these objects are Furniture, they have very different uses. The Table has no need for methods that involve people sitting down, and the other two have no need for methods that set dishes on them. Theoretically, you could create a whole structure of inheritance, breaking down Furniture into SeatingFurniture, DisplayFurniture, SupportFurniture, etc., but you can see that this is becoming extremely unwieldy. Also, any changes that are made in large inheritance Chapter 5 THE LEAST YOU CAN DO VERSUS AN ARCHITECT’S APPROACH 85 structures can “ripple” down to subclasses and create problems where none existed before. This is where interfaces come in very handy. For these three classes, you can simply define distinct interfaces that support each one’s specific needs. You could break down the interfaces as such: • IFurniture: contains move() method • ISeatedFurniture: contains sitDown() method • ILayingFurniture: contains layDown() method • ITableFurniture: contains setDishes() method Unlike inheritance, where a class can only inherit directly from one other class, you can use, however, many interfaces you like with a single class. The Chair would implement IFurniture and ISeatedFurniture. The Sofa would contain those two, as well as ILayingFurniture, and the Table would contain IFurniture and ITableFurniture. Also, because interfaces can extend one another, the latter three interfaces could all extend the first one as well, making implementation even simpler. Now that you have some basic interfaces defined for different furniture purposes, you can mix and match them as needed to apply to a particular piece of furniture. Don’t worry if some of this abstract terminology gets confusing. When we build a full-scale game in Chapter 14, you’ll be able to see these concepts in practice. Practical OOP in Game Development By default, AS3 supports OOP and good encapsulation through the use of events to send messages between objects. I’ve heard AS3’s event model described as being akin to the Observer design pattern, but regardless of the niche it falls into, it is the native way in which the language operates. Remember that despite the advantages other patterns may offer, all of them are altering the default behavior of the language if they deviate from this model. Figure 5.1 shows the relationship of objects to each other in AS3’s hierarchy. Dispatch bubbling events* Dispatch events Object2 Public interface Public interface Object1 (root level) Dispatch events Eventdispatcher/display list* hierarchy Object3 Figure 5.1 The basic event and communication model for AS3. 86 Chapter 5 THE LEAST YOU CAN DO VERSUS AN ARCHITECT’S APPROACH Dispatch bubbling events* Dispatch events Object2 Dispatch events Public interface Object4 Public interface Figure 5.2 A model similar to Fig. 5.1, but with a new object inserted into the hierarchy. Public interface Object1 (root level) Dispatch events Object3 Eventdispatcher/display list* hierarchy In this illustration, Object1 is at the top of the hierarchy either as the root DisplayObject or just as a generic data EventDispatcher. It has a reference to Object2 and can give it commands directly via its public interface because it knows Object2’s type. However, Object2 has no way of knowing its parent without breaking encapsulation. In fact, Object2 should be able to function regardless of what its parent object is. In order to send information out, it dispatches events. If Object1 adds itself as a listener to Object2, it will receive these events. The same is true between Object2 and Object3. If all of these are DisplayObjects, any events Object3 sets to bubble will eventually reach Object1 if it is listening for them. You can think of these objects as a line of people all facing one direction. The person at the back of the line can see all the other people and address each one directly, even if it has to go through the people directly in front of them. However, everyone has no way of knowing whom, if anyone, is directly behind him or her or if they are even listening. All they can do is say something (dispatch an event); they don’t have to care whether it is heard. By avoiding a reliance on knowing the hierarchy above any particular object, adding new objects to the hierarchy becomes relatively trivial. In Fig. 5.2, we have added Object4 to the second level of the hierarchy. All that needs to change is that Object1 needs to know the correct type of Object4 to properly address its public interface, and Object4 needs to know the same information about Object2. Granted, this is a very abstract and simple example, but a well thought-out structure will allow you to make changes like this without dire consequences to the rest of your application. Because games can vary so widely in their mechanics and behavior and because elements of gameplay tend to change throughout playtesting, having a flexible system is a requirement when building a game engine. The Singleton: A Good Document Pattern Although I don’t subscribe to anyone about the design pattern for game development, I do like to use one particular pattern for the document class of my games. That pattern is known as Chapter 5 THE LEAST YOU CAN DO VERSUS AN ARCHITECT’S APPROACH the Singleton. The name sort of implies the concept behind it. A class that is a Singleton will only ever have one instance of itself in memory and provides a global point of access to that instance. In the context of a document or top-level class in a site, it ensures that there is always an easy way to get back to some basic core functionality. Say, for instance, that all the text for my game is loaded in from an external XML file because it is being localized into other languages. I don’t want to load the XML over and over again whenever I need it, so it makes sense for my document class to be responsible for loading it and then make it available to all the objects down the display list. The Singleton pattern provides a good way of doing this because it essentially creates a global access point from anywhere, even non-DisplayObjects. However, this is a double-edged sword because abuse of this pattern to store too much data or rely too heavily on references back to the main class will break your encapsulation. In practice, you should never put references to a Singleton class inside an engine component you intend to re-use as this will make it too rigid. It should be reserved for classes that are being built for that specific game. Let’s look at an example of a class set up as a Singleton. This file can be found in the Chapter 5 folder under SingletonExample.as. package { import flash.display.MovieClip; public class SingletonExample extends MovieClip { static private var _instance:SingletonExample; public function SingletonExample(se:SingletonEnforcer) { if (!se) throw new Error("The SingletonExample class is a Singleton. Access it via the static getInstance method."); } static public function getInstance():SingletonExample { if (_instance) return _instance; _instance = new SingletonExample(new SingletonEnforcer()); return _instance; } } } internal class SingletonEnforcer {} 87 88 Chapter 5 THE LEAST YOU CAN DO VERSUS AN ARCHITECT’S APPROACH Traditionally in other languages, a Singleton class would have a private constructor function, preventing you from calling it. However, in AS3, all constructors must be public, so we have to put in an error check to enforce proper use. The class keeps a static reference to its only instance, and the static getInstance method returns it. To prevent someone from arbitrarily instantiating the class, we create a secondary private class that is only accessibly to the main document. Think of it like the secret password for the Singleton’s constructor. Only the getInstance method knows how to properly create a new SingletonExample instance as it will fail without this private class. This is a pretty commonly accepted way of dealing with basic Singleton classes in AS3. However, this particular example will also break when used as a document class. This is because Flash will automatically try to instantiate the class to create the display list hierarchy. To get this, we must modify the time of instantiation, alter the way the constructor works, and eliminate the private class. This new version can be found in SingletonExampleDocument.as. package { import flash.display.MovieClip; public class SingletonExampleDocument extends MovieClip { static private var _instance:SingletonExampleDocument; public function SingletonExampleDocument() { if (_instance) throw new Error("This class is a Singleton. Access it via the static SingletonExampleDocument.getInstance method."); _instance = this; addEventListener(Event.REMOVED_FROM_STAGE, onRemove, false, 0, true); } private function onRemove(e:Event):void { _instance = null; } static public function getInstance():SingletonExampleDocument { if (_instance) return _instance; _instance = new SingletonExampleDocument(); return _instance; } } } Chapter 5 THE LEAST YOU CAN DO VERSUS AN ARCHITECT’S APPROACH As you can see in this modified version, we allow instantiation through the constructor once, relying on Flash to do it for us. Once it is created, the constructor will throw an error from here on out. The other addition we made is in case this document is loaded into another SWF. If this game is loaded into a container that has the ability to load and unload it multiple times, it’s best to have the Singleton cleanup by itself once it is removed from the stage. This will prevent persistence of the Singleton in memory. For another example of a Singleton in practice, refer to Chapter 8 on audio. The SoundEngine class we will create there will follow the same pattern. These types of controllers, or “engines,” are good candidates for Singletons because they need to be easily accessible from anywhere in your game. Summary If you are interested in learning more about design patterns to use in your game development, there are links to good articles and other books on this book’s website, www.flashgamebook.com. The bottom line to remember is to always do what makes sense for your situation and don’t go overboard with a solution that isn’t applicable to what you’re doing. Ultimately, if your game is no fun, no one will care that it is a perfectly implemented, flawlessly designed model-view-controller pattern. Learning to program well and effectively is a journey, and given the ever-changing landscape of new languages, technologies, and platforms, no one will ever reach a destination where they can say “I’m done!” Well, someone might, but they’ll be left in the dust pretty quickly by everyone else. 89