Game Programming Patterns
文章目录
What is Software Architecture? It’s less about writing code than it is about organizing it. Good architecture makes a huge difference in productivity. But, like all things in life, it doesn’t come free. Good architecture takes real effort and discipline. Every time you make a change or implement a feature, you have to work hard to integrate it gracefully into the rest of the program. You have to think about which parts of the program should be decoupled and introduce abstractions at those points. If there is any method that eases these constraints, it’s simplicity.
Design Patterns Revisited
Command
A command is a reified method call. “Reify”, in case you’ve never heard it, means “make real”. Both terms mean taking some concept and turning it into a piece of data — an object — that you can stick in a variable, pass to a function, etc. So by saying the Command pattern is a “reified method call”, what I mean is that it’s a method call wrapped in an object.
That sounds a lot like a “callback”, “first-class function”, “function pointer”, “closure”, or “partially applied function” depending on which language you’re coming from, and indeed those are all in the same ballpark. The Gang of Four later says: Commands are an object-oriented replacement for callbacks.
Configuring Input
|
|
This function typically gets called once per frame by the game loop. This code works if we’re willing to hardwire user inputs to game actions, but many games let the user configure how their buttons are mapped. To support that, we need to turn those direct calls to jump() and fireGun() into something that we can swap out. We define a base class that represents a triggerable game command:
|
|
Then we create subclasses for each of the different game actions:
|
|
In our input handler, we store a pointer to a command for each button:
|
|
Now the input handling just delegates to those:
|
|
Where each input used to directly call a function, now there’s a layer of indirection. This is the Command pattern in a nutshell.
The only thing the JumpCommand can make jump is the player. Instead of calling functions that find the commanded object themselves, we’ll pass in the object that we want to order around:
|
|
Here, GameActor is our “game object” class that represents a character in the game world. We pass it in to execute() so that the derived command can invoke methods on an actor of our choice, like so:
|
|
We’re just missing a piece between the input handler and the command that takes the command and invokes it on the right object. First, we change handleInput() so that it returns commands:
|
|
It can’t execute the command immediately since it doesn’t know what actor to pass in. Here’s where we take advantage of the fact that the command is a reified call — we can delay when the call is executed. Then, we need some code that takes that command and runs it on the actor representing the player. Something like:
|
|
Adding a layer of indirection between the command and the actor that performs it has given us a neat little ability: we can let the player control any actor in the game now by changing the actor we execute the commands on.
Undo and Redo
We’re conveniently already using commands to abstract input handling, so every move the player makes is already encapsulated in them. For example, moving a unit may look like:
|
|
Our earlier input handler held on to a single command object and called its execute() method anytime the right button was pressed. Here, the commands are more specific. They represent a thing that can be done at a specific point in time. This means that the input handling code will be creating an instance of this every time the player chooses a move. Something like:
|
|
To make commands undoable, we define another operation each command class needs to implement:
|
|
An undo() method reverses the game state changed by the corresponding execute() method. Here’s our previous move command with undo support:
|
|
When a unit moves, it forgets where it used to be. If we want to be able to undo that move, we have to remember the unit’s previous position ourselves. Supporting multiple levels of undo isn’t much harder. Instead of remembering the last command, we keep a list of commands and a reference to the “current” one. When the player executes a command, we append it to the list and point “current” at it.
When the player chooses “Undo”, we undo the current command and move the current pointer back. When they choose “Redo”, we advance the pointer and then execute that command. If they choose a new command after undoing some, everything in the list after the current command is discarded.
In some ways, the Command pattern is a way of emulating closures in languages that don’t have them. For example, if we were building a game in JavaScript, we could create a move unit command just like this:
|
|
We could add support for undo as well using a pair of closures:
|
|
Flyweight
Forest for the Trees
We’re talking thousands of trees, each with detailed geometry containing thousands of polygons. Each tree has a bunch of bits associated with it:
- A mesh of polygons that define the shape of the trunk, branches, and greenery.
- Textures for the bark and leaves.
- Its location and orientation in the forest.
- Tuning parameters like size and tint so that each tree looks different.
If you were to sketch it out in code, you’d have something like this:
|
|
The key observation is that even though there may be thousands of trees in the forest, they mostly look similar. They will likely all use the same mesh and textures. That means most of the fields in these objects are the same between all of those instances. We can model that explicitly by splitting the object in half. First, we pull out the data that all trees have in common and move it into a separate class:
|
|
The game only needs a single one of these, since there’s no reason to have the same meshes and textures in memory a thousand times. Then, each instance of a tree in the world has a reference to that shared TreeModel. What remains in Tree is the state that is instance-specific:
|
|
We provide two streams of data. The first is the blob of common data that will be rendered multiple times — the mesh and textures. The second is the list of instances and their parameters that will be used to vary that first chunk of data each time it’s drawn. With a single draw call, an entire forest grows.
The Flyweight Pattern separate out an object’s data into two kinds. The first kind of data is the stuff that’s not specific to a single instance of that object and can be shared across all of them. The Gang of Four calls this the intrinsic state. The rest of the data is the extrinsic state, the stuff that is unique to that instance.
A Place To Put Down Roots
We’ll make the ground tile-based: the surface of the world is a huge grid of tiny tiles. Each tile is covered in one kind of terrain. Each terrain type has a number of properties that affect gameplay:
- A movement cost that determines how quickly players can move through it.
- A flag for whether it’s a watery terrain that can be crossed by boats.
- A texture used to render it.
A common approach is to use an enum for terrain types:
|
|
Then the world maintains a huge grid of those:
|
|
To actually get the useful data about a tile, we do something like:
|
|
I think of movement cost and wetness as data about a terrain, but here that’s embedded in code. Worse, the data for a single terrain type is smeared across a bunch of methods. It would be really nice to keep all of that encapsulated together. After all, that’s what objects are designed for. It would be great if we could have an actual terrain class, like:
文章作者 huijian142857
上次更新 2016-10-16