IsoGame Engine
关于 2D 等轴测游戏引擎的文章
引言
本文是关于用 C++ 编写的 2D 等距游戏引擎。它处理从头开始编写时需要考虑的大多数基本事项,例如:图像、精灵、地形生成、碰撞、AI、游戏脚本等。
背景
关于这个主题,您可以在互联网上的许多其他文章中找到本文的背景。主要的想法是提出一种可能的解决方案,同时考虑到当今可用的所有技术进步,包括不同的语言、库或硬件资源。因此,本项目中的许多内容都是从头开始编写的,以展示此类有趣但非常困难的软件开发部分所必须进行的工作。毕竟,一切都在游戏中。
游戏故事板
它始于我们的旅程。我们都喜欢玩像“帝国时代”(我的最爱)、“沙丘”(我的另一个最爱)、“星际争霸”(又是最爱)这样的老式 2D 等距策略游戏,随着我深入童年,这份清单还会不断增长。所以,有一天,问题自然而然地出现了——如何做到?这不是如何做得更好,也不是如何卖出数百万份,而是一个简单的问题“如何做到?”
当然,需要一个概念或某种故事。它就从那里开始。如果我想在 2D 中创建一个生动的世界,以 45 度角观察它,并操作角色(或后来的游戏单位),那么我需要想象那个世界会是什么样子。我的愿望是它是一个等距的旧世界,有农舍、草地(后来有森林、湖泊、山丘、动物等)和一些居民(后来的战士)。所以,第一件事是为我的游戏寻找图形,如果我不能自己创建的话。
村民的简单图像还不够。它应该移动和反应。所以,我需要一系列特殊的相似图像,称为“精灵
”,请看下面
游戏精灵
每个精灵都包含有限数量的相同大小的单个图像。程序例程将提取并绘制正确的序列,正如稍后将定义的。所以,现在我有了简单而有效的动画。这是一项非常古老的技术,现在在 3D 世界中,但在那时它已经超出了我的预期。
对于位图操作,我“借用”了我在 CodeProject 上提供的图像库,即 CBitmapEx
类。整篇文章在此处提供。这个类允许我读取“.BMP”图像文件并对其进行操作,以及执行快速屏幕绘制。
因此,我必须为这个项目编写的第一个类之一是 CGameSprite
类。它将处理前面提到的 CBitmapEx
类,以便加载游戏精灵图像序列,提取正确的图像并将其渲染到屏幕上。下面是这个类的头文件
#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;
};
因此,这个类将允许您基于提供的不同精灵图像创建游戏对象。每个精灵图像都包含有限数量的单个图像,称为“图块
”。每个图块具有相同的宽度和高度。此外,这个精灵还需要支持透明区域(不会渲染到屏幕上),以及阴影(如果可用)将半透明渲染。从基础游戏对象的角度来看,这已经足够了。游戏对象现在可以根据精灵图块集合中不同图像序列进行移动和反应。
CGameSprite
类的主要方法是 GameSprite_Create 方法
,它将根据图像文件和图块参数实际创建精灵。
另一个重要的方法是 GameSprite_Draw 方法
(有两个版本),它将精灵渲染到屏幕上。
其他方法是关于精灵图像图块集的信息方法。
现在,我有了游戏对象,但它们需要一些关于自身的额外信息。
游戏单位
名为 CGameUnit
的类是这个挑战的答案。它还使用了前面定义的游戏精灵类。在这个类的头文件中,您会发现许多不同的属性(在 enums
或 structs
中定义)。这些属性定义了游戏单位的特征,例如类型(单位、建筑、资源或其他)、移动方向和相应的动画、当前位置、移动、速度、生命值、脚本等。因此,这里应该全局定义与游戏单位相关的一切。除了属性之外,这里还应该定义游戏单位可以执行的游戏动作,例如创建、定位、移动、通过选择与用户交互、脚本编写等。这个类的头文件如下所示
#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;
};
好的,这个类比前一个更严肃。CGameUnit
类使用游戏精灵类进行视觉表示,但增加了功能和逻辑。首先,游戏单位是在现有精灵上创建的。如果游戏单位没有“身体”,就无法存在。
这里的重点是,您可以根据项目最初的故事板,拥有任意数量的游戏单位。相同类型的游戏单位将共享相同的游戏精灵。因此,它们将共享视觉表现,但会与其他游戏单位或周围环境以不同方式进行反应。
CGameUnit
类中的主要方法是 GameUnit_Create
方法。当您想创建游戏单位,或大量使用相同游戏精灵的游戏单位时,可以调用它。
根据其当前位置在屏幕上渲染游戏单位的方法是 GameUnit_Draw
方法。
命令游戏单位移动的方法是 GameUnit_Move
方法。现在,游戏单位将开始改变其在屏幕上的当前位置并显示其移动动画序列。
此类别还提供了许多信息方法。
还有一些非常具体的考虑 AI 和脚本的方法,但我们稍后会介绍。
好的,现在我需要一个游戏世界。
游戏地图
CGameMap
类提供了为我们的游戏单位创建地形(或世界)的可能性。请查看下面的头文件
#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;
};
游戏地形是一个图像,如下图所示
CGameMap
类与游戏精灵类非常相似。但在这里,您只能有一个游戏世界,并且可以创建多个游戏精灵。与游戏精灵类一样,这个类将读取由不同地形图块组成的地形图像文件。如您所见,地形应该由草地、沙地、水和雪组成,但所有这些都已在项目最初的故事板中定义(希望如此)。除了简单地渲染我们游戏世界中所谓的“图块”之外,这个类还会在整个游戏世界中执行滚动。
主要方法仍然是 GameMap_Create
方法,它将创建游戏世界精灵(让我们这样定义它)。
在屏幕上渲染游戏世界的方法是 GameMap_Draw
方法。
这里应该提到的是,除了基本的四种(草地
、沙地
、水
和雪
)地形模型之外,我还通过使用 CBitmapEx
类进行混合实现了不同的地形模型,因此游戏地图图块可以按我想要的方式在屏幕上排列,从而实现非常不同的地形变化。
现在,必须编写游戏逻辑。
游戏引擎
名为 CIsoGameEngine
的类是我们系统的核心。头文件再次显示如下
#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];
};
这个类包含了我们之前提到的所有其他类,以及一些特殊的类(我稍后会解释)。正如我之前所说,游戏地图是单个对象。另一方面,游戏精灵和游戏单位被定义和创建不止一次。
要添加新的游戏精灵,请使用 IsoGameEngine_AddGameSprite
方法。要删除它,请使用 IsoGameEngine_RemoveGameSprite
方法。
要添加新的游戏单位,请使用 IsoGameEngine_AddGameUnit
方法。同样,要删除它,请使用 IsoGameEngine_RemoveGameUnit
方法。要获取确切的游戏单位,请使用 IsoGameEngine_GetGameUnit
方法。
这个类提供了与用户(玩家)的基本交互。它处理鼠标事件(移动、拖动或点击)。玩家希望选择不同的游戏单位,命令它们四处移动或执行特定动作。此外,他还可以选择不可移动的游戏单位,如房屋、农场或其他,以便以某种方式改变其状态。
此类别还会通过在游戏地形中移动时滚动游戏地形来更新游戏世界。游戏地形的特殊功能是未探索区域,其图块完全渲染为黑色。另一方面,之前访问过的游戏地图部分渲染为半透明黑色,因此您可以稍微看穿它们。被游戏单位占据的游戏地图部分完全可见(没有秘密)。
在游戏地图上的这些移动过程中,游戏单位会相互碰撞,因此必须编写“碰撞检测与响应”代码。这意味着当两个或更多游戏对象发生某种接触时,游戏逻辑必须决定如何处理。在碰撞的情况下,这里实现了“避让技术”。这意味着在实际游戏单位移动在屏幕上更新之前,这个类会计算可能的碰撞。一些单位会停止,而另一些单位会尝试改变路径以避开它。碰撞可能发生在两个移动的游戏对象之间,以及一个移动的游戏对象和一个静止的游戏对象之间,后者更容易处理。
此类别支持的另一个功能称为“脚本编写”。它是什么,为什么需要它?
游戏脚本
脚本是改变游戏过程中游戏单位行为的好方法。当游戏引擎完成、编译和发布后,就没有传统的方式来改变它的方法,就像你最初编写它时那样。如果你编写了程序例程让游戏单位始终沿着同一路径移动,那么就会发生这种情况。但是,脚本为你提供了一种在游戏过程中“改变”游戏单位行为或其动作的方式。脚本甚至可以在游戏过程中生成,并在你需要时加载。对于游戏引擎设计师来说,这是一个非常有趣的工具。
因此,开发了一个名为 CIsoGameScript
的类,请参见下面的头文件
#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;
};
此类读取、解析并执行以下类型的文件
// 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
这是一种不存在的 IsoGameEngine
脚本语言,是我为它设计的。它是一种基于命令的语言。如果您需要定位您的游戏单位,您可以编写带有游戏单位 name.property
(例如 Adam.positionX
)参数的“SET
”命令,以及另一个关键字“TO
”,然后是游戏单位(称为“Adam
”)的新 X
坐标参数。
很明显,通过这种方式,我们可以从外部世界影响游戏对象的属性,这正是我们想要的。因此,您可以编写任何您喜欢的移动脚本,而无需重新编译源代码。
这个类的主要方法是 GameScript_Create
方法,它将从文件中加载您的游戏脚本,解析它,编译它,并在您需要时执行它。
那么,游戏 AI 到底在哪里呢?
游戏人工智能
嗯,基本上,CIsoGameEngine
、CIsoGameScript
和 CIsoGameUtils
类(这里没有提到,但也不难理解)的组合构成了游戏 AI。我们可以随时创建游戏单位,也可以移除(销毁)它们。我们可以使用脚本安排它们的移动路线,通过脚本安排它们的对话(是的,这也是)。引擎会平滑地渲染我们的地形,随着我们四处移动,碰撞也会被计算和避免。
这里没有实现,但可以非常容易添加的一点是游戏声音。为此,我甚至编写了自己的声音类,可在 CodeProject 这里获取,名为 CWave
类。它不仅可以加载和播放声音,还可以进行“混音”,这对于本项目来说是一个完美的升级。有了这个升级,游戏单位实际上可以说话,游戏世界也可以拥有自己的声音(例如鸟鸣、风声或河流声等)。
Using the Code
在 main.cpp 文件(执行的主程序文件)中,有一个名为 GameInit
的函数。在这个函数中,我创建了初始游戏世界,代码如下
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);
}
因此,首先创建了游戏地图,并设置了特定游戏地形图块的属性。这在代码中完成,但也可以在游戏资源中定义。之后加载游戏精灵,并在其基础上创建游戏单位(或游戏角色),例如农民和建筑物。最后,加载游戏脚本。
现在,乐趣可以开始了。
关注点
这可能是我一直想做的最好的项目,即使它是被儿时的旧梦所驱使的。
对我来说,它实现了……
历史
- IsoGame 引擎 - 版本 1.0 (2017 年,但很久以前编写)