The main goal of me doing this was to learn DirectX and learn game programming. I chose to clone Adventure because:
- I only want to tackle 2D to start off.
- I wanted to clone an existing game so I didn't get wrapped up in game
design and lose sight of learning DirectX and game programming
- It was pretty easy to figure out how Adventure worked, but it wasn't a
totally limited game.
- Adventure is the coolest Atari game!
So, I basically read a couple of books ("Learn Computer Game Programming..." by
Ian Parberry, and "Tricks of the Windows Game Programming Gurus" by Andre
Lamothe) and got going. I wanted to also do a really good analysis and design
and try to "do it right", so I could understand the issues facing a game
programmer when trying to have a nice architecture, but also a functioning
game. As such, I decided on a few rules for myself:
- Everything except for WinMain would be done in C++
- No templates, operator overloading or
multiple implementation inheritance
- No purely global variables, and only constant values exposed as public
static members of a class
So, with these rules in mind, started on my quest.
Before jumping straight into Adventure, I decided to re-implement the code in
the Parberry book. I found his code hard to understand and poorly done, plus
it seemed like an easy way to get my head around DirectDraw, DirectSound and
DirectInput. After that, I began my Analysis and Design of Adventure.
The first thing I did was outline an analysis-level
class hierarchy. I figured
that all objects in adventure should inherit from a base object, called
GameObject
. This object could encapulate stuff like collisions,
movement and position. It could also encapsulate drawing itself to the screen
(which was accomplished vi a ScreenImage
class, contained within
a GameObject
, that draws a bitmap to a DirectDraw surface).
Then, each object would extend the GameObject to provide it's special
features. All GameObjects have an update
method, where they can
do some processing each time through the loop. Thus, a dragon or bat could
implement their AI inside this method.
Once this was done, I outlined a basic system for moving the player from room
to room (in terms of a class diagram
and a sequence diagram).
Since you can only see one room at a time, and there are many ways
to leave a room (e.g. edges of screen, gate), I had my Room
object
contain a connector object that could determine if an object was
trying to leave the room. It could also handle the details of moving the object
to the new room.
At this point, I threw together a basic
game loop diagram for how I thought everything would fit together. Note
that these are analysis level documents that helped to drive my design and
implementation. I had to diverge from them in several places and did not
go back and update the documents.
Now, I was ready to write some code. I skipped doing a formal design since
the act of coding would reveal much more about how I should architect the game
than more thinking, plus I wasn't working with anyone else, so I could more
easily make design decisions in my head at the keyboard. Before starting,
I created an implementation plan
that listed each step I would take. The idea
was that at each sitting, I would code up at least one step, and that after
each step, I would have a functioning system. Also, if I needed to show my
work to anyone, I made sure I'd always have something to show them.
After that, I went down my list. If I found out something else needed to be
done, or that a particular step was too much to do in one sitting, I modified
my list. I also kept a buglist as a went, and a TODO list of things I wanted
to do before being "done", but that were not essential to the game.
What did I learn?
The most challenging implementation was actually the collision detection.
The problem I kept having was that the player would hit a wall, and the
detection algorithm would bounce him back in the wrong direction. The problem
arose because it was not straightforward to tell where the player was in
relation to a wall so that he could be bounced properly. I finally settled on
a brute force detection approach that turned out to not cause any performance
issues, so I kept it. I basically created a rectangle that contained the two
objects to check, and then went through each point of that rectangle looking
for a non-transparent pixel that was in both objects. Then, I calculated
various differences between the player and a wall (in that case) and bounced
him back appropriately. It still isn't perfect, but it's good enough for
my purposed.
The main things I learned that I wasn't expecting to learn (i.e. stuff other
than DirectX and some general game programming ideas) were:
- Load times are significant! - Even in lowly adventure, there
is a noticable blip when starting a large level (such as classic game
3), and on a slow machine, the program appears to hang for a second. Not
the mark of a professional game, but by the time I realized it was a
problem, it would've been nontrivial to modify the architecture to handle
giving some feedback during loading. It did give me a chance to use the
profiler and do some optimizations, however.
- A game has various states other than "running" - I had completely
neglected to plan for things like the game being won or lost, selecting
a new game, pausing, etc. I ended up having to do some significant
restructuring of my main controlling class to accomodate this.
- Make things event-based - Don't know if this is always the
gospel, but I found that creating a basic event firing/listening
infrastructure was incredibly helpful. I originally had sound-playing
code inside my game objects. I also had tons of duplicated code where
an object sets a timer for itself and checks to see if it's elapsed.
I broke this stuff out and created an EventDispatcher and had objects
implement an EventListener interface. This way, the player could, for
example, announce that he had picked up an object. A sound playing
class, then, could hear that announcement and play a sound. This made
the code a lot cleaner and helped toward loose coupling of objects
in the system.
- Memory Management is a non-trivial task - I'd been doing
Java at work most of the time, and you basically don't have to worry
about deallocating an object, or who official "owns" a reference.
The garbage collector takes care of it. In C++, however, I had to
carefully decide when and where objects would be allocated and deallocated.
I finally decided to have most objects owned by the World classes (which
implement particular games, e.g. game #1), and the world class kept
arrays of allocated objects. GameObjects and Rooms didn't have to worry
about allocating or deallocating objects like ScreenImages or other
GameObjects. Everything was deleted from an array at the end of the game.
I'm not sure if this is the best approach, but it worked and I don't think
I have any memory leaks.
So, all in all, creating a clone of Adventure was a fun experience. I'm
including the code in the download, and it should be pretty easy to extend
my clone to create new "Adventures" or new objects or a totally different game.
I even created three new objects just for fun, and a fourth world for the player
to explore and find the ever-lost chalice.