Introduction
After reading the article of Michael Birken, one has to admit that it is again proven that there is only one ingredient that makes a good game: the gameplay. Not the looks, not the sound. As a huge fan of the Introversion Software games, which all have a special 'old' look, I thought why not add this simple retro look to this game.
Like James Curran stated, Michael Birken's article covers the game from top to bottom, there isn't much light I can shed over this. So, I will focus more on the differences of building a 2D game (which isn't a console) that looks like a console game.
Background
The first question I needed to solve was, which technique I would use for the game. DirectX or OpenGL? The second was even more important: Since this is my first project on graphics, would I be able to master the chosen technique and write all the needed code for this on time?
After looking for a while on the web, I stumbled upon the following site: IrrLicht. This is a free Graphics Engine that supports both DirectX and OpenGL, which also solved my first question. This engine provides both 2D and 3D modes, which is perfect. I could start in 2D, and move on to 3D without the need to change the engine.
One of the largest differences when not using a console is the way we process inputs. A console program is an input-driven program. After analysing the input, we perform the required logic and then redraw the screen. A Windows (non console) program is an event-driven program. This means that I would need to analyse the events and create my own input to drive the game logic. And, because it's a window, I would need to draw the screen myself and keep on updating the screen while waiting for input.
Before Using the Code
If you want to use and compile this code, you also need the IrrLicht SDK, this can be downloaded here. It contains everything you need to start, including an already built DLL and lib.
After placing the SDK on your disk, you need to add the location of the include files and the library files to Visual Studio. Open the Options menu under the Tools menu. Select the option Project and Solution and the sub option VC++ directories.
Using the Code
You are now ready to use and compile this code. I have started from a console project template; this gives the advantage that the console will be used as the output and trace window, which makes it easier to debug the engine. In the main, I create a Game
object. This object contains the functionality to setup, run, and end the game.
int _tmain(int argc, _TCHAR* argv[])
{
Game* pTheGame = Game::getInstance();
if(pTheGame)
{
pTheGame->setupGame();
pTheGame->createData();
pTheGame->runGame();
pTheGame->endGame();
}
return 0;
}
The setupGame()
method creates the game window and the game acts. The game acts will take care of the game logic. I have put the game logic in an Act
object. This Act
object is handed over to a Director
object. And, this Director
object will allow you to switch between acts. There are three acts in this game: IntroAct
, PlayAct
, and CreditsAct
. The Intro will draw a Star Trek logo and will show the goal of the game. The Play will contain the actual game, while the Credits will be called when the game is over, to show tribute to our victorious captain or to weep over the destruction of the greatest starship ever. The last object we need is an InputManager
. This object will convert the events returned by the IrrLicht engine to inputs we like to be notified about. In our case, these will be KeyPress
events.
In order to send the KeyPress
events to the acts, I could have used a simple callback function, but I like the delegate concept of C#. In C++, this can be done by a Functor. The boost library provides several classes to do this, but I didn't want to use boost (at least not for this article). So, I created my own hardcoded Functor for this job.
struct FKeyPressed
{
virtual ~FKeyPressed() {};
virtual bool operator()(EKEY_CODE) = 0;
};
template class KeyPressed : public FKeyPressed
{
public:
typedef bool (ACTOR::*FunctionType)(EKEY_CODE);
public:
KeyPressed(ACTOR* pActor, FunctionType pFunctor)
{
m_pActor = pActor;
m_pFunctor = pFunctor;
}
virtual ~KeyPressed() {};
virtual bool operator()(EKEY_CODE keyCode)
{
return (m_pActor->*m_pFunctor)(keyCode);
}
protected:
ACTOR* m_pActor;
FunctionType m_pFunctor;
};
The Act
object that would like to receive KeyPress
events just needs to provide a function of the following signature:
bool OnKeyPressed(EKEY_CODE keyCode);
In the function runGame
, the screens are rendered. For us, this means the draw
function of the active Act
will be called. Either we draw everything here, which I will do for both the IntroAct
and the CreditsAct
, because it isn't much, or we load some IDrawable
objects into the Act
.
I have used two types of IDrawable
, those that remain rather static, and those that are more dynamic. An example of a static drawable is the ShipDisplay
. This class shows the status of the ship and the game on the right side of the screen. This information always needs to be drawn. An example of a dynamic drawable is the Torpedo
, which will be drawn for some period of time and will be removed from the screen afterwards. This type, that I have called an Animator
, implements the IAnimator
interface, which expands the IDrawable
interface. An example of this is the Phaser
class. The Animator
provides all the code to draw it and to control the lifecycle. So, the Phaser
only needs to add what is different.
void Phaser::updateInfo(Info& rInfo)
{
if(rInfo.Alpha >= 0)
{
rInfo.Alpha += rInfo.Fade;
}
if(rInfo.Alpha >= 255)
{
rInfo.Alpha = 255;
rInfo.Fade = - rInfo.Fade;
}
}
When the lifecycle of the Phaser
ends, the endAnimator
is called. In the case of the Phaser
, the targeted vessel is hit.
void Phaser::endAnimator()
{
if (m_pVessel)
{
m_pVessel->hitPhaser(m_iEnergy);
}
}
One of the most difficult parts was to implement the console interface. The Console
class handles the looks and the input. Whenever the Enter or Escape key is pressed, the CommandManager
is triggered. This manager stores the state where the input of the game is in.
enum Mode
{
WaitForCommand,
NavigateWaitForCourse,
NavigateWaitForDistance,
LaunchWaitForEnergy,
LaunchWaitForHit,
LaunchWaitForCourse,
LaunchWaitForCoordinates,
TransferWaitForEnergy,
ComputerWaitForCommand,
WaitForAnimation
};
The CommandManager
validates the input and takes the appropriate action, e.g., let the Enterprise
object transfer energy to the shields. The third class is the Controller
, this object actually creates the Torpedo
or Phaser
animators and adds them to the PlayAct
. The function runEnemyAI
checks if there is a need to run and whether there are enemy vessels in the sector or not. There are three vessels: Enterprise
, KlingonShip
, and StarBase
, which all implement the IVessel
interface.
Points of Interest
First, I would like to apologize for using a great library like IrrLicht to just draw a simple console look. But, it is fairly easy to change the characters <E> to a 2D image of the Enterprise. Or, you could go even further and make it full 3D. It should also be easy to change the Controller
and CommandManager
classes to make it real-time instead of turn-based.
References
History
- 27th August, 2008 - First release of this article