怪物在地图上的位置是不变的,但它们都有对应的原地站立时的动画。大家可能想到使用前面创建的动画管理器来创建动画模板,然后播放各个怪物精灵的动画。这样做没错,但是想想就觉得麻烦:如果有100种怪物的话,难道要定义100个动画模板吗?答案是否定的。那么该如何实现怪物原地站立的动画呢?从提供的怪物图片素材可以发现:每个怪物的动作由4帧动画组成,且每帧尺寸一致,都正好是一个图块的大小。是否可以定时地更新怪物对应的图块,从而产生动画的效果?结论是可以。cocos2d-x工程下test项目中的TMXReadWriteTest例子演示了如何动态改变TileMap上的图块,以及用定时器不停地重复更新图块的工作。关键代码如下:
//创建定时器,反复更新图块
schedule(schedule_selector(TMXReadWriteTest::repaintWithGID), 2.0f);
void TMXReadWriteTest::repaintWithGID(float dt)
{
//获取TileMap地图对象
CCTMXTiledMap* map = (CCTMXTiledMap*)getChildByTag(kTagTileMap);
//获取第0层
CCTMXLayer *layer = (CCTMXLayer*)map->getChildByTag(0);
CCSize s = layer->getLayerSize();
//遍历一行
for( int x=0; x<s.width;x++)
{
//倒数第二行
int y = (int)s.height-1;
//获取指定位置当前的图块ID
unsigned int tmpgid = layer->tileGIDAt( ccp((float)x, (float)y) );
//更新指定位置的图块ID
layer->setTileGID(tmpgid+1, ccp((float)x, (float)y));
}
}
明白了上面的代码我们就对实现怪物动画有清晰的思路了:创建一个定时器,在定时器中遍历所有怪物,令其图块ID加一,如果动画完成,则将图块ID置回初始的数值。
下面请大家思考一下如何遍历所有怪物呢?由于遍历方法需要在定时器中调用,就必须尽可能优化其速度。TestCpp中的例子使用了二维数组的遍历。但设想一下,如果地图很大,遍历起来会很耗时。另外,怪物的分布是稀疏的,二维数组中的大部分元素都有可能为空。所以直接在定时器中遍历地图上的每个格子是不明智的。
那么我们可以在地图初始化完毕后,先做一个预处理:将每个怪物的方位,初始时的图块ID存放到一个数组中。然后在定时器中遍历这个数组就可以了。
首先创建一个Enemy类,用于存放怪物的方位和初始的图块ID,将来还会存放怪物的属性等信息:
#ifndef __ENEMY_H__
#define __ENEMY_H__
#include "cocos2d.h"
using namespace cocos2d;
class Enemy:public CCObject
{
public:
Enemy(void);
~Enemy(void);
//怪物在TileMap上的方位
CCPoint position;
//怪物的图块ID
int startGID;
};
#endif
然后修改GameMap的初始化方法,遍历TileMap上的所有怪物,生成Enemy对象,添加到一个CCArray中。随后启动一个定时器,每隔0.2s更新一次怪物动画。首先在GameMap.h里面声明一个变量,即怪物层,添加代码“CC_PROPERTY_READONLY(CCTMXLayer*,enemyLayer,EnemyLayer);”,然后在“protected:”下面添加需要用到的数组和方法,代码如下:
//Enemy数组
CCArray* enemyArray;
//更新怪物图块
void updateEnemyAnimation(float dt);
接下来在GameMap.cpp文件里面实现怪物层的getter方法:
//返回怪物层
CCTMXLayer *GameMap::getEnemyLayer()
{
return enemyLayer;
}
现在我们可以修改初始化方法了,修改后的代码如下:
//TiledMap额外的初始化方法
void GameMap::extraInit()
{
//开启各个图层的纹理抗锯齿
enableAnitiAliasForEachLayer();
//初始化地板层和墙壁层对象
floorLayer=this->layerNamed("floor");
wallLayer=this->layerNamed("wall");
//获取怪物层
enemyLayer=this->layerNamed("enemy");
CCSize s=enemyLayer->getLayerSize();
enemyArray=CCArray::create();
//遍历enemy层,将存在的怪物保存到数组中
for(int x=0;x<s.width;x++){
for(int y=0;y<s.height;y++){
int gid=enemyLayer->tileGIDAt(ccp(x,y));
if(gid!=0){
Enemy* enemy=new Enemy();
//保存怪物坐标
enemy->position=ccp(x,y);
//保存怪物起始的图块ID
enemy->startGID=gid;
//添加怪物对象到数组
enemyArray->addObject(enemy);
}
}
}
//别忘了retain()
enemyArray->retain();
//用于更新敌人动画
schedule(schedule_selector(GameMap::updateEnemyAnimation),0.2f);
}
在updateEnemyAnimation中,需要遍历enemyArray,并计算下一帧对应的图块ID,具体代码如下:
//更新怪物的图块
void GameMap::updateEnemyAnimation(float dt)
{
//遍历保存所有怪物对象的数组
CCObject* pObject;
Enemy* enemy;
CCARRAY_FOREACH(enemyArray,pObject){
enemy=(Enemy*)pObject;
if(enemy!=NULL){
//获取怪物当前的图块ID
int gid=enemyLayer->tileGIDAt(enemy->position);
gid++;
//如果结束,设置为起始图块ID
if(gid-enemy->startGID>3){
gid=enemy->startGID;
}
//给怪物设置新的图块
enemyLayer->setTileGID(gid,enemy->position);
}else{
break;
}
}
}
还有,别忘了写Enemy.cpp文件,它要对.h里面声明的构造函数和析构函数进行实现,代码如下:
#include "Enemy.h"
Enemy::Enemy(void)
{
}
Enemy::~Enemy(void)
{
}
现在我们编译运行,发现地图上的怪物们都动起来了。注意,在GameMap的析构方法里要释放enemyArray和定时器:
//析构函数
GameMap::~GameMap(void)
{
this->unscheduleAllSelectors();
enemyArray->release();
}