5.6 实战:利用动画实现人物行走
学习了这么多的动作和行为之后,最重要的还是要能够将它们组合在一起。如果说到通过组合这些内容来达到练习的目的,无疑实现RPG游戏中的某些功能模块是非常不错的选择。
常见的RPG游戏有很多,包括比较经典的《梦幻西游》、《仙剑奇侠传》以及《桃花源记OL》等,这些都很受玩家的欢迎,许多游戏开发者也是出于对这些游戏的喜爱才走上了游戏开发的道路。比如笔者就经常抓取《梦幻西游》中的某些素材来模仿供自己娱乐。当然,由于书刊在印刷时会牵扯到版权的问题,自然无法在书中使用游戏中出现了的素材。这里就拿一个简单的模型来代替,读者可以自行去游戏中抓取素材来练习使用。
在本节要实现的就是人物角色在地图中行走的内容,当然由于本章还没有接触到Cocos2d-x的地图系统,因此将仅研究人物行走的功能,对在地图上行走的功能将在今后讲解。
图5-14就是今天所要用到的素材,其中最前面的英文单词表示图片所在序列的行为,比如run就代表该图片所展示的动作是行走,而stand所代表的动作就是站立。第一位数字表示人物的方向,分别用1~8表示8个方向的姿势,而剩下的数字则表示当前画面在所在序列的第几帧。这些素材已经被打包在了文件hero.rar中。实际使用时还将被打包成plist格式,具体的方法在5.5节已经做过介绍,这里不再赘述。
图5-14 本范例所使用的素材
本范例将显示一个人物在屏幕中央,单击屏幕后人物将向触摸点走去,到达目的地后再停下,恢复到站立的姿势。
(1)在实现这些之前,首先要定义一个类Hero用来封装对人物角色进行控制的方法。对该类的定义如范例5-6所示。
【范例5-6】对类Hero的定义。
01 enum hero_direction //定义角色的8个方向 02 { 03 RIGHT_DOWN = 1, 04 LEFT_DOWN = 2, 05 LEFT_UP = 3, 06 RIGHT_UP = 4, 07 DOWN = 5, 08 LEFT = 6, 09 UP = 7, 10 RIGHT = 8 11 }; 12 class Hero :public Cocos2d::CCNode //对类Hero的定义 13 { 14 public: 15 int direction; //人物方向 16 Cocos2d::Point position; //人物坐标 17 Cocos2d::Sprite* sprite; 18 void initHeroSprite(int direction,Cocos2d::Point position); //对人物进行初始化 19 void heroMoveTo(Cocos2d::Point position); //人物移动到 20 void heroResume(); //人物恢复站立姿势 21 int getDirection(Cocos2d::Point pos1, Cocos2d::Point pos2); //获取人物前进方向 22 float getDistance(Cocos2d::Point pos1, Cocos2d::Point pos2); //获取人物前进距离 23 Cocos2d::Animate* createAnimate(int direction, const char *action, int num); //获取动画 24 CREATE_FUNC(Hero); 25 };
(2)然后再实现各个方法,如范例5-7所示。
【范例5-7】类Hero的实现。
01 #define PI 3.1415926 02 void Hero::initHeroSprite(int direction, Cocos2d::Point position) 03 { 04 this->position = position; 05 sprite = Sprite::create("stand11.png"); 06 sprite->setPosition(position); 07 addChild(sprite); 08 sprite->runAction(this->createAnimate(direction, "stand", 7)); 09 } 10 Animate* Hero::createAnimate(int direction, const char *action, int num) 11 { 12 auto* m_frameCache = SpriteFrameCache::getInstance(); 13 m_frameCache->addSpriteFramesWithFile("hero.plist", "hero. png"); 14 Vector<SpriteFrame*> frameArray; 15 for (int i = 1; i <= num; i++) 16 { 17 auto* frame = m_frameCache->getSpriteFrameByName(String:: createWithFormat( 18 "%s%d%d.png",action,direction,i)-> getCString()); 19 frameArray.pushBack(frame); 20 } 21 Animation* animation = Animation::createWithSpriteFrames (frameArray); 22 animation->setLoops(-1); 23 animation->setDelayPerUnit(0.1f); 24 return Animate::create(animation); 25 } 26 int Hero::getDirection(Cocos2d::Point pos1, Cocos2d::Point pos2) 27 { 28 float x = pos2.x - pos1.x; 29 float y = pos2.y - pos1.y; 30 float r = sqrt(x*x + y*y); 31 if (x > 0 && fabs(y / r) < sin(15 * PI / 180)) 32 { 33 CCLOG("RIGHT"); 34 this->direction = RIGHT; 35 } 36 else if (x < 0 && fabs(y / r) < sin(15 * PI / 180)) 37 { 38 CCLOG("LEFT"); 39 this->direction = LEFT; 40 } 41 else if (y > 0 && fabs(x / r) < sin(15 * PI / 180)) 42 { 43 CCLOG("UP"); 44 this->direction = UP; 45 } 46 else if (y < 0 && fabs(x / r) < sin(15 * PI / 180)) 47 { 48 CCLOG("DOWN"); 49 this->direction = DOWN; 50 } 51 else if (x > 0 && y > 0) 52 { 53 CCLOG("RIGHT-UP"); 54 this->direction = RIGHT_UP; 55 } 56 else if (x > 0 && y < 0) 57 { 58 CCLOG("RIGHT-DOWN"); 59 this->direction = RIGHT_DOWN; 60 } 61 else if (x < 0 && y < 0) 62 { 63 CCLOG("LEFT-DOWN"); 64 this->direction = LEFT_DOWN; 65 } 66 else if (x < 0 && y > 0) 67 { 68 CCLOG("LEFT-UP"); 69 this->direction = LEFT_UP; 70 } 71 return this->direction; 72 } 73 float Hero::getDistance(Cocos2d::Point pos1, Cocos2d::Point pos2) 74 { 75 float x = pos1.x - pos2.x; 76 float y = pos1.y - pos2.y; 77 return sqrt(x*x+y*y); 78 } 79 void Hero::heroMoveTo(Cocos2d::Point position) 80 { 81 sprite->stopAllActions(); 82 this->position = sprite->getPosition(); 83 float distance = getDistance(this->position, position); 84 //行走的动画 85 auto* animate = createAnimate(getDirection(this->position, position), "run", 8); 86 //移动 87 auto* move = MoveTo::create((float)distance / 50, position); 88 //动作监听器,当移动到指定位置后调用回调函数恢复站立 89 auto* callFunc = CallFunc::create(CC_CALLBACK_0(Hero::heroResume, this)); 90 auto* sequence = Sequence::create(move, callFunc, NULL); 91 sprite->runAction(animate); 92 sprite->runAction(sequence); 93 } 94 void Hero::heroResume() 95 { 96 sprite->stopAllActions(); 97 sprite->runAction(createAnimate(this->direction, "stand", 7)); 98 }
(3)再在HelloWorldScene.cpp中做出如范例5-8所示的修改。
【范例5-8】最终的实现。
01 bool HelloWorld::init() 02 { 03 if ( !Layer::init() ) 04 { 05 return false; 06 } 07 //白色背景 08 auto* background = LayerColor::create(ccc4(255, 255, 255, 255)); 09 addChild(background); 10 //创建角色 11 hero = new Hero(); 12 hero->initHeroSprite(1, Vec2(320, 180)); 13 addChild(hero); 14 //设置对触摸行为进行响应的触发器 15 auto dispatcher = Director::getInstance()->getEventDispatcher(); 16 auto listener = EventListenerTouchOneByOne::create(); 17 listener->onTouchBegan = CC_CALLBACK_2(HelloWorld::onTouchBegan, this); 18 dispatcher->addEventListenerWithSceneGraphPriority(listener, this); 19 return true; 20 } 21 bool HelloWorld::onTouchBegan(Touch *touch, Event *unused_event) 22 { //仅对点击操作进行响应 23 hero->heroMoveTo(touch->getLocation()); 24 return false; 25 }
运行程序之后的效果如图5-15所示。当玩家单击屏幕后,人物会从所在地向玩家所单击的位置行走,如图5-16所示。当人物最终行走到目的地,就会停止行走,如图5-17所示。当然也可以再单击屏幕其他位置让人物折返回来,或者去其他方向运动,如图5-18所示。
图5-15 程序开始运行,人物站在屏幕中央
图5-16 单击屏幕人物开始行走
图5-17 行走到目标位置后角色又恢复站立姿势
图5-18 人物向其他方向运动
下面来介绍一下实现思路。首先看到范例5-8中的第10~12行,在此处将一个Hero类型的对象加入到了场景中。然后在15~19行中定义了触发器,当用户单击屏幕时将会调用响应函数onTouchBegan(范例20~25行)。其中的内容为使用Hero类的heroMoveTo方法让人物行走到目标坐标。
这时就可以看到范例5-7的79~93行,这里定义了heroMoveTo的内容。即先停止人物身上做带有的全部动画和行为,然后给它附加一个行走的动画和移动的动作。同时,为移动的动作加入一个触发器,当移动到目标位置后调用触发函数heroResume使人物恢复到站立状态。
这其中需要用到方法getDistance和getDirection。getDirection用于获取代表人物方向的整数,其内容已经在范例5-6的第1~11行用一个枚举类被定义。getDistance用于获取目标位置与人物当前位置的距离,因为在创建使人物移动的动作时需要运动时间作为参数,为了防止人物运动时快时慢的现象发生,就会使用该方法来保证人物的运动大概是匀速的。
再看看范例5-7的第26~72行,这部分代码的作用是返回人物运动的方向,该方法有两个参数,分别代表人物当前的坐标和运动的目标位置坐标。这样就可以通过对应的x、y坐标的差值以及对应的直角三角形的弦来获取所要行走的方向与正方向(水平向右)的余弦值或正弦值。然后可以用这个值与15度的余弦值或者正弦值进行比较,再配合方向是向上还是向下(即y的正负)就可以得到最终的方向,比如当人物方向为向右行走,就可能是如图5-19所示的情况。
图5-19 当人物方向为向右行走
至于其他的内容就靠读者自行去阅读代码来理解了,要查看完整项目可以参考源文件当前目录下的项目ChapterFive06。
其实,在这个demo中还有一个很严重的缺陷,就是当玩家在同方向连续单击屏幕时,人物就会出现一种很奇怪的跛脚的状态。这是由于每次单击在同方向都要重新对运动动画进行重置导致的,具体的解决方法将在下一章中给出。