Click here to Skip to main content
15,867,568 members
Articles / Multimedia / GDI

IsoGame Engine

Rate me:
Please Sign up or sign in to vote.
4.84/5 (17 votes)
18 May 2017CPOL12 min read 24.7K   595   23   14
Article about the 2D isometric game engine

IsoGame Engine - in action

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:

Farmer - the spriteFarmhouse - the sprite

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:

C++
#pragma once

// Includes
#include "BitmapEx.h"


// CGameSprite class definition
class CGameSprite
{
public:
	CGameSprite(void);
	virtual ~CGameSprite(void);

public:
	// Public methods
	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:
	// Private methods
	void GameSprite_Draw(int dstX, int dstY, int width, int height, 
      CBitmapEx* pScreenBitmap, int srcX, int srcY, _PIXEL transparentColor, _PIXEL shadowColor);

private:
	// Private members
	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:

C++
#pragma once

// Includes
#include "GameSprite.h"

// Constants
#define MAX_WAYPOINTS		128
#define MESSAGE_FONT_FACE	_T("Courier New")
#define MESSAGE_FONT_SIZE	10

// Definitions
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;


// CGameUnit class definition
class CGameUnit
{
public:
	CGameUnit(void);
	virtual ~CGameUnit(void);

public:
	// Public methods
	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:
	// Private members
	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:

C++
#pragma once

// Includes
#include "BitmapEx.h"

// Constants
#define MIN_ROWS	32
#define MAX_ROWS	1024
#define MIN_COLS	32
#define MAX_COLS	1024

// Definitions
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;


// CGameMap class definition
class CGameMap
{
public:
	CGameMap(void);
	virtual ~CGameMap(void);

public:
	// Public methods
	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:
	// Private methods
	void GameMap_ScrollLeft();
	void GameMap_ScrollRight();
	void GameMap_ScrollUp();
	void GameMap_ScrollDown();

private:
	// Private members
	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:

Game terrain

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:

C++
#pragma once

// Includes
#include "IsoGameUtils.h"
#include "IsoGameScriptManager.h"
#include "IsoGameScript.h"
#include "GameMap.h"
#include "GameSprite.h"
#include "GameUnit.h"

// Constants
#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

// CGameIsoEngine  class definition
class CIsoGameEngine
{
public:
	CIsoGameEngine(void);
	virtual ~CIsoGameEngine(void);

public:
	// Public methods
	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:
	// Private methods
	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:
	// Private members
	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:

C++
#pragma once

// Includes
#include "..\\main.h"


// Definitions
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;


// CIsoGameScript  class definition
class CIsoGameScript
{
public:
	CIsoGameScript(void);
	virtual ~CIsoGameScript(void);

public:
	// Public methods
	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:
	// Private methods
	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:
	// Private members
	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:

C++
// Move farmer Adam
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

// Wait for farmers Joe finish their conversation
SLEEP 23000

// Show farmer Adam's message
SET Adam.message TO "Hey, wait for me guys !!!\nI'm coming with you..."
SHOW Adam.message
MOVE Adam

// Check for farmer Adam reached his destination
SET moving TO true
WHILE moving = true
	IF Adam.isMoving = false
		THEN
			SET moving TO false
	ENDIF
	SLEEP 500
ENDWHILE

// Hide farmer Adam's message
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:

C++
void GameInit()
{
	int i, j;

	// Create game map
	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);

	// Create game sprites
	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);

	// Create game units
	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);

	// Load game scripts
	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");

	// Start game thread
	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)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Elektromehanika d.o.o. Nis
Serbia Serbia
He has a master degree in Computer Science at Faculty of Electronics in Nis (Serbia), and works as a C++/C# application developer for Windows platforms since 2001. He likes traveling, reading and meeting new people and cultures.

Comments and Discussions

 
SuggestionChange of Article Title Pin
Randor 22-May-17 0:55
professional Randor 22-May-17 0:55 
GeneralRe: Change of Article Title Pin
darkoman22-May-17 7:07
darkoman22-May-17 7:07 
GeneralRe: Change of Article Title Pin
darkoman22-May-17 7:17
darkoman22-May-17 7:17 
GeneralRe: Change of Article Title Pin
Member 139774062-Jan-19 16:24
Member 139774062-Jan-19 16:24 
GeneralNot isometric drawing Pin
Shao Voon Wong20-May-17 20:55
mvaShao Voon Wong20-May-17 20:55 
GeneralRe: Not isometric drawing Pin
darkoman20-May-17 21:43
darkoman20-May-17 21:43 
QuestionDownload link working again Pin
darkoman19-May-17 10:42
darkoman19-May-17 10:42 
QuestionLooks like a fun project Pin
Member 255500619-May-17 10:27
Member 255500619-May-17 10:27 
AnswerRe: Looks like a fun project Pin
darkoman19-May-17 10:55
darkoman19-May-17 10:55 
GeneralMy vote of 1 Pin
Country Man19-May-17 1:52
Country Man19-May-17 1:52 
GeneralRe: My vote of 1 Pin
darkoman19-May-17 10:55
darkoman19-May-17 10:55 
It is working now, please check.
Thank you.
QuestionDownload Link Pin
Member 177254818-May-17 19:17
professionalMember 177254818-May-17 19:17 
AnswerRe: Download Link Pin
darkoman19-May-17 10:54
darkoman19-May-17 10:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.