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