上一章中,我们制作并添加了游戏地图。这一章我们要把英雄添加到地图上去并让他动起来,还有上一章没有用到的Meta层也要用上。
首先选好英雄的图片,起码要有前后两个方向和左右方向中的任意一个。因为左右方向只要有任意一个方向镜面翻转后就可以得到另一方向了,cocos2d的sprite只要调用自身的一个setFlippedX(true)方法。当然你也可以准备两个方向的图片。
新建类MagicHero,使其继承于CCNode,这样一会儿我们就可以添加到地图上去了。考虑到英雄就一个,这里我使用了单例模式,MagicHero.h代码如下:
#ifndef __HERO__H_ #define __HERO__H_ #include "cocos2d.h" #define _BLOCK_SIZE_ 32 class MagicHero : public cocos2d::CCNode { public: ~MagicHero(); static MagicHero* getHeroInstance(); const static int UP = 0; const static int DOWN = 1; const static int LEFT = 2; const static int RIGHT = 3; //移动一格,返回一个移动位移偏移量 cocos2d::Vec2 moveHeroStep(); //设置英雄方向 void setHeroDirect(int dir); int getHeroDirect(); //英雄移动相关操作 void incIsMove(); void decIsMove(); void setIsMoveToZero(); bool isHeroMove(); //获取英雄地图坐标 cocos2d::Point getHeroMapPosition(); //英雄移动动画 void idleHero(); void runAnimation(); void stopHero(); private: MagicHero(); int direct;//英雄当前面朝方向 int pRoom;//所在房间 int isMove;//英雄是否在移动 //英雄单例 static MagicHero* heroInstance; cocos2d::Sprite *heroSprite;//英雄精灵 //初始化英雄animation相关Frame void initSpriteAndAnimation(); //英雄动作 cocos2d::Action *moveAction; char* idel[4]; class Garbo { public: Garbo(); ~Garbo() { if (heroInstance != NULL) { delete MagicHero::heroInstance; } } }; //垃圾回收器——静态变量会被回收 static Garbo garbo; }; #endif
因为在析构函数中无法释放单例指针heroInstance,所以MagicHero类内添加了一个垃圾回收器类——Garbo。这是利用静态成员变量会被回收的特性,Garbo的析构函数被调用时就可以删除单例指针了。调用方法getHeroInstance()就可以获取英雄单例了。MagciHero类是用于移动,动画等相关功能的,英雄的相关数值(如HP,EXP等)会在另一个类里面实现。
这里面有一个IsMove的int型的变量,这个变量我需要解释一下,就是用来标记当前按下的方向移动键有多少个,按下一个它就加1,松开一个它就减一,为0就不动了。这是为了游戏按键走动式体验更友好而改动的。
其他的函数应该也很好理解,无非就是一些对外的接口函数和内部的初始化函数,还有一些用于记录信息的变量,我就不逐一解释了,哦对了,_BLOCK_SIZE_是方块大小。接下来就是实现功能部分了。贴上MagicHero.cpp的代码:
#include "MagicHero.h" USING_NS_CC; MagicHero::MagicHero() { this->direct = MagicHero::DOWN;//英雄当前面朝方向 this->pRoom = 1;//所在房间 this->isMove = 0; CCNode::init(); initSpriteAndAnimation(); } void MagicHero::idleHero() { this->removeChild(heroSprite, true); char path[100] = {}; sprintf_s(path, "%s1.png", idel[direct]); auto s = CCSprite::create(path); s->setAnchorPoint(Vec2(0.3, 0)); this->addChild(s); heroSprite = s; } MagicHero::~MagicHero() { if (heroSprite != NULL) { heroSprite->release(); heroSprite = NULL; } if (moveAction != NULL) { moveAction->release(); moveAction = NULL; } } MagicHero* MagicHero::heroInstance = NULL; MagicHero * MagicHero::getHeroInstance() { if (MagicHero::heroInstance == NULL) { MagicHero::heroInstance = new MagicHero(); } return MagicHero::heroInstance; } Vec2 MagicHero::moveHeroStep() { Vec2 v(0,0); switch (direct) { case MagicHero::UP: setPositionY(getPosition().y + _BLOCK_SIZE_); v.y = _BLOCK_SIZE_; break; case MagicHero::DOWN: setPositionY(getPosition().y - _BLOCK_SIZE_); v.y = -_BLOCK_SIZE_; break; case MagicHero::RIGHT: v.x = _BLOCK_SIZE_; setPositionX(getPosition().x + _BLOCK_SIZE_); break; case MagicHero::LEFT: v.x = -_BLOCK_SIZE_; setPositionX(getPosition().x - _BLOCK_SIZE_); break; default: break; } return v; } void MagicHero::initSpriteAndAnimation() { idel[0] = "hero/u"; idel[1] = "hero/d"; idel[2] = "hero/l"; idel[3] = "hero/r"; //将图片添加到缓存中去 char path[100] = {}; for (int i = 0; i < 4; i++) { for (int j = 1; j < 4; j++) { sprintf_s(path, "%s%d.png", idel[i], j); SpriteFrame *sf = SpriteFrame::create(path, Rect(0, 0, 72, 55)); SpriteFrameCache::getInstance()->addSpriteFrame(sf, path); } } idleHero(); this->moveAction = NULL; } void MagicHero::setHeroDirect(int dir) { this->direct = dir; } void MagicHero::runAnimation() { Vector<SpriteFrame*> allFrames; char path[100] = {}; for (int i = 1; i < 4; i++) { sprintf_s(path, "%s%d.png", idel[direct], i); SpriteFrame *sf = SpriteFrameCache::getInstance()->getSpriteFrameByName(path); allFrames.pushBack(sf); } Animation* ani = Animation::createWithSpriteFrames(allFrames, 0.15); moveAction = heroSprite->runAction(RepeatForever::create(Animate::create(ani))); } bool MagicHero::isHeroMove() { return isMove > 0 ? true : false; } Point MagicHero::getHeroMapPosition() { return Point(getPositionX()/_BLOCK_SIZE_, getPositionY()/_BLOCK_SIZE_); } void MagicHero::stopHero() { heroSprite->stopAction(moveAction); idleHero(); } void MagicHero::incIsMove() { isMove++; } void MagicHero::decIsMove() { isMove--; } void MagicHero::setIsMoveToZero() { isMove = 0; idleHero(); } int MagicHero::getHeroDirect() { return direct; }
SpriteFrameCache::getInstance()->getSpriteFrameByName(path);
这是因为移动是一个很频繁的操作,放到缓冲池里面,就可以避免反复的去读文件了。这里偷了个懒,直接把路径名作为Frame的名字给传进去了,大家不要误会。
这里因为资源问题,用的都是小图(如下),所以要一个一个的读进去。推荐不要这样,最好是做成一个大的合图,用TexturePacker软件(使用教程网上有很多,)感兴趣的自己查一下就可以了就可以把这些小的图片合成一个大的图片,并且还会自动生成一个同名的plist文件,它是一个xml文件,里面记录的是各个小图合成在大图里面的位置,使用方法如下即可添加到缓存池中去,取出来用还是一样的。
SpriteFrameCache::getInstance()->addSpriteFramesWithFile(plist文件路径)
至于为什么要做成大图呢,且听我慢慢道来。这涉及到OpenGL ES的渲染方式,OpenGL ES将一张图片载入内存时,要求长和宽都是2的n次幂。举个极端的例子,如果你的图片恰好是513*513,它为了补全会自动变成1024*1024。这一下浪费的内存是(1024*1024-513*513)*4 = 3M,因为是4通道所以要乘以4。一张图片就浪费3M内存,这样是不是太不好了。而做成大图可以把小图拼起来,是长和宽尽量接近于2的n次幂,能够很好的利用内存。
接下来就是主场景中将人物添加进去,并且与地图的碰撞结合起来移动了。修改主场景中的init方法代码如下:
bool GameMainScene::init() { if (!Layer::init()) { return false; } //初始化计数 this->moveCount = 9; //记录窗口大小 this->winHeight = Director::getInstance()->getWinSize().height; this->winWidth = Director::getInstance()->getWinSize().width; //生成瓦片地图对象并设置初始位置 this->myTMXTiledMap = CCTMXTiledMap::create("room/room01.tmx"); this->myTMXTiledMap->setAnchorPoint(Vec2(0, 0)); this->myTMXTiledMap->setPosition(Vec2(winWidth / 2 - myTMXTiledMap->getMapSize().width*_BLOCK_SIZE_ / 2, winHeight / 2)); this->addChild(myTMXTiledMap, 0); //初始化地图相关成员变量 this->barrierLayer = this->myTMXTiledMap->layerNamed("Meta"); barrierLayer->setVisible(false); this->mapHeight = myTMXTiledMap->getMapSize().height; this->mapWidth = myTMXTiledMap->getMapSize().width; //生成英雄对象,绑定到地图上对应的object位置 this->myMagicHero = MagicHero::getHeroInstance(); this->myMagicHero->setAnchorPoint(Vec2(0, 0)); this->objects = myTMXTiledMap->getObjectGroup("obj"); auto heroPoint = objects->getObject("HERO"); int x = heroPoint["x"].asInt(); int y = heroPoint["y"].asInt(); myMagicHero->setPosition(Vec2(x, y)); myTMXTiledMap->addChild(myMagicHero, 2); //将MainScene的键盘监听函数注册监听器,并绑定到magicHero上 auto keyListener = EventListenerKeyboard::create(); keyListener->onKeyPressed = CC_CALLBACK_2(GameMainScene::onKeyPressed, this); keyListener->onKeyReleased = CC_CALLBACK_2(GameMainScene::onKeyReleased, this); _eventDispatcher->addEventListenerWithSceneGraphPriority(keyListener, myMagicHero); this->scheduleUpdate(); return true; }
然后就是移动函数、监听函数和判断函数,代码有点多:
void GameMainScene::update(float delta) { if (myMagicHero->isHeroMove()) { moveCount++; if (moveCount >= 10 && isHeroMoveAvailable()) { moveCount = 0; Vec2 v = myMagicHero->moveHeroStep(); myTMXTiledMap->setPosition(myTMXTiledMap->getPosition() - v); //printf("hero position x:%f , y:%f\n", magicHero->getPositionX(), magicHero->getPositionY()); } //加载词典,放在英雄移动函数中,只运行一次。(不放在init函数中是为了提高加载速度) if (!isLoadingMXL) { printf("size of xml is %d \n", sizeof(monsters)); monsters = FileUtils::getInstance()->getValueVectorFromFile("data/monster.plist"); isLoadingMXL = true; printf("size of xml is %d \n", sizeof(monsters)); } } } void GameMainScene::onKeyPressed(EventKeyboard::KeyCode keycode, Event* event) { int dir = -1; int exdir = myMagicHero->getHeroDirect(); switch (keycode) { case EventKeyboard::KeyCode::KEY_UP_ARROW: //hero move up dir = MagicHero::UP; //magicHero break; case EventKeyboard::KeyCode::KEY_DOWN_ARROW: //hero move down dir = MagicHero::DOWN; break; case EventKeyboard::KeyCode::KEY_LEFT_ARROW: //hero move left dir = MagicHero::LEFT; break; case EventKeyboard::KeyCode::KEY_RIGHT_ARROW: //hero move right dir = MagicHero::RIGHT; break; default: break; } if (dir != -1) {//判断是否按下了有效键 //如果不是第一次按下键,就停止之前的运动 if (myMagicHero->isHeroMove()) { myMagicHero->stopHero(); } this->myMagicHero->setHeroDirect(dir); this->myMagicHero->runAnimation(); this->myMagicHero->incIsMove(); } } void GameMainScene::onKeyReleased(EventKeyboard::KeyCode keycode, Event* event) { if (keycode == EventKeyboard::KeyCode::KEY_UP_ARROW || keycode == EventKeyboard::KeyCode::KEY_DOWN_ARROW || keycode == EventKeyboard::KeyCode::KEY_RIGHT_ARROW || keycode == EventKeyboard::KeyCode::KEY_LEFT_ARROW) { if (this->myMagicHero->isHeroMove()) { this->myMagicHero->decIsMove(); if (!myMagicHero->isHeroMove()) { moveCount = 9; myMagicHero->stopHero(); } } } } bool GameMainScene::isHeroMoveAvailable() { Vec2 deltaPos = getDeltaPostion(); if (deltaPos.x == -1 && deltaPos.y == -1) { return false; } //如果该块没有碰撞,就可以行走 return barrierLayer->tileAt(deltaPos) == 0? true : false; } cocos2d::Vec2 GameMainScene::getDeltaPostion() { Vec2 deltaPos(myMagicHero->getHeroMapPosition().x, 20 - myMagicHero->getHeroMapPosition().y); switch (myMagicHero->getHeroDirect()) { case MagicHero::UP: deltaPos.y--; if (deltaPos.y < 0) deltaPos = Vec2(-1, -1); break; case MagicHero::DOWN: deltaPos.y++; if (deltaPos.y >= this->mapHeight) deltaPos = Vec2(-1, -1); break; case MagicHero::LEFT: deltaPos.x--; if (deltaPos.x < 0) deltaPos = Vec2(-1, -1); break; case MagicHero::RIGHT: deltaPos.x++; if (deltaPos.x >= this->mapWidth) deltaPos = Vec2(-1, -1); break; } return deltaPos; }上面的函数都很简单,就不逐一解释了,其中getDeltaPosition()是用于获得下一个将要到达的地方的坐标,这是根据MagicHero的当前方向来计算的。
MoveCount是用于平滑移动的一个变量,因为一开始设置了帧率为60,所以update函数一秒钟执行60次,如果按下移动键一秒计算60次移动就太快了,所以设置为moveCount大于或等于10才计算,每次update函数都moveCount++,进行计算就清零。
有一句代码需要单独解释一下:
<span style="white-space:pre"> </span>return barrierLayer->tileAt(deltaPos) == 0? true : false;barrierLayer是在init函数中就读出来的Meta层,是个CCTMXLayer类型的指针,通过tileAt方法可以判断目标位置是否有瓦片的存在,如果有就不为0,就说明不可以行走,是碰撞体积。还有这个坐标是对应的TileMap坐标,与cocos2d的坐标不同,它是左上角为(0,0),这一点要注意,我一开始也被坑了。
好了,代码到此就完成了,下面看看效果吧:
恩,跑动的动画和碰撞的检测都很正常。这一章就到此结束,下一章将为你的英雄添加状态栏等数值相关并且添加战斗功能啦~