深度探索DxFramework


请尊重原作者的工作,转载时请务必注明转载自: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中,游戏对象有几个重要的行为。它们是:

  1. 游戏对象和场景间的碰撞检测。即是说某个游戏对象,比如主角,能否通过某一个格子而向前移动。
  2. 游戏对象沿着某个方向移动。
  3. 游戏对象在游戏场景地图中绘制。
  4. 游戏对象在小地图中绘制。
//负责移动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的主要功能模块的实现原理都分析完毕了,一切都昭然若揭。万流归海。

返回首页
上一章
下一章