深度探索DxFramework 8-2
Table of Contents
请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com
第8章 World Map的剖析 2
8.2 Demo的游戏角色
8.2.1 游戏角色(role)的概念
world map demo用C_MapPC类描述了游戏主角player。而C_MapPC又继承自C_MapObject。追本溯源,就必须先要分析和理解C_MapPC类的实现。前面提过,C_MapObject继承自C_GameObject2D。是作为任何一种在游戏场景地图上的游戏对象(game object)的基类。比如主角,敌人,各种宝物等等。在这个demo中。这些“地图上的游戏对象”有什么共同的属性?答案就在C_MapObejct类的成员变量中:
class C_MapObject : public C_GameObject2D
{
protected:
DIR_TYPE face; //角色的朝向,东,南,西,北
int moveX; //每次角色移动距离的
int moveY; //大小,以像素为单位
int stepSize; //moveX/moveY变量的修正值
int xPos, yPos; //角色在世界中的坐标
int xModifier, //用在包围盒计算的修正值,将会在
int yModifier; //CheckWalkOn成员函数中使用
int animFrame; //当前播放的动画帧
C_Sprite* p_MapSprite // sprite on map
};
DIR_TYPE类型的face成员变量很好理解,在大部分2D游戏中,主角,或者是NPC。都有着四个或者更多的朝向。如果是静止的物品,比如说是一个不会动的道具,那么它的朝向就只有一个。对应地,每一个朝向也对应着一系列的动画帧,如图:
int类型的成员变量moveX,moveY也容易理解,当游戏对象在世界中移动时,每一帧都产生了位移。world map demo是基于二维空间的,所以必须使用两个变量来分别描述游戏对象沿着X轴和Y轴产生的位移。游戏对象既然生存在“游戏世界”中,所以就需要两个成员变量xPos,yPos来描述它在世界中的位置,以便进行绘制和其他相关的操作。作为一个2D游戏对象,要能渲染出来,就肯定少不了需要一个C_Sprite实例来作为它的成员。如果这个2D游戏对象还有动画效果的话,就应还需要一个变量来维护当前正绘制的动画帧。C_Sprite类型的对象指针p_MapSprite和int类型成员变animFrame正是满足所需。另外的几个修正值变量:stepSize,xModifier,yModifer将会在后面代码分析中详细描述它们的含义和作用。
在world map demo中,游戏对象有几个重要的行为。它们是:
- 游戏对象和场景间的碰撞检测。即是说某个游戏对象,比如主角,能否通过某一个格子而向前移动。
- 游戏对象沿着某个方向移动。
- 游戏对象在游戏场景地图中绘制。
- 游戏对象在小地图中绘制。
//负责移动map object在地图上移动的函数
void C_MapObject::Move(DIR_TYPE direction)
{
if (direction == DIR_NORTH) {
moveY = -stepSize;
} else if (direction == DIR_SOUTH) {
moveY = stepSize;
} else if (direction == DIR_EAST) {
moveX = stepSize;
} else { //(direction == DIR_WEST)
moveX = -stepSize;
}
face = direction;
++animFrame;
}
上面的函数很好理解,stepSize成员变量的作用在此也很明白了。就是根据不同的移动方向,设定沿X轴,Y轴移动的位移量。
//这个函数负责在小地图中绘制缩小了的map object,参数miniMapLeft,
//miniMapTop分别小地图左上角的屏幕坐标X,Y值。miniMapScale就是map
//object的放缩比例
void C_MapObject::RenderMini2D(float miniMapLeft,
float miniMapTop, float miniMapScale)
{
//计算缩小后的map object在小地图的屏幕坐标
float xCoord = miniMapLeft + xPos*miniMapScale;
float yCoord = miniMapTop + yPos*miniMapScale;
p_MapSprite->Render2D(D3DXVECTOR2(xCoord, yCoord),
D3DXVECTOR2(0,0), 0,
D3DXVECTOR2(miniMapScale,miniMapScale));
}
//计算两个游戏对象可能的要移动到的瓷砖。检查此两个瓷砖,看它是不是障碍
//物,如果不是的话返回TRUE,表示可以通过
BOOL C_MapObject::CheckWalkOn(DIR_TYPE direction)
{
int NTile = 0, STile = 0, WTile = 0, ETile = 0;
switch(direction) {
case DIR_NORTH: //向北走
// xPos是map object包围盒矩形左上角的顶点,
// 计算出相对于map object目前所在的瓷砖位置西北边
// 的瓷砖索引
WTile = (xPos+xModifier)/tileSize;
// 计算出相对于map object目前所在的瓷砖位置东北边的
// 瓷砖索引
ETile = (xPos+(int)width-xModifier)/tileSize;
//北边的,因为要向北边移动,Y坐标变小,所以要减去移动
//的距离stepSize
NTile = (yPos+yModifier-stepSize)/tileSize;
//检查东北边和西北边的瓷砖是否能通行,只要有一个不能
//通行,map object便不能向北边移动
if (!Map[NTile][WTile].walkOn ||
!Map[NTile][ETile].walkOn)
return FALSE;
break;
case DIR_SOUTH:
// 计算出相对于map object目前所在的瓷砖位置西南边
// 和东南边的瓷砖索引
WTile = (xPos+xModifier)/tileSize;
ETile = (xPos+(int)width-xModifier)/tileSize;
STile = (yPos+(int)height-yModifier+stepSize)
/tileSize;
//检查能否通行
if (!Map[STile][WTile].walkOn ||
!Map[STile][ETile].walkOn)
return FALSE;
break;
case DIR_EAST: //原理同上
NTile = (yPos+yModifier)/tileSize;
STile = (yPos+(int)height-yModifier)/tileSize;
ETile = (xPos+(int)width-xModifier+stepSize)
/tileSize;
if (!Map[NTile][ETile].walkOn ||
!Map[STile][ETile].walkOn)
return FALSE;
break;
case DIR_WEST:
NTile = (yPos+yModifier)/tileSize;
STile = (yPos+(int)height-yModifier)/tileSize;
WTile = (xPos+xModifier-stepSize)/tileSize;
if (!Map[NTile][WTile].walkOn ||
!Map[STile][WTile].walkOn)
return FALSE;
break;
};
return TRUE;
}
至于map object的在游戏场景地图中绘制行为,C_MapObject类的基类C_GameObject2D已经留下虚函数Render2D()作为渲染绘制接口。C_MapObject类没有改写此函数,这是合理的,因为每一个不同的map object将会有不同的渲染表现方式。把具体的渲染实现交给每个具体的map object实现类中来实现,更为妥当。
在world map demo中,map object只有一种具体实现,便是我们要分析的C_MapPC类,C_MapPC的对象实例就是demo中的主角player。除去C_GameObject2D类和C_MapObject所具有的行为之外,C_MapPC还有它特定的行为,比如根据它的世界坐标,计算成屏幕坐标。世界坐标和屏幕坐标的对应关系在上面已经介绍过了,对此遗忘了的读者可以看上面的介绍和示意图。根据世界坐标计算出主角的屏幕坐标,目的是为了渲染出主角。所以在分析如何把主角player的世界坐标转换成屏幕坐标之前。我们再来仔细观察一下主角player在屏幕中是如何渲染的:
主角player在移动的时候,是把它定位在场景的中间,移动地图,直到主角player走到当前场景地图的边界区域。如果主角player在场景的边界区域,从屏幕中间移动到屏幕边缘的时候,场景将不移动。知道了主角player是怎样渲染后,一切就很明了了。void C_MapPC::WorldPosToTrans()函数其实就是按上面的主角渲染方法来实现的。如下:
void C_MapPC::WorldPosToTrans()
{
D3DXVECTOR2 tempTrans;
// 把主角player定位在游戏地图窗口的中间。tempTrans保存的是主角
// player的包围盒矩形的左上角屏幕坐标
tempTrans.x = (float)(mapViewWidth/2 - tileSize/2);
tempTrans.y = (float)(mapViewHeight/2 - tileSize/2);
// 如果主角player的世界坐标X值已经小于地图宽度的一半,这表示,主
// 角已经处在了本地图的场景的西部,这时候地图已经不能卷动了,只能移
// 动主角player而场景地图静止。这个时候的主角player的屏幕坐标值X
// 值就等于它的世界坐标值X值。因为此时的世界坐标和屏幕坐标的Y轴重
// 合了
if (xPos < (mapViewWidth/2 - tileSize/2))
tempTrans.x = (float)xPos;
// 如果主角player的世界坐标Y值已经小于地图高度的一半,这表示,主
// 角已经处在了本地图的场景的北部,这时候地图已经不能卷动了,只能移
// 动主角player而场景地图静止。这个时候的主角player的屏幕坐标值
// Y值就等于它的世界坐标值Y值。因为此时的世界坐标和屏幕坐标的X轴
// 重合了。
if (yPos < (mapViewHeight/2 - tileSize/2))
tempTrans.y = (float)yPos;
// mapX,mapY分别是本地图当前的纵向和横向瓷砖数
// mapX*tileSize表示本地图X轴方向的长度,单位为像素
// mapY*tileSize表示本地图Y轴方向的长度,单位为像素
// 如果主角player的世界坐标X值已经大于地图宽度的一半,这表示,主
// 角已经处在了本地图的场景的东部,这时候地图已经不能卷动了,只能移
// 动主角player而场景地图静止。这表示当前地图的最东端X值和屏幕
// 坐标的最大X值已经重合
if (xPos >= (mapX*tileSize)-
(mapViewWidth/2 + tileSize/2))
{
tempTrans.x = (float)(mapViewWidth -
((mapX*tileSize) - xPos));
}
// 如果主角player的世界坐标Y值已经大于地图高度的一半,这表示,主
// 角已经处在了本地图的场景的东部,这时候地图已经不能卷动了,只能
// 移动主角player而场景地图静止。这表示当前地图的最南端Y值和屏
// 幕坐标的最大Y值已经重合
if (yPos >=(mapY*tileSize)-
(mapViewHeight/2 + tileSize/2))
{
tempTrans.y = (float)(mapViewHeight -
((mapY*tileSize) - yPos));
}
SetTranslation(tempTrans);
}
至此,主角player的根据世界坐标计算出屏幕坐标的方法已经分析完毕,主角player可以着手进行渲染了。渲染的代码非常之简单,就是对C_Sprite::Render2D函数的调用而已,代码如下:
void C_MapPC::Render2D()
{
p_MapSprite->Render2D(GetTranslation());
}
每一帧要更新player的状态,更新状态的接口是void C_MapPC::Update()函数。此函数是一个虚函数,在C_GameObject2D类中声明。继承自C_GameObject2D类的各子类可以依据它本身的更新状态行为改写此函数。C_MapPC::Update()函数主要功能是:响应外设输入,进行主角player移动方向的判断;更新主角player的世界坐标;根据世界坐标计算出主角的渲染坐标。更新主角player的当前渲染帧。代码如下:
void C_MapPC::Update()
{
if((gp_Controller->GetKeyState(DIK_UP) == BUTTON_DOWN)
&& CheckWalkOn(DIR_NORTH))
{
Move(DIR_NORTH);
}
else if((gp_Controller->GetKeyState(DIK_DOWN)
== BUTTON_DOWN) && CheckWalkOn(DIR_SOUTH))
{
Move(DIR_SOUTH);
}
else
moveY = 0;
if ((gp_Controller->GetKeyState(DIK_RIGHT)
== BUTTON_DOWN) && CheckWalkOn(DIR_EAST))
{
Move(DIR_EAST);
}
else if ((gp_Controller->GetKeyState(DIK_LEFT)
== BUTTON_DOWN) && CheckWalkOn(DIR_WEST))
{
Move(DIR_WEST);
}
else
moveX = 0;
//每间隔20毫秒更新一次主角player的世界坐标
if (gp_Timer->TimeMod(.02) == 0)
{
xPos += moveX;
yPos += moveY;
}
// 根据世界坐标计算出player的屏幕坐标
WorldPosToTrans();
// 改变方向了,当前显示帧的帧数也要改变
if (face == DIR_NORTH &&
(animFrame < 6 || animFrame > 11))
animFrame = 6;
if (face == DIR_SOUTH && (animFrame > 5))
animFrame = 0;
if (face == DIR_EAST &&
(animFrame < 18 || animFrame > 23))
animFrame = 18;
if (face == DIR_WEST &&
(animFrame < 12 || animFrame > 17))
animFrame = 12;
p_MapSprite->SetAnimation(animFrame);
}
到现在为止,demo的主要功能模块的实现原理都分析完毕了,一切都昭然若揭。万流归海。