【cocos2D-x学习】11.移动管理

【目标】:为游戏中的敌人赋予移动能力,使其可以按照预定的轨迹移动


【参考】:

1、MoonWarrior 工程源码

2、CCDictionary 解析xml总结

3、cocos2D-x权威指南(基本没什么用)


一、移动控制的大致架构

       当前正在进行的尝试是制作一个简单的弹幕射击类游戏。我以前没有设计游戏架构的经验,这里设计的架构可能是非常糟糕的,仅作为一种早期的探索和尝试。

       这里的处理的基本思路如下:

       1、所有的敌人都由一个管理者管理。

       2、每个人物有自己预制的一条行动路线。

       3、每个行动路线可以被拆分为多个连续的简单路径。

      具体到代码,会设置一个 EnemyManager 来管理所有的Enemy,其中又包含已经登场的敌人,和等待登场的敌人。每个Enemy都有一个自己的行动路线,称为 Movement,每个Movement又保有一个简单路径的队列,这个简单路径是以MovementFunction为基类的类型:


      下面分别介绍各个部分。


二、人物管理:敌人的登场和离开

1、几种模式

     这个部分面对的问题是:如何创建一坨敌人,让他们按照指定的时间入场和离开?

     回忆我们玩过的游戏,可以大致总结出以下几种模式:

     1、地雷式:中国RPG中相当常见的踩地雷(最新的轩辕剑6还在用这种模式)。完全不在主游戏界面中创建敌人,仅仅在满足一定条件时触发战斗,根据当时场景的信息(例如主角所在的位置),来创建敌人。这种方式最简单,或者说根本就不存在我们所说的问题。但是对于我们这里要做的射击游戏来说,是完全不可行的。

     2、生态式:游戏仅仅预制敌人的位置,并赋予敌人一定的人工智能,使得其可以根据场景来自动采取行动。很多ARPG都是这个套路,或者是和下面的设定路线式结合使用。我这里主要是探究移动管理,所以这里暂时不采用这种方法。

     3、脚本式:游戏场景设定脚本,所有的敌人完全按照脚本行动。很多弹幕射击类是这个方式(所以你背板就会容易不少)。我这里也就采取这个策略。

      具体来说,就是通过一个脚本文件,设定所有的敌人的行为,然后通过一个Manager管理所有的敌人,使得其按照脚本行动。这个管理者, 就是我前面图中的 EnemyManager。


2、实现方式

      EnemyManager 维护两个列表,一个是已经登场的敌人existingEnemyList,一个是将要登场的敌人waitingEnemyList。

      游戏刚开始时,所有的敌人都处在等待登场的列表中,按照入场的时间顺序排列。然后在重载的update中检查列表头,让需要入场的敌人入场。update中同时还会更新每个已经入场的敌人的状态:

void EnemyManager::update(float dt) {
    static float ellapsedTime = 0;
    ellapsedTime += dt;

    //更新场上现有的
    std::vector<PlaneBase *>::iterator itor;
    for ( itor = existingEnemyList.begin(); itor != existingEnemyList.end(); itor ++ )
        (*itor)->updatePlane(dt);

    //如果有敌人该上场了,则将它加入,并移入 existingEnemyList
    while ( !waitingEnemyList.empty() && waitingEnemyList.front()->isTimeToAppear(ellapsedTime) ) {
        PlaneBase * newEnemy = popFromWaitingQueue();
        addChild(newEnemy);
        existingEnemyList.push_back(newEnemy);
    }

    std::vector<PlaneBase *>::iterator iter =  existingEnemyList.begin();
    while ( iter != existingEnemyList.end() ) {
        if ( (*iter)->isTimeTodisappear() )
            iter = removeFromExistingList(iter);
        else
            iter ++;
    }
}
      这里有两个辅助的小函数:

//这里不做queue为空的检查
PlaneBase * EnemyManager::popFromWaitingQueue() {
    PlaneBase * ret = waitingEnemyList.front();
    waitingEnemyList.pop();
    return ret;
}

//先保存结果,我不清楚release会不会使得这里的erase失效
std::vector<PlaneBase *>::iterator EnemyManager::removeFromExistingList(std::vector<PlaneBase *>::iterator itor) {
    CCNode * item = (*itor);
    std::vector<PlaneBase *>::iterator ret = existingEnemyList.erase(itor);

    removeChild(item, true);
    item->release();
    return ret;
}

3、内存管理:记得释放引用

       这里被管理的Enemy是一个加入了内存自动管理池的CCNode,在创建的时候,为了不在函数结束的时候自动释放,需要retain一把(这一套可以从 addChild中学习),那么在EnemyManager析构的时候,自然需要将所有的enemy的引用释放掉,以免内存泄露。

        其实这一套在移除一个消失的敌人的时候也需要做,所以在上面的 removeFromExistingList 中就已经有了 release操作,这里还需要照葫芦画瓢再来一次:

EnemyManager::~EnemyManager() {
    //移除未释放的list中的元素,否则会内存泄露
    while ( !waitingEnemyList.empty() )
        popFromWaitingQueue()->release();

    std::vector<PlaneBase *>::iterator iter =  existingEnemyList.begin();
    while ( iter != existingEnemyList.end() ) {
        iter = removeFromExistingList(iter);
    }
}


4、单实例的manager

     最后,我们还需要把这个类做成单实例,方便外部访问。这里使用静态局部变量来实现:

EnemyManager * EnemyManager::getInstance() {
    static EnemyManager * manager = EnemyManager::create();
    return manager;
}

     这个时候,我们只需要打个桩,实现一个简单的enemy,就可以让这一套系统运作起来了。


三、移动函数:让敌人动起来

      但是要让敌人实现复杂的运动轨迹,就需要为其赋予一个移动函数,让他照着函数轨迹运动。虽然最终的目标是可以是一个连续的分段函数,不过这里先做简单的一段式的运动函数。

        这里实际上我只实现了两种最基本的运动函数,一种是完全不动的,一种是多项式的。两者都继承于基类MoveFunctionBase。实现并不复杂,主要只有两个困难点。

1、多项式的保存

        由于C中的数组是不知道length的,所以主要的麻烦在于确定多项式的项数。我这里想了两种初始化多项式的方法,一种使用模板,可以参考《非类型模板类》,缺点是使用的时候必须要特化,比较麻烦。另外一种就是构造的时候直接传入项数,这个简单直接,所以最终还是选择了这一种。


2、屏幕分辨率的适应

      众所周知,android手机有多种分辨率。移动函数计算的结果应该是 position。但是对于同样一个 ccp(500, 600),可能在某些手机上在屏幕外,但是在某些手机上可能还只是在屏幕中央,所以需要适配屏幕分辨率。

        我这里借鉴了 MoonWarrior的处理办法,具体实现是在 LevelManager.js 中。它的方法是取出屏幕大小,然后入场在屏幕分辨率的基础上再取偏移量:

            case MW.ENEMY_MOVE_TYPE.VERTICAL:
                offset = cc.p(0, -winSize.height - enemycs.height);
                tmpAction = cc.MoveBy.create(4, offset);
                break;
   我这里更进一步,让移动函数计算出来的结果只是一个比例 (x, y),然后再用屏幕分辨率换算成实际坐标

    (px, py) =  (x * winSize.width, y * winSize.height)

    这样,无论在什么样的屏幕上,至少位置应该是没有问题了。


3、移动的开始和结束

      如何标记这段移动函数已经结束?我这里使用的是时间做为标记。每个移动函数都有一个持续时间,外部会不断调用其update来更新位置,同时累计已经持续的时间,并用这个 currentTime 和持续时间比较。如果超过持续时间, 则判定本次移动已经结束。


四、复杂的移动轨迹:连接多个分段的移动函数

     这一步,是要把多段移动函数组合成一条复杂的移动曲线。这里暂时还不考虑循环移动的情况,使用一个队列来记录各段移动函数。然后类似于上面的enemyManager,不停的检查当前运动轨迹的情况,并更新队列。

1、构造函数

      由于要初始化一个队列,所以又面临要传递数组的情况,这里使用另一种解决方案,用可变参数列表来解决。一些关键的语法点,可以参考《可变参数列表》。具体代码如下:

Movement::Movement(MoveFunctionBase * firstMove, ...)
    : totalStage(0), currentStage(0), currentMoveFunction(firstMove), isOver(false),
    currentPosition(firstMove->startPoint)
{
    //放入第一个元素
    pushIntoQueue(firstMove);

    //处理后面的可变参数列表
    va_list itor;
    va_start(itor, firstMove);
    MoveFunctionBase * current = va_arg(itor, MoveFunctionBase*);
    while ( current != NULL ) {
        pushIntoQueue(current);
        current = va_arg(itor, MoveFunctionBase*);
    }
    va_end(itor);

    this->autorelease();
}
     同样需要注意的是,为了防止 CCNode在构造函数结束时被自动销毁,这里在 pushIntoQueue 的时候要 retain一下:

void Movement::pushIntoQueue(MoveFunctionBase * move) {
    movementQueue.push(move);
    move->retain();
    totalStage ++;
}

      当然,在析构或出栈的时候,需要做对应的 release操作。


2、更新位置和队列

逻辑实在是很简单,这里直接贴一下代码。

CCPoint Movement::updatePosition(float dt) {
    //已经结束了,不再更新
    if ( isOver )
        return currentPosition;

    currentPosition = currentMoveFunction->updateMove(dt);

    //变化也发生在下一次更新时
    if (  currentMoveFunction->isMoveOver() ) {
        currentStage ++;
        removeFrontOfQueue();

        //理论上第一个判断成立即可
        if ( currentStage >= totalStage || movementQueue.empty()) {
            isOver = true;
        } else {
            currentMoveFunction = movementQueue.front();
        }
    }

    return currentPosition;
}

       OK,我们现在就有了一套可以使得敌人运动的机制了,不过既然是脚本,那么就要支持通过读取文件来创建,这是我们下一步的工作。


五、从list文件中创建

     这里主要是参考《CCDictionary 解析xml总结》,这篇文章写的非常好。另外就是 CCParticleSystem 的源码。

1、CCDictionary的大致情况

     以前就使用过 CCDictionary类,不过当时的用途非常简单,我们这里要稍微复杂一点。目前我已经知道的可以填入的类型有以下类型:

     1)bool类型:使用 <true></true>  或者 <false></false>标签,TAG里面不需要填充

     2)int类型:使用 <integer>1</integer> 标签

     3)浮点数类型:使用<real>0.5</real>标签

     4)字符串类型:使用 <string>{0.5}</string> 标签

     5)数组类型:使用如下结构:

     <array>

           <??>……第一个元素……</??>

           <??>……第二个元素……</??>

           …………

           <??>……第N个元素……</??>

     </array>

     6)字典类型:使用<dict>………</dict>标签


2、数据的解析

     CCDictionary 有两个主要的解析数据的方法,一个是 objectForKey,返回的是 CCObject *,另一个是 valueForKey,返回的是 CCString。

      按照我的使用习惯,一般对于基本类型和字符串类型,使用 valueForKey获得字符串,然后再调用CCString 的方法,如 IntValue,来转换成对应的值。需要注意的是,CCstring的各种转换的默认值都是0,或者是“”。

     对于数组,或者字典,则一般使用  objectForKey,然后用 static_cast 来进行转换。


3、数组的解析

    一般的数组类型解析都很简单,而对于数组,常见的有两种方法。

1)使用 <array>标签:

     例如plist中这样的一个数组:

<key>movement</key>
<array>
    <dict>  元素1  </dict>
    <dict>  元素2  </dict>
    <dict>  元素3  </dict>
</array>
    (这里的元素1、2、3都是伪代码)

     要解析这样一个数组,首先通过对应的key找到array指针:

CCArray * moveArray = static_cast<CCArray *>(dict->objectForKey("movement"));
    然后再需要使用的时候,将每个元素转成CCDictionary指针:

for ( unsigned int i = 1; i < moveArray->count(); i ++ ) {
    movementDict = static_cast<CCDictionary *>(moveArray->objectAtIndex(i));
}
     可能会有另外的疑惑:如果这里的数组保存的是基本变量,该如何处理呢?其实CCDictionary解析出来的结果都可以转换成 CCString *。所以将这里类型转换的目标替换成 CCString,然后再取intValue即可。


2)解析String:

    上面的方法,如果是写一个dict这样比较大的元素,还可以接受,但是如果要保存一个float数组,还要用array再去一个个写plist,未免有些麻烦。我们可以参考cocos源码中 CCNS 中解析 CCPoint的方法,通过保存一个 {-0.1,-0.1} 这样的字符串,然后通过代码解析,来实现在plist保存float数组的目的。

//简化版,去掉很多强壮型判断
void RoninUtils::splitWithForm(const char* pStr, const char * token, std::vector<std::string> & strs) {
    RETURN_IF_COND(pStr == NULL);

    std::string content = pStr;
    int nPosLeft  = content.find('{');
    int nPosRight = content.find('}');
    RETURN_IF_COND( nPosLeft >= nPosRight );


    content = content.substr(nPosLeft + 1, nPosRight - nPosLeft - 1);

    int nend=0;
    int nbegin=0;
    while(nend != -1)
    {
        nend = content.find(token, nbegin);
        if(nend == -1)
            strs.push_back(content.substr(nbegin, content.length()-nbegin));
        else
            strs.push_back(content.substr(nbegin, nend-nbegin));
        nbegin = nend + strlen(token);
    }
}
       基本的流程实际上就是抄 CCNs中同名函数的,不过那个函数解析的最长数组长度只有2,所以这里做了一点点小的扩展。另外由于plist是由我自己写,正确性可以保证,所以这里去掉了很多强壮型的判断语句。


      到此,这一套移动管理的体系,就算基本完工了。

猜你喜欢

转载自blog.csdn.net/ronintao/article/details/10684885
今日推荐