This is the second hands-on post of our journey to SpaceRock Miners. We are going to start modeling the game: we should have an initial class model for the Output block by the end of the post.
I must say that I decided to license this under the MIT license. It basically allows you to use it for whatever you want, however you want, given that you follow one simple requirement; it doesn't even require attribution. Read it here.
From this point on, I am assuming you read the previous post and successfully achieved allegro compilation. Also, you must have a basic grasp on C++ as well as understand the basics of Object Orientation. I'd like to emphasize that this tutorial has an intermediate level of difficulty, so it may be overwhelming for some of us. If so, go around the web studying Object Orientation, Modeling and programming.
The Media Manager Class
Our goal here is to create a functional Media Manager that can control our window resolution and state (fullscreen, windowed, border-less windowed [...]) as well as rendering and playing sounds. Based on our initial and simple architecture (found here) we'll need at least two classes and a interface:
- A media_manager class that issue the basic commands, like draw, play sound and update, to the System block; it is part of the Logic block.
- A media_system class that will implement an interface between the media_manager and Allegro itself, in order to decouple our game from Allegro as much as possible; coupling is never good if you can avoid it.
- A allegro_media to manage all the actual drawing, rendering, resizing [...]. Highly coupled with Allegro. Will implement the System portion of all our graphical needs.
Looks easy, doesn't it? Still, these classes will have a lot of stuff in them. Stop and think for a second: what do we need it to do for us?
- Draw Sprites, static and animated, rotated and scaled;
- Draw a parallax scrolling background image;
- Draw the User Interface (Health Bar, Score [...]);
- Play Effect Sounds;
- Play the Background Music;
- Control the Resolution and Type of display we want;
- Update the display for every frame.
The Sprites needs Bitmaps, that are the images themselves which, as do the fonts, need to be loaded from disk, so we need some way to do it.
All of these classes need their own interface for system access, so I guess 8 more classes on the way: 4 interfaces and 4 system block classes. But my guess is probably wrong and it'll take more classes.
An interface is really simple to implement, it is basically a list of functions; while the system classes are where we actually implement these functions. In a sense, an interface is not a complete class, but since we are using C++ it is a class and is declared as one.
What a Mess!
This is getting bigger and bigger, ain't it? I think that if we go straight to our IDE and start to mash our keyboards it'll get even worse. What should we do? We should plan. Plan our Manager classes in advance, so we can think of what they'll need (system interface functions that the manager will use) and how they'll interact with the other classes (their own interface, that the manager will provide).
How do we do that?, you ask me. This is called modeling, and is something really simple to explain but hard enough to make big companies hire specialized professionals to do it for their big
We will use an Open Source UML tool called ArgoUML. I am not going to talk about UML since I am not very experienced with it myself. In addition, my posts are already 3x bigger than I wish they were (megamoths!). Still, there are plenty of resources on UML over the web, feel free to look it up!
Media Manager and the Output block
Based on this initial overlook, these are the classes we are going to need for the Output block:
- mediaManager - the manager itself;
- sprite - represents a simple animated 2D sprite;
- userInterface - a class to construct our user interface in order to render it;
- text - a class to draw texts and manage fonts;
- drawable - represents anything drawable;
- entity - a base class for every entity in our game (player, comets, stones, triggers, anything);
- drawableEntity - a class that derives from both, entity and drawable;
- playable - a class that represents any sounds;
- playableMusic - an specialized playable that represents background music, allowing for different volume and other special features.
In order for these to work, we'll need the system block to actually do everything they need it to, like allocating memory, loading sound/image files, render stuff. The Logic block will actually use these classes to output to the player the current game state. I may need to change, add or remove one or two, as I model the whole game and any problems along the way.
The programming necessary to all these classes alone can be quite big. For this reason, I won't be able to go over every line of the code or function explaining it. Still, I'll try to keep all the code decently commented and formatted, while explaining the functions and classes; it should be enough. The point of this guide is not for you to read and compile the result project on your machine; it is for you to create a project with the same (or higher) complexity and write it yourself! If you want to use the same architecture we are using here, go ahead, but I would recommend you to try and create one for your project or, at least, make sure you understand why we need the classes we are using.
System block, the base for all our media
There's no way to program anything 100% decoupled from the operational system we are working. Even with java and other interpreted technologies we will face some differences between platforms; C++ is no different. Even though the language itself is cross-platform, the environment isn't. As cross-compiling is something I, personally, find hard to use and sometimes frustrating, I simply set up my environment for all the necessary platforms.
"Why are you blabbing about that?", you ask me; well, we are about to enter the part where it gets harder to keep the code 100% cross-platform. For this project, I aim to make a game that is completely platform independent at source level. Even though it may even end up cross-platform, this block will be highly tied to Allegro 5. If we ever need to port this for, say, SDL, we'd need to rewrite a lot of code in here.
All classes of the system block that messes up with media will include some allegro headers. The classes here will be significantly more complex than the ones up there in question of number of lines, complexity and burnt neurons. It was expected, since the sprites will actually be rendered in here, as well as loaded, deleted, [...], you got it. The same with the music, sound effects, UI...
In short, for now, we'll need one system level class for every high level output class that involves media, here's a list for the media related system classes:
- sysSprite - Renders the sprites, animates it, loads from disk, deletes it, does everything sprite-related. Maybe we can even use it to draw our UI;
- sysText - manage text, but at the system level, loading, drawing to an offset...;
- sysPlayable - our system level sounds.
These will be significantly harder to code than the ones up there. The classes here are more complex due to the fact that they actually do the work. The ones up there are mere interfaces for our game, through which it tells the platform what, where and how to render a sprite or what sound and how to play it.
The first classes we will build will be the sprite and sysSprite, but how?
Class Attributes, what goes where?
I assume all of you have made a simple "animated sprite" prototype where you can draw a cute animated sprite somehow; maybe even translated horizontally, speed it up or down, this kind of stuff. So I ask, what attributes does a sprite needs? To start off with the right foot, I'll list the ones I'll be using.
High Level class:
- std::string anim_current; - the animation being drawn for the sprite; (IDLE, FLYING, [...])
- sysSprite* renderer; - See Below;
- int frame_current; - Stores what is the current frame;
- int frame_delay; - Stores the time since the last frame started, so we know when to swap to the next frame;
- float scale; - Stores the scale factor for rendering; (0.1x, 1x, 10x ...)
- int dir; - Defines the direction of the animation; (want an asteroid rotating on ccw? no problem.)
- int rotation; - Stores the actual rotation of the sprite, so we can draw rotated sprites with ease;
- bool translate_horizontally; - Rotates the sprite horizontally if true, not really useful for our game.
- std::multimap<std::string, std::pair<int, ALLEGRO_BITMAP*> > anim_list;
The high level sprite class is supposed to be created 1-1 for every entity using them. For an example every asteroid on our little game will have its own instance of the sprite class, with its own "current frame", its own rotation, et cetera. This way, we avoid the problem where every asteroid would be moving in a synchronous manner with every other one like a lame choreography. This is why we need all of them to have its own state.
|Scaled, Rotated, doesn't matter, similar sprite objects share only one sysSprite instance.|
The system level sysSprite class is supposed to have only one instance for every different sprite. Every similar sprite would need an instance of a sprite class object but all of these sprite objects would be linked to a single sysSprite object. This saves us from wasting memory and cpu by loading the same sprite image several times.
We should use a similar approach to the sound system.
Now that you (hopefully) understood the relation between the sysSprite and sprite classes, it gets easier to understand that sysSprite std::multimap. It is nothing more than a matrix, a matrix that stores a string on the first column (animation names like IDLE, FLYING, [...]) a number on the second (the duration of a given frame) and an allegro5 bitmap (the image itself) on the third.
This way, whenever we want to play the idle animation for our spaceship, we'll be able to simply go through the frames of the spaceship's sprite where the animation is called IDLE. Of course, string isn't necessarily our best approach, but I opted for it over simple numbers so they get a little clearer for us to understand.
But still, do you see the problem arising here? We'll need to have a structured way to store animated sprites; we'll probably need a similar solution for the sounds in the game. I don't know about you, but I smell XML.
Next step: create the sprite and sysSprite classes and test them. What'll we need? Aside from a lot of patience, we'll need an Allegro 5 project properly linked and tinyxml. If you don't have tinyxml, no problem, get it here. A little playing around tinyxml would do good for us as I don't plan to spend too much time on it, explaining it and how to use; but, if you prefer, you can ignore it and program a custom file parser using fopen: your call. I recommend tiny-xml.
I hope to finish the sprite class on our next entry,
Over and Out.