Introduction
This article is about the 2D isometric game engine written in C++. It deals with most of the basic things you need to take into account when starting to write it from the scratch, like: images, sprites, terrain generation, collisions, AI, game scripting, etc.
Background
You can find the background of this article in many other articles all over the Internet, considering this subject. The main idea was to present one of the possible solutions, having in mind all the technology advancements that are available today, in different languages, libraries or hardware resources. So, many things in this project were written from scratch, in order to show work that must be undertaken for this type of funny but very hard part of software development. After all, it is all in the game.
The Game Storyboard
It comes at the beginning of our journey. We all liked to play the good old 2D isometric strategy games like the "Age of Empires" (my favourite), "Dune" (another of my favourite), "Starcraft" (again the favourite), and the list will keep growing as I go deeper in my childhood. So, one day, the question came by itself - how to do it? It is not how to make it better, or to sell it to the million of copies, but just a simple question "how to do it?".
Well, the concept, or some kind of the story is needed, of course. It starts just there. If I wanted to create a live world in 2D, to look at it at 45 degrees, to manipulate with the characters (or a game units, later) than I needed to imagine what that world would look like. My wish was for it to be an isometric old world, with farm houses, grass terrain (later with forests, lakes, hills, animals, etc.) and some inhabitants (later warriors). So, the first thing was to find the graphics for my game, if I could not create it by myself.
The simple image of the villager was just not enough. It should move and react. So, I needed the special sequence of the similar images called the "sprite
", please see below:
The Game Sprites
Every sprite has a finite number of single images of the same size. The program routine will extract and draw the correct sequence, as it will be later defined. So, now I had the simple but effective animation. This is a very old technology, now in the 3D world, but it was more than I wanted in that time.
For the bitmap manipulation, I have "borrowed" my own image library available here on CodeProject known as the CBitmapEx
class. The whole article is available here. This class allowed me to read the ".BMP" image files and manipulate them, as also to perform the quick screen drawing.
So, the one of the first classes that I had to write for this project was the CGameSprite
class. It will deal with the mentioned CBitmapEx
class in order to load the game sprite image sequence, extract the correct image and render it to the screen. Below is the header file of this class:
#pragma once
#include "BitmapEx.h"
class CGameSprite
{
public:
CGameSprite(void);
virtual ~CGameSprite(void);
public:
int GameSprite_Create(LPTSTR lpszSpriteFile, int iTileWidth,
int iTileHeight, _PIXEL transparentColor, _PIXEL shadowColor);
BOOL GameSprite_IsValid() {return m_GameSpriteBitmap.IsValid();}
void GameSprite_Draw(CBitmapEx* pScreenBitmap, int x, int y, int iCurrentFrame);
void GameSprite_Draw(CBitmapEx* pScreenBitmap, int x, int y, int iCurrentFrame, int iAlpha);
int GameSprite_GetWidth() {return m_iTileWidth;}
int GameSprite_GetHeight() {return m_iTileHeight;}
int GameSprite_GetTotalRows() {return m_iSpriteRows;}
int GameSprite_GetTotalCols() {return m_iSpriteCols;}
int GameSprite_GetTotalCells() {return (m_iSpriteRows*m_iSpriteCols);}
private:
void GameSprite_Draw(int dstX, int dstY, int width, int height,
CBitmapEx* pScreenBitmap, int srcX, int srcY, _PIXEL transparentColor, _PIXEL shadowColor);
private:
int m_iTileWidth;
int m_iTileHeight;
int m_iSpriteRows;
int m_iSpriteCols;
_PIXEL m_TransparentColor;
_PIXEL m_ShadowColor;
CBitmapEx m_GameSpriteBitmap;
};
So, this class will allow you to create the game objects, based on the different sprite images you provide. Each sprite image has a finite number of single images, called "tiles
". Each tile has the same width and height. Also, this sprite would need to support the transparent areas (which will not be rendered on the screen), and also the shadows (if available) which will be rendered semi-transparent. And, from the point of view of the base game object, this is enough. The game objects now can move and react, according to the different sequence of the images inside the sprite tile's collection.
The main method of the CGameSprite
class is the GameSprite_Create method
which will actually create the sprite based on the image file, and tile params.
Another important method is the GameSprite_Draw method
(in two versions) which will render the sprite on the screen.
Other methods are information methods considering the sprite image tileset.
Now, I had my game objects, but they needed some additional information about themselves.
The Game Units
The class called CGameUnit
was the answer to this challenge. It also uses the previously defined class for the game sprites. In the header file of this class, you will find many different properties (defined in the enums
or structs
). These properties define the game unit characteristics, like the type (unit, building, resource, or something else), movement direction and the corresponding animation, current position, movement, speed, health, scripts, etc. So, here should globally be defined everything that has to do with the game units. Besides the properties, here, you should define the game actions that the game units can perform, like creation, positioning, moving, interaction with the user by selecting, scripting, etc. The header file of this class is shown below:
#pragma once
#include "GameSprite.h"
#define MAX_WAYPOINTS 128
#define MESSAGE_FONT_FACE _T("Courier New")
#define MESSAGE_FONT_SIZE 10
typedef enum _GAMEUNITINFOTYPE
{
GIT_NAME = 0x0001,
GIT_POSITION = 0x0002,
GIT_DESTINATION = 0x0004,
GIT_SPEED = 0x0008,
GIT_HEALTH = 0x0010,
GIT_RANGE = 0x0020,
GIT_VISIBILITY = 0x0040,
GIT_SELECTED = 0x0080,
GIT_ANIMTIME = 0x0100,
GIT_ANIMPARAMS = 0x0200
} GAMEUNITINFOTYPE;
typedef enum _GAMEUNITANIMATION
{
GUA_NONE = 0,
GUA_ONCE,
GUA_ONCEBLEND,
GUA_REPEAT,
} GAMEUNITANIMATION;
typedef enum _GAMEUNITTYPE
{
GUT_NONE = 0,
GUT_UNIT,
GUT_BUILDING,
GUT_RESOURCE,
GUT_POINTER
} GAMEUNITTYPE;
typedef enum _GAMEUNITDIRECTION
{
GUD_LEFT = 0,
GUD_RIGHT,
GUD_UP,
GUD_DOWN,
GUD_LEFTUP,
GUD_LEFTDOWN,
GUD_RIGHTUP,
GUD_RIGHTDOWN
} GAMEUNITDIRECTION;
typedef enum _GAMEUNITCOLLISION
{
GUC_MOVE = 0,
GUC_STOP
} GAMEUNITCOLLISION;
typedef struct _GAMEUNITINFO
{
CHAR lpszName[255];
int positionX;
int positionY;
int destinationX;
int destinationY;
int speedX;
int speedY;
int iHealth;
int iRange;
BOOL bVisible;
BOOL bSelected;
DWORD dwAnimationTime;
int iStartFrame;
int iEndFrame;
int iDefaultFrame;
WORD wFlags;
} GAMEUNITINFO, *LPGAMEUNITINFO;
typedef struct _GAMEUNITACTION
{
CHAR lpszActionName[255];
int iStartFrame;
int iEndFrame;
int iDefaultFrame;
} GAMEUNITACTION, *LPGAMEUNITACTION;
typedef struct _GAMEUNITWAYPOINT
{
POINT ptDestination;
int iCost;
BOOL bValid;
} GAMEUNITWAYPOINT, *LPGAMEUNITWAYPOINT;
class CGameUnit
{
public:
CGameUnit(void);
virtual ~CGameUnit(void);
public:
BOOL GameUnit_Create(CGameSprite* pGameSprite,
int iCurrentFrame, GAMEUNITTYPE gameUnitType, RECT rcCollisionBox);
BOOL GameUnit_IsValid() {return ((m_pGameSprite != NULL) &&
(m_pGameSprite->GameSprite_IsValid()));}
void GameUnit_Draw(CBitmapEx* pScreenBitmap);
void GameUnit_Update();
void GameUnit_ProcessMouseEvent();
void GameUnit_SetInfo(GAMEUNITINFO gameUnitInfo);
LPSTR GameUnit_GetName() {return m_lpszName;}
void GameUnit_SetPosition(POINT ptPosition) {m_ptPosition = ptPosition;}
POINT GameUnit_GetPosition() {return m_ptPosition;}
void GameUnit_SetDestination(POINT ptDestination) {m_ptDestination = ptDestination;}
POINT GameUnit_GetDestination();
void GameUnit_SetSpeed(POINT ptSpeed) {m_ptSpeed.x=abs(ptSpeed.x); m_ptSpeed.y=abs(ptSpeed.y);}
POINT GameUnit_GetSpeed() {return m_ptSpeed;}
void GameUnit_SetHealth(int iHealth) {m_iHealth = max(0, min(100,iHealth));}
int GameUnit_GetHealth() {return m_iHealth;}
void GameUnit_SetVisible(BOOL bVisible) {m_bVisible = bVisible;}
BOOL GameUnit_IsVisible() {return m_bVisible;}
void GameUnit_SetSelected(BOOL bSelected) {m_bSelected = bSelected;}
BOOL GameUnit_IsSelected() {return m_bSelected;}
BOOL GameUnit_IsMoving() {return m_bMoving;}
void GameUnit_SetPaused(BOOL bPaused) {m_bPaused = bPaused;}
BOOL GameUnit_IsPaused() {return m_bPaused;}
void GameUnit_SetAnimationTime(DWORD dwAnimationTime) {m_dwAnimationTime=abs(dwAnimationTime);}
DWORD GameUnit_GetAnimationTime() {return m_dwAnimationTime;}
RECT GameUnit_GetBounds();
RECT GameUnit_GetCollisionBox() {return m_rcCollisionBox;}
void GameUnit_AddAction(LPSTR lpszActionName, int iStartFrame, int iEndFrame, int iDefaultFrame);
void GameUnit_ExecuteAction(LPSTR lpszActionName, GAMEUNITANIMATION gameUnitAnimationType);
void GameUnit_ExecuteCurrentAction(GAMEUNITANIMATION gameUnitAnimationType);
int GameUnit_FindAction(LPSTR lpszActionName);
void GameUnit_Select(POINT ptSelection);
void GameUnit_Select(RECT rcSelection);
BOOL GameUnit_IsVisibleOnScreen();
void GameUnit_Move(POINT ptDestination);
void GameUnit_Move();
void GameUnit_UndoMove();
void GameUnit_RedoMove();
void GameUnit_Stop();
BOOL GameUnit_IsUnit() {return (m_iGameUnitType == GUT_UNIT);}
BOOL GameUnit_IsBuilding() {return (m_iGameUnitType == GUT_BUILDING);}
BOOL GameUnit_IsResource() {return (m_iGameUnitType == GUT_RESOURCE);}
GAMEUNITDIRECTION GameUnit_GetDirection() {return m_iGameUnitDirection;}
int GameUnit_GetWidth() {return m_pGameSprite->GameSprite_GetWidth();}
int GameUnit_GetHeight() {return m_pGameSprite->GameSprite_GetHeight();}
void GameUnit_SetRange(int iRange) {m_iRange = max(1, min(10, iRange));}
int GameUnit_GetRange() {return m_iRange;}
void GameUnit_SetWaypoint(LPPOINT lpWaypoint, int iNumberWaypoints);
void GameUnit_GetWaypoint(LPPOINT lpWaypoint, int& iNumberWaypoints);
BOOL GameUnit_IsWaypointMode() {return m_bWaypointMode;}
void GameUnit_ClearWaypoint();
GAMEUNITCOLLISION GameUnit_ProcessCollision(CGameUnit* pGameUnit);
void GameUnit_SetScripted(BOOL bScripted) {m_bScripted= bScripted;}
BOOL GameUnit_IsScripted() {return m_bScripted;}
void GameUnit_SetMessage(LPSTR lpszMessage);
LPSTR GameUnit_GetMessage() {return m_szMessage;}
void GameUnit_ShowMessage(BOOL bShowMessage) {m_bShowMessage = bShowMessage;}
BOOL GameUnit_IsMessageVisible() {return m_bShowMessage;}
private:
void GameUnit_UpdatePosition();
void GameUnit_UpdateAnimation();
private:
CGameSprite* m_pGameSprite;
CHAR m_lpszName[255];
POINT m_ptPosition;
POINT m_ptSpeed;
POINT m_ptCurrentSpeed;
int m_iHealth;
int m_iRange;
POINT m_ptDestination;
BOOL m_bVisible;
BOOL m_bSelected;
BOOL m_bMoving;
BOOL m_bPaused;
BOOL m_bWaypointMode;
BOOL m_bScripted;
RECT m_rcCollisionBox;
LPGAMEUNITACTION* m_lpGameUnitActions;
int m_iTotalActions;
int m_iCurrentAction;
DWORD m_dwCurrentTime;
DWORD m_dwAnimationTime;
int m_iStartFrame;
int m_iEndFrame;
int m_iDefaultFrame;
int m_iCurrentFrame;
GAMEUNITANIMATION m_iGameUnitAnimationType;
GAMEUNITTYPE m_iGameUnitType;
GAMEUNITDIRECTION m_iGameUnitDirection;
LPPOINT m_lpWaypoint;
int m_iNumberWaypoints;
int m_iCurrentWaypoint;
CHAR m_szMessage[4096];
_SIZE m_MessageBounds;
BOOL m_bShowMessage;
int m_iAlphaLevel;
};
OK, this class is more serious than the previous one. The CGameUnit
class uses the game sprite class for the visual representation, but adds the features and logic. First of all, the game unit is created upon the existing sprite. You cannot have the game unit if it cannot have its "body".
The main point here is that you can have as many of the game units as you are supposed to in your storyboard from the beginning of our project. The same type of the game unit will share the same game sprite. So they will share the visual representation, but will react differently with other game units or with the surrounding.
The main method of the CGameUnit
class in the GameUnit_Create
method. You call it when you would like to create your game unit, or plenty of them, using the same game sprite.
The method that will render the game unit on the screen, based on its current position, is the GameUnit_Draw
method.
The method that will order the game unit to move is the GameUnit_Move
method. Now, the game unit will start to change its current position on the screen and show its moving animation sequence.
There are also many information methods available for this class.
There are also some very specific methods considering the AI and the scripting, but we will cover this later.
OK, now I needed a game world.
The Game Map
The CGameMap
class offers the possibility to create the terrain (or the world) for our game units. Please check the header file below:
#pragma once
#include "BitmapEx.h"
#define MIN_ROWS 32
#define MAX_ROWS 1024
#define MIN_COLS 32
#define MAX_COLS 1024
typedef enum _GAMEMAPMODE
{
GMM_ORTHOGONAL = 0,
GMM_ISOMETRIC
} GAMEMAPMODE;
typedef enum _GAMEMAPTILETYPE
{
GMT_GRASS = 0,
GMT_DIRT,
GMT_SNOW,
GMT_WATER,
GMT_SAND,
GMT_MUD,
GMT_ROCK,
GMT_LAND,
GMT_ROAD
} GAMEMAPTILETYPE;
typedef struct _GAMEMAPTILEINFO
{
GAMEMAPTILETYPE iType;
int iPassable;
int iHidden;
int iOccupied;
int iRow;
int iCol;
} GAMEMAPTILEINFO, *LPGAMEMAPTILEINFO;
class CGameMap
{
public:
CGameMap(void);
virtual ~CGameMap(void);
public:
int GameMap_Create(LPTSTR lpszMapFile, int iTileWidth, int iTileHeight,
int iScreenWidth, int iScreenHeight, int iRows, int iCols);
void GameMap_Destroy();
int GameMap_SetInfo(LPGAMEMAPTILEINFO* lpMapInfo);
LPGAMEMAPTILEINFO* GameMap_GetInfo() {return m_lpGameMap;}
BOOL GameMap_IsValid() {return (m_lpGameMap != NULL);}
void GameMap_Draw(CBitmapEx* pScreenBitmap);
void GameMap_Update();
void GameMap_UpdateMapTile(int x, int y, int range);
int GameMap_GetScreenOffsetX() {return m_iScreenOffsetX;}
int GameMap_GetScreenOffsetY() {return m_iScreenOffsetY;}
LPGAMEMAPTILEINFO GameMap_GetTile(POINT ptDestination);
private:
void GameMap_ScrollLeft();
void GameMap_ScrollRight();
void GameMap_ScrollUp();
void GameMap_ScrollDown();
private:
LPGAMEMAPTILEINFO* m_lpGameMap;
int m_iRows;
int m_iCols;
int m_iMapWidth;
int m_iMapHeight;
int m_iTileWidth;
int m_iTileHeight;
int m_iScreenWidth;
int m_iScreenHeight;
int m_iScreenRows;
int m_iScreenCols;
int m_iScreenOffsetX;
int m_iScreenOffsetY;
CBitmapEx m_GameMapBitmap;
CBitmapEx m_GameMapBitmapUnoccupied;
CBitmapEx m_GameBitmap;
GAMEMAPMODE m_iGameMapMode;
};
The game terrain is an image like the one shown below:
The CGameMap
class is a very similar class to the game sprite class. But here, you can have only one game world, and you can create more than one game sprite. Like the game sprite class, this class will read the terrain image file, made of different terrain tiles. As you can see, the terrain is supposed to be made of grass, send, water and snow, but it is all already defined in the storyboard at the beginning of our project (hopefully). Besides the simple rendering of the so called "tiles" of our game world, this class performs the scrolling all over your game world.
The main method again is the GameMap_Create
method which will create the game world sprite (let's define it like this).
The method that will render the game world on the screen is the GameMap_Draw
method.
Here, it should be mentioned that I have achieved the different terrain models, beside the basic four (grass
, send
, water
and snow
) by blending it using the CBitmapEx
class, so the game map tiles can be arranged the way I want on the screen, and a very different terrain variations can be achieved with this.
Now, the game logic had to be written.
The Game Engine
The class called CIsoGameEngine
is the heart of our system. The header file is, again, shown below:
#pragma once
#include "IsoGameUtils.h"
#include "IsoGameScriptManager.h"
#include "IsoGameScript.h"
#include "GameMap.h"
#include "GameSprite.h"
#include "GameUnit.h"
#define WINDOW_WIDTH 1024
#define WINDOW_HEIGHT 768
#define WORLD_WIDTH 128
#define WORLD_HEIGHT 128
#define SCROLL_LIMIT 20
#define SCROLL_SIZE 10
#define GAME_SLEEP_TIME 10
#define TOTAL_NEIGHBOURS 4
#define GAME_MAX_SPRITES 256
#define GAME_MAX_UNITS 1000
class CIsoGameEngine
{
public:
CIsoGameEngine(void);
virtual ~CIsoGameEngine(void);
public:
static void IsoGameEngine_SetGameMap(CGameMap* pGameMap) {m_pGameMap = pGameMap;}
static CGameMap* IsoGameEngine_GetGameMap() {return m_pGameMap;}
static POINT IsoGameEngine_GetMousePosition() {return m_ptMouse;}
static RECT IsoGameEngine_GetMouseDragRegion() {return m_rcMouse;}
static DWORD IsoGameEngine_GetGameTime() {return m_dwGameTime;}
static void IsoGameEngine_Draw(HDC hDC);
static void IsoGameEngine_Update();
static void IsoGameEngine_ProcessMouseEvent(UINT uMsg, WPARAM wParam, LPARAM lParam);
static void IsoGameEngine_AddGameSprite(CGameSprite* pGameSprite);
static void IsoGameEngine_RemoveGameSprite(int index);
static void IsoGameEngine_AddGameUnit(CGameUnit* pGameUnit);
static void IsoGameEngine_RemoveGameUnit(int index);
static CGameUnit* IsoGameEngine_GetGameUnit(LPSTR lpszName);
static BOOL IsoGameEngine_GetMouseLeftClick() {return m_bLeftClick;}
static BOOL IsoGameEngine_GetMouseRightClick() {return m_bRightClick;}
static BOOL IsoGameEngine_GetMouseDrag() {return m_bMouseDrag;}
static POINT IsoGameEngine_GetScreenOffset();
static RECT IsoGameEngine_GetScreenRect();
static void IsoGameEngine_AddGameScript(CIsoGameScript* pGameScript);
static void IsoGameEngine_ExecuteGameScript(LPSTR lpszName);
private:
static void IsoGameEngine_RebuildDisplayList();
static void IsoGameEngine_ResolveUnitCollision(CGameUnit* pGameUnit);
static void IsoGameEngine_ResolveTerrainCollision(CGameUnit* pGameUnit);
static void IsoGameEngine_DoPathfinding(CGameUnit* pGameUnit);
static BOOL IsoGameEngine_InCollision(CGameUnit* pFirstGameUnit, CGameUnit* pSecondGameUnit);
static BOOL IsoGameEngine_InCollision(CGameUnit* pGameUnit);
static void IsoGameEngine_UpdateDestinations();
private:
static CGameMap* m_pGameMap;
static POINT m_ptMouse;
static RECT m_rcMouse;
static DWORD m_dwGameTime;
static CGameSprite* m_lpGameSprites[GAME_MAX_SPRITES];
static int m_iTotalSprites;
static CGameUnit* m_lpGameUnits[GAME_MAX_UNITS];
static int m_iTotalUnits;
static CGameUnit* m_lpDisplayGameUnits[GAME_MAX_UNITS];
static int m_iDisplayTotalUnits;
static CGameUnit* m_lpSelectedGameUnits[GAME_MAX_UNITS];
static int m_iSelectedTotalUnits;
static CGameSprite* m_pPointerSprite;
static CGameUnit* m_pPointerUnit;
static CBitmapEx* m_pScreenBitmap;
static BOOL m_bLeftClick;
static BOOL m_bRightClick;
static BOOL m_bMouseDrag;
static CIsoGameScriptManager m_IsoGameScriptManager;
static long g_iFPS;
static DWORD g_dwCurrentTime;
static CHAR g_lpszFPS[255];
};
This class includes all other classes that we have mentioned before, and some special (I will explain them later). As I said before, the game map is the single object. On the other hand, the game sprites and the game units are defined and created more than once.
To add a new game sprite, use the IsoGameEngine_AddGameSprite
method. To remove it, use the IsoGameEngine_RemoveGameSprite
method.
To add a new game unit, use the IsoGameEngine_AddGameUnit
method. Similarly, to remove it, use the IsoGameEngine_RemoveGameUnit
method. To get the exact game unit, use the IsoGameEngine_GetGameUnit
method.
This class provides the basic interaction with the user (the player). It processes the mouse events (movements, dragging or clicks). The player would like to select the different game units, order them to move around or to perform a specific action. Also, he might select the non movable game units like houses, farms or other, in order to change its states somehow.
This class also updates the game world by scrolling the game terrain as you move around it. The special features of the game terrain are unexplored areas which tiles are rendered totally black. On the other hand, the parts of the game map that are visited before are rendered semi-black transparent, so you can see a bit through them. The parts of the game map that are occupied by the game unit are rendered fully visible (no secrets).
During these movements on the game map, the game units will collide with each other, so something called "the collision detection and response" must be written. This means that when the two, or more, game objects are in some type of the contact, the game logic must make a decision what to do. In the case of the collision, the "avoidance technique" is implemented here. This means that before the actual game unit movements are updated on the screen, this class will calculate the possible collisions. Some units will stop, while other will try to change its path to avoid it. The collision can be between the two moving game objects and one moving and one standing game object, which is easier to handle.
Another feature that is supported in this class is called the "scripting". What is it, and why is it needed?
The Game Scripts
The script is the good way to change the behavior of the game units during the game. When the game engine is finished, compiled and released, there is no conventional way to change its methods like you do it when you write it in the first place. If you have written the program routine to move the game unit always along the same path, that is what will happen. But, the scripts offer you the way to "change" the behavior of the game unit, or its actions, during the gameplay. The scripts can even be generated during the gameplay and loaded later, when you need them. A very interesting tool for the game engine designer.
So, the class called CIsoGameScript
is developed, please see the header file below:
#pragma once
#include "..\\main.h"
typedef enum _ISOGAMESCRIPTCOMMANDTYPE
{
SCT_NONE = 0,
SCT_COMMENT,
SCT_SET,
SCT_IF,
SCT_THEN,
SCT_ELSE,
SCT_ENDIF,
SCT_WHILE,
SCT_ENDWHILE,
SCT_BLOCK,
SCT_CALL,
SCT_ENDBLOCK,
SCT_SLEEP,
SCT_MOVE,
SCT_SHOW,
SCT_HIDE
} ISOGAMESCRIPTCOMMANDTYPE;
typedef enum _ISOGAMESCRIPTCONDITIONTYPE
{
SDT_NONE = 0,
SDT_EQUAL,
SDT_NOTEQUAL,
SDT_GREATER,
SDT_GREATEROREQUAL,
SDT_LESS,
SDT_LESSOREQUAL
} ISOGAMESCRIPTCONDITIONTYPE;
typedef enum _ISOGAMESCRIPTPROPERTYTYPE
{
SPT_NONE = 0,
SPT_POSITIONX,
SPT_POSITIONY,
SPT_DESTINATIONX,
SPT_DESTINATIONY,
SPT_SPEEDX,
SPT_SPEEDY,
SPT_HEALTH,
SPT_VISIBILITY,
SPT_SELECTED,
SPT_SCRIPTED,
SPT_ANIMTIME,
SPT_ISMOVING,
SPT_MESSAGE
} ISOGAMESCRIPTPROPERTYTYPE;
typedef enum _ISOGAMESCRIPTVARIABLETYPE
{
SVT_INTEGER = 0,
SVT_BOOLEAN,
SVT_STRING
} ISOGAMESCRIPTVARIABLETYPE;
typedef struct _ISOGAMESCRIPTSTATEMENTINFO
{
ISOGAMESCRIPTCOMMANDTYPE commandType;
CHAR szComment[4096];
CHAR szParam1[255];
CHAR szParam2[255];
CHAR szObjectName1[255];
CHAR szObjectProperty1[255];
ISOGAMESCRIPTPROPERTYTYPE propertyType1;
CHAR szObjectName2[255];
CHAR szObjectProperty2[255];
ISOGAMESCRIPTPROPERTYTYPE propertyType2;
CHAR szCondition[255];
ISOGAMESCRIPTCONDITIONTYPE conditionType;
} ISOGAMESCRIPTSTATEMENTINFO, *LPISOGAMESCRIPTSTATEMENTINFO;
typedef struct _ISOGAMESCRIPTVARIABLE
{
CHAR szVariableName[255];
ISOGAMESCRIPTVARIABLETYPE variableType;
union VARIABLETYPE
{
int iValue;
BOOL bValue;
CHAR szValue[4096];
} vValue;
} ISOGAMESCRIPTVARIABLE, *LPISOGAMESCRIPTVARIABLE;
class CIsoGameScript
{
public:
CIsoGameScript(void);
virtual ~CIsoGameScript(void);
public:
BOOL GameScript_Create(LPSTR lpszScriptName, LPSTR lpszScriptFile);
void GameScript_Execute();
BOOL GameScript_IsValid() {return (m_lpszGameScript != NULL);}
LPSTR GameScript_GetName() {return m_szName;}
BOOL GameScript_IsExecuting() {return m_bExecuting;}
private:
void GameScript_Compile();
void GameScript_ParseStatement
(LPSTR lpszStatement, LPISOGAMESCRIPTSTATEMENTINFO lpStatementInfo);
ISOGAMESCRIPTPROPERTYTYPE GameScript_GetPropertyType(LPSTR lpszProperty);
ISOGAMESCRIPTCONDITIONTYPE GameScript_GetConditionType(LPSTR lpszCondition);
int GameScript_GetNextStatement
(ISOGAMESCRIPTCOMMANDTYPE commandType, BOOL bCondition, int iCurrentStatement);
void GameScript_SetVariable
(LPSTR lpszVariableName, ISOGAMESCRIPTVARIABLETYPE variableType, void* variableValue);
LPISOGAMESCRIPTVARIABLE GameScript_GetVariable(LPSTR lpszVariableName);
static DWORD ScriptProc(LPVOID lpParameter);
private:
CHAR m_szName[255];
LPSTR m_lpszGameScript;
int m_iLen;
LPISOGAMESCRIPTSTATEMENTINFO* m_lpStatements;
int m_iTotalStatements;
LPISOGAMESCRIPTVARIABLE* m_lpVariables;
int m_iTotalVariables;
HANDLE m_hScriptThread;
BOOL m_bExecuting;
};
This class reads, parses and executes the files of the following type:
SET Adam.positionX TO 600
SET Adam.positionY TO 600
SET Adam.destinationX TO 250
SET Adam.destinationY TO 250
SET Adam.scripted TO true
SLEEP 23000
SET Adam.message TO "Hey, wait for me guys !!!\nI'm coming with you..."
SHOW Adam.message
MOVE Adam
SET moving TO true
WHILE moving = true
IF Adam.isMoving = false
THEN
SET moving TO false
ENDIF
SLEEP 500
ENDWHILE
HIDE Adam.message
SET Adam.scripted TO false
This is a non-existing IsoGameEngine
scripting language that I have designed for it. It is a command based language. If you need to position your game unit, you can write the "SET
" command with the param of game unit name.property
(like Adam.positionX
) and another keyword "TO
" and then the param which is the new X
coordinate for the game unit (called "Adam
").
It is obvious that in this way, we can affect the properties of our game objects, from the outer world, which is what we have wanted. So, you can script the movement whatever you like, and you don't need to recompile your source code.
The main method of this class is the GameScript_Create
method that will load your game script from the file, parse it, compile it, and execute it, whenever you need it.
So, where is the game AI at all?
The Game AI
Well, basically, the combination of the CIsoGameEngine
, CIsoGameScript
and the CIsoGameUtils
class (not mentioned here, but not so difficult to understand) makes the game AI. We can create the game units when we like, we can remove (destroy) them. We can arrange their moving route using the script, their conversation (yes, also this) through the scripting. The engine will keep our terrain rendered, as we move around, smoothly, the collisions will be calculated and avoided.
One thing that is not implemented here, but can be very easily added, is the game sound. Even for this, I have written my own sound class, available here on CodeProject and called CWave
class. It does not only the loading and the playback of the sounds, but can also do a "sound mixing" what is a perfect upgrade for this project. With this upgrade, the game units can actually talk, and the game world can have its own sounds (like birds, wind or rivers, etc.)
Using the Code
Inside the main.cpp file (the main program file that executes) there is a function called GameInit
. Inside this function, I have created the initial game world, and here is the code:
void GameInit()
{
int i, j;
g_pGameMap = new CGameMap();
g_pGameMap->GameMap_Create(_T("res\\Terrain_Map.bmp"), 64, 64,
WINDOW_WIDTH, WINDOW_HEIGHT, WORLD_HEIGHT, WORLD_WIDTH);
g_lpGameMapInfo = (LPGAMEMAPTILEINFO*)malloc(WORLD_HEIGHT*sizeof(LPGAMEMAPTILEINFO));
for (i=0; i<WORLD_HEIGHT; i++)
g_lpGameMapInfo[i] = (LPGAMEMAPTILEINFO)malloc(WORLD_WIDTH*sizeof(GAMEMAPTILEINFO));
for (i=0; i<WORLD_HEIGHT; i++)
{
for (j=0; j<WORLD_WIDTH; j++)
{
g_lpGameMapInfo[i][j].iType = GMT_GRASS;
g_lpGameMapInfo[i][j].iPassable = 1;
g_lpGameMapInfo[i][j].iHidden = 1;
g_lpGameMapInfo[i][j].iRow = 0;
g_lpGameMapInfo[i][j].iCol = 0;
}
}
g_lpGameMapInfo[0][0].iType = GMT_WATER;
g_lpGameMapInfo[0][0].iPassable = 0;
g_lpGameMapInfo[0][0].iRow = 0;
g_lpGameMapInfo[0][0].iCol = 1;
g_lpGameMapInfo[0][1].iType = GMT_WATER;
g_lpGameMapInfo[0][1].iPassable = 0;
g_lpGameMapInfo[0][1].iRow = 0;
g_lpGameMapInfo[0][1].iCol = 1;
g_lpGameMapInfo[1][0].iType = GMT_WATER;
g_lpGameMapInfo[1][0].iPassable = 0;
g_lpGameMapInfo[1][0].iRow = 0;
g_lpGameMapInfo[1][0].iCol = 1;
g_lpGameMapInfo[1][1].iType = GMT_WATER;
g_lpGameMapInfo[1][1].iPassable = 0;
g_lpGameMapInfo[1][1].iRow = 0;
g_lpGameMapInfo[1][1].iCol = 1;
g_lpGameMapInfo[0][2].iType = GMT_WATER;
g_lpGameMapInfo[0][2].iPassable = 1;
g_lpGameMapInfo[0][2].iRow = 0;
g_lpGameMapInfo[0][2].iCol = 5;
g_lpGameMapInfo[1][2].iType = GMT_WATER;
g_lpGameMapInfo[1][2].iPassable = 1;
g_lpGameMapInfo[1][2].iRow = 0;
g_lpGameMapInfo[1][2].iCol = 5;
g_lpGameMapInfo[2][0].iType = GMT_WATER;
g_lpGameMapInfo[2][0].iPassable = 1;
g_lpGameMapInfo[2][0].iRow = 0;
g_lpGameMapInfo[2][0].iCol = 7;
g_lpGameMapInfo[2][1].iType = GMT_WATER;
g_lpGameMapInfo[2][1].iPassable = 1;
g_lpGameMapInfo[2][1].iRow = 0;
g_lpGameMapInfo[2][1].iCol = 7;
g_lpGameMapInfo[2][2].iType = GMT_WATER;
g_lpGameMapInfo[2][2].iPassable = 1;
g_lpGameMapInfo[2][2].iRow = 1;
g_lpGameMapInfo[2][2].iCol = 2;
g_lpGameMapInfo[2][10].iType = GMT_WATER;
g_lpGameMapInfo[2][10].iPassable = 1;
g_lpGameMapInfo[2][10].iRow = 1;
g_lpGameMapInfo[2][10].iCol = 4;
g_lpGameMapInfo[2][11].iType = GMT_WATER;
g_lpGameMapInfo[2][11].iPassable = 1;
g_lpGameMapInfo[2][11].iRow = 0;
g_lpGameMapInfo[2][11].iCol = 6;
g_lpGameMapInfo[2][12].iType = GMT_WATER;
g_lpGameMapInfo[2][12].iPassable = 1;
g_lpGameMapInfo[2][12].iRow = 1;
g_lpGameMapInfo[2][12].iCol = 5;
g_lpGameMapInfo[3][10].iType = GMT_WATER;
g_lpGameMapInfo[3][10].iPassable = 1;
g_lpGameMapInfo[3][10].iRow = 0;
g_lpGameMapInfo[3][10].iCol = 4;
g_lpGameMapInfo[3][11].iType = GMT_WATER;
g_lpGameMapInfo[3][11].iPassable = 0;
g_lpGameMapInfo[3][11].iRow = 0;
g_lpGameMapInfo[3][11].iCol = 1;
g_lpGameMapInfo[3][12].iType = GMT_WATER;
g_lpGameMapInfo[3][12].iPassable = 1;
g_lpGameMapInfo[3][12].iRow = 0;
g_lpGameMapInfo[3][12].iCol = 5;
g_lpGameMapInfo[4][10].iType = GMT_WATER;
g_lpGameMapInfo[4][10].iPassable = 1;
g_lpGameMapInfo[4][10].iRow = 1;
g_lpGameMapInfo[4][10].iCol = 3;
g_lpGameMapInfo[4][11].iType = GMT_WATER;
g_lpGameMapInfo[4][11].iPassable = 1;
g_lpGameMapInfo[4][11].iRow = 0;
g_lpGameMapInfo[4][11].iCol = 7;
g_lpGameMapInfo[4][12].iType = GMT_WATER;
g_lpGameMapInfo[4][12].iPassable = 1;
g_lpGameMapInfo[4][12].iRow = 1;
g_lpGameMapInfo[4][12].iCol = 2;
g_pGameMap->GameMap_SetInfo(g_lpGameMapInfo);
CIsoGameEngine::IsoGameEngine_SetGameMap(g_pGameMap);
CGameSprite* pFarmerSprite = new CGameSprite();
pFarmerSprite->GameSprite_Create(_T("res\\farmer.bmp"), 96, 96, _RGB(106,76,48), _RGB(39,27,17));
CIsoGameEngine::IsoGameEngine_AddGameSprite(pFarmerSprite);
CGameSprite* pFarmHouseSprite = new CGameSprite();
pFarmHouseSprite->GameSprite_Create(_T("res\\farmhouse.bmp"), 288,
288, _RGB(191,123,199), _RGB(12,9,5));
CIsoGameEngine::IsoGameEngine_AddGameSprite(pFarmHouseSprite);
srand((unsigned int)time(NULL));
for (int i=0; i<10; i++)
{
RECT rcFarmer = {30, 70, 60, 80};
CGameUnit* pFarmerUnit = new CGameUnit();
pFarmerUnit->GameUnit_Create(pFarmerSprite, rand()%64, GUT_UNIT, rcFarmer);
GAMEUNITINFO farmerUnitInfo;
if (i < 3)
farmerUnitInfo.wFlags = GIT_NAME | GIT_POSITION | GIT_SPEED |
GIT_HEALTH | GIT_RANGE | GIT_ANIMTIME;
else
farmerUnitInfo.wFlags = GIT_POSITION | GIT_SPEED | GIT_HEALTH | GIT_RANGE | GIT_ANIMTIME;
if (i == 0)
strcpy(farmerUnitInfo.lpszName, "Joe");
else if (i == 1)
strcpy(farmerUnitInfo.lpszName, "John");
else if (i == 2)
strcpy(farmerUnitInfo.lpszName, "Adam");
farmerUnitInfo.positionX = 50 + rand()%(WINDOW_WIDTH-100);
farmerUnitInfo.positionY = 50 + rand()%(WINDOW_HEIGHT-100);
farmerUnitInfo.speedX = 2;
farmerUnitInfo.speedY = 2;
farmerUnitInfo.iHealth = rand() % 100;
farmerUnitInfo.iRange = 3;
farmerUnitInfo.dwAnimationTime = 50;
pFarmerUnit->GameUnit_SetInfo(farmerUnitInfo);
pFarmerUnit->GameUnit_AddAction("Up", 0, 7, 6);
pFarmerUnit->GameUnit_AddAction("Down", 8, 15, 14);
pFarmerUnit->GameUnit_AddAction("RightUp", 16, 23, 22);
pFarmerUnit->GameUnit_AddAction("LeftUp", 24, 31, 30);
pFarmerUnit->GameUnit_AddAction("RightDown", 32, 39, 38);
pFarmerUnit->GameUnit_AddAction("LeftDown", 40, 47, 46);
pFarmerUnit->GameUnit_AddAction("Right", 48, 55, 54);
pFarmerUnit->GameUnit_AddAction("Left", 56, 63, 62);
CIsoGameEngine::IsoGameEngine_AddGameUnit(pFarmerUnit);
}
RECT rcFarmHouse = {30, 120, 260, 270};
CGameUnit* pFarmHouseUnit = new CGameUnit();
pFarmHouseUnit->GameUnit_Create(pFarmHouseSprite, 0, GUT_BUILDING, rcFarmHouse);
GAMEUNITINFO farmhouseUnitInfo;
farmhouseUnitInfo.wFlags = GIT_POSITION | GIT_HEALTH | GIT_RANGE | GIT_ANIMTIME;
farmhouseUnitInfo.positionX = 300;
farmhouseUnitInfo.positionY = 300;
farmhouseUnitInfo.iHealth = rand() % 100;
farmhouseUnitInfo.iRange = 7;
farmhouseUnitInfo.dwAnimationTime = 5000;
pFarmHouseUnit->GameUnit_SetInfo(farmhouseUnitInfo);
pFarmHouseUnit->GameUnit_AddAction("Default", 0, 1, 0);
pFarmHouseUnit->GameUnit_ExecuteAction("Default", GUA_REPEAT);
CIsoGameEngine::IsoGameEngine_AddGameUnit(pFarmHouseUnit);
CIsoGameScript* pFamerJoeScript = new CIsoGameScript();
pFamerJoeScript->GameScript_Create("Farmer_Joe", "res\\Farmer_Joe.igs");
CIsoGameEngine::IsoGameEngine_AddGameScript(pFamerJoeScript);
CIsoGameScript* pFamerAdamScript = new CIsoGameScript();
pFamerAdamScript->GameScript_Create("Farmer_Adam", "res\\Farmer_Adam.igs");
CIsoGameEngine::IsoGameEngine_AddGameScript(pFamerAdamScript);
CIsoGameEngine::IsoGameEngine_ExecuteGameScript("Farmer_Joe");
CIsoGameEngine::IsoGameEngine_ExecuteGameScript("Farmer_Adam");
g_bRunning = TRUE;
g_hThread = ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)GameProc, NULL, 0, NULL);
}
So, at first, the game map is created, and the properties of the specific game terrain tiles were set. Here is that done inside the code, but can also be defined somewhere in the game resources. After that the game sprites are loaded and upon them, the game units (or the game characters) are created, like farmers and buildings. Finally, the game scripts are loaded.
Now, the fun can start.
Points of Interest
This is, maybe, the best project I ever wanted to work on, even if it was pushed by the old childhood dream.
For me, it came true...
History
- IsoGame Engine - version 1.0 (2017, but written a long time ago)