cocos2d-x: v3.0-alpha-pre
Windows环境: Windows8 + Visual Studio 2012
Linux环境: Ubuntu12.04 + gcc 4.7.2
Android环境: Android Studio v0.1 + Mi2 + MIUIv5
老早知道cocos2d-x出3.0的预览版了,据说变化很大(更牛x了),但对于我这个初学者而言依然努力保持一颗蛋定的心,目前手上拥有的是2.0.3的版本并已经完成了PompaDroid
(在英文教程中名为《PompaDroid》不过网上有人发布了的应用叫旋风小子)的开发和适配工作,而那也是一个月之前的事了。。。。今天脑子一热跑去cocos2d-x的官网上了解了一下3.0alpha版的内容,果然很牛逼,于是果断下载并彻头彻尾的改造了手上的《PompaDroid》代码,一来为了熟悉cocos2d-x3.0,二来巩固一下之前的学习并做个学习笔记,温故而知新嘛~
先来看个之前做出来的效果:
闲聊一下,其实这次3.0版本吸引我的地方除了各种性能提升的传说之外,主要有两点:
-
取消了各种类、宏的CC前缀,例如CCSprite变为了Sprite,老实说我很不喜欢之前那种类似匈牙利的命名方式,总觉得既然有cocos2d这个namespace了干嘛还要画蛇添足的在每个类前面加个CC呢?我不知道是不是cocos2d-iPhone的传统(懒得研究),但真心不喜欢这个CC前缀。
-
加入了2.5D的支持,这个纯属个人喜好,因为在我脑子了已经勾勒出了一两个游戏,而且都是2.5d才能更好的表达游戏的效果(3d人物+2d场景的效果很炫,目前正苦逼的练习手绘+photoshop)
扯远了,进入正题,横版格斗游戏《PompaDroid》的制作过程
新建工程
使用create-multi-platform-projects.py
纳尼!!!之前的install-templates-msvc.bat
呢,没有模版怎么在vs里新建工程??查阅了一番文档后发现其实从2.1.4开始,所有cocos2dx项目都用create-multi-platform-projects.py脚本来创建了(唉,落伍啊)。于是乎:
$ python create-multi-platform-projects.py -p PompaDroid -k cn.philon.pompadroid -l cpp
完成之后得到这样一个目录:
Classes | 用于存放游戏的各种类各种实现各种算法(总之我想说的是.h和.cpp) |
proj.xxx | 即是各个平台的相应工程项目 |
Resources | 游戏中所用到的资源全在这里 |
Wonderful!!我很喜欢这样的目录结构,简单明了。
接下来先从Windows平台开始吧,进入proj.win32并打开PompaDroid.sln
在解决方案中有6个项目:
PompaDroid | 很明显,需要我们实现的游戏项目 |
libBox2D | 物理引擎(这里用不到,不管它) |
libchipmunk | 貌似和box2d一样是物理引擎 |
libcocos2d | 这个就不用解释了 |
libCocosDenshion | 音频引擎(我没在游戏里加入音效) |
libExtensions | 扩展库,具体参考这里 |
依照惯例,cocos2dx新工程里会有个HelloWorld实例,展开cyclone-kid\Classes就可以看到了,果断编译运行,一切ok!
好了,删除HelloWorld.h和HelloWorld.cpp文件,开始实现CycloneKid的代码吧!
首先点击这里下载所需的美术资源,解压放到Resources目录下。
GameScene
一款游戏中会有很多个场景(Scene):主菜单、游戏、游戏结束、过场动画等等,由于只是学习,我仅做了主游戏场景(GameScene),该场景包涵两个图层(Layer):游戏层和操作层。
顾名思义,游戏层主要负责游戏的内容(地图的渲染,各个精灵的调度等);而操作层负责响应触摸(前进、后退、攻击等操作)。
因此根据上图所示,我们需要建立三个类,GameScene
GameLayer
OptionLayer
务必把类文件建立在Classes目录下,方便之后夸平台编译
GameScene.h
class GameScene : public cocos2d::Scene { public: GameScene(); ~GameScene(); virtual bool init(); CREATE_FUNC(GameScene); CC_SYNTHESIZE(GameLayer*, _gameLayer, GameLayer); CC_SYNTHESIZE(OptionLayer*, _optionLayer, OptionLayer); };
GameLayer.h
class GameLayer : public cocos2d::Layer { public: GameLayer(); ~GameLayer(); virtual bool init(); CREATE_FUNC(GameLayer); };
OptionLayer.h
class OptionLayer : public cocos2d::Layer { public: OptionLayer(); ~OptionLayer(); virtual bool init(); CREATE_FUNC(OptionLayer); };
1. GameLayer
GameLayer主要有背景地图(map)
、主角(hero)
、机器人若干(robots)
人组成。
1.1 瓦片地图
在Resources/pd_tilemap.tmx
便是背景地图资源,tile即瓦片的意思,整个背景地图有若干个32*32像素的图片组成,像瓦片一样,cocos2dx是支持.tmx文件的,因此在GameLayer.h文件中声明一个私有变量_map
,用于游戏的背景地图。
GameLayer.h
private: cocos2d::TMXTileMap *_map;
新建GameLayer.cpp文件,实现游戏层
GameLayer.cpp
using namespace cocos2d; GameLayer::GameLayer() {} GameLayer::~GameLayer() {} bool GameLayer::init() { bool ret = false; do { CC_BREAK_IF(!Layer::init()); _map = TMXTiledMap::create("pd_tilemap.tmx"); this->addChild(_map); ret = true; } while(0); return ret; }
游戏层已经有地图了,现在只差把游戏层添加到场景中,新建GameScene.cpp,实现游戏场景的内容:
GameScene.cpp
GameScene::GameScene() { } GameScene::~GameScene() { } bool GameScene::init() { bool ret = false; do { CC_BREAK_IF(!Scene::init()); // 游戏层初始化 _gameLayer = GameLayer::create(); this->addChild(_gameLayer, 0); ret = true; } while (0); return ret; }
编译并运行一下!
在编译中我的环境会出现两个警告:
-
现默认库“LIBCMT”与其他库的使用冲突;请使用 /NODEFAULTLIB:library
在属性-->配置属性-->链接器-->输入-->忽略特定库:增加LIBCMT即可; -
忽略“/EDITANDCONTINUE”(由于“/SAFESEH”规范)
在属性-->配置属性-->链接器-->高级-->映像具有安全异常处理程序:改为“否”即可。
1.2 动作精灵
主角和机器人的资源包涵在pd_sprites.plist
与pd_sprites.pvr.ccz
当中,打开pd_sprites.plist文件可以看到很多hero_xx_xx.png
和robot_xx_xx.png
,这些便是主角英雄和机器人的动作序列图片。从plist文件中可以清楚的看到,不论是“hero”还是“robot”都具备了idle
空闲、attack
攻击、walk
行走、knockout
(被)击倒、hurt
受伤这五个动作,因此需要建立一个动作类ActionSprite
来统一实现动作播放。
如上图所示,我们需要新建三个类ActionSprite
、Hero
、Robot
,其中ActionSprite既然是“动作精灵”所以需要继承cocos2d的Sprite类。
原本我想ActionSprite类只负责调用精灵每个动作的动画,精灵的“攻击力”、“生命值”、“移动”等属性方法放到另一个新的类当中管理,但为了省事我还是决定把这些内容完全塞到ActionSprite当中(尽管我知道这是一种灾难,但是。。。让bug来得更猛烈些吧!谁让我懒呢)。
首先,新建三个类ActionSprite
、Hero
、Robot
ActionSprite.h
// 根据pd_sprites.plist得到,动作精灵有五种状态 typedef enum { ACTION_STATE_NONE = 0, ACTION_STATE_IDLE, ACTION_STATE_WALK, ACTION_STATE_ATTACK, ACTION_STATE_HURT, ACTION_STATE_KNOCKOUT, } ActionState; class ActionSprite : public cocos2d::Sprite { public: ActionSprite(); ~ActionSprite(); void idle(); void walk(cocos2d::Point direction); void attack(); void hurt(int damage); void knockout(); // 定义每个状态动作的get/set方法 CC_SYNTHESIZE_RETAIN(cocos2d::Action*, _idleAction, IdleAction); CC_SYNTHESIZE_RETAIN(cocos2d::Action*, _attackAction, AttackAction); CC_SYNTHESIZE_RETAIN(cocos2d::Action*, _hurtAction, HurtAction); CC_SYNTHESIZE_RETAIN(cocos2d::Action*, _knockoutAction, KnockoutAction); CC_SYNTHESIZE_RETAIN(cocos2d::Action*, _walkAction, WalkAction); // 精灵的当前状态 CC_SYNTHESIZE(ActionState, _currentState, ActionState); CC_SYNTHESIZE(float, _velocity, Velocity); // 移动速度 CC_SYNTHESIZE(cocos2d::Point, _direction, Direction); // 移动方向(向量) CC_SYNTHESIZE(unsigned int, _hp, HP); // 生命值 CC_SYNTHESIZE(unsigned int, _atk, ATK); // 攻击力 protected: // 定义一个创建状态动画的方法 // fmt - 状态的图片名格式(查看pd_sprites.plist,每种状态格式都类似hero_idle_xx.png) // count - 状态图片序列的数量 // fps - 动画的播放帧率 static cocos2d::Animation *createAnimation(const char *fmt, int count, float fps); private: // 切换演员的当前状态 bool _changeState(ActionState state); };
然后是具体ActionSprite的实现,要注意动作切换时的基本逻辑关系,例如某个角色已经挂了,就不能在执行其他动作了!还有一点要非常小心,尤其是用惯了cocos2d-x以前的版本,SpriteFrameCache::sharedSpriteFrameCache
这个函数已经不能用了(尽管编译时能通过)如果沿用以前的代码,会有大麻烦!!所以用SpriteFrameCache::getInstance()
代替。ccp
这个坐标宏也不能用了,我现在用Point(x, y)
代替。
- ActionSprite.cpp ```cpp ActionSprite::ActionSprite() { _idleAction = NULL; _walkAction = NULL; _attackAction = NULL; _hurtAction = NULL; _knockoutAction = NULL; }
ActionSprite::~ActionSprite()
{}
void ActionSprite::idle()
{
if (_changeState(ACTION_STATE_IDLE)) {
runAction(_idleAction);
_velocity = Point(0, 0);
}
}
void ActionSprite::attack()
{
if (_changeState(ACTION_STATE_ATTACK)) {
runAction(_attackAction);
}
}
void ActionSprite::walk(Point direction)
{
if (_changeState(ACTION_STATE_WALK)) {
runAction(_walkAction);
_direction = direction;
// 根据精灵的x向量,来判断精灵的正面“朝向”
_direction.x > 0 ? setFlipX(true) : setFlipX(false);
}
}
void ActionSprite::hurt(int damage)
{
if (_changeState(ACTION_STATE_HURT)) {
runAction(_hurtAction);
_hp -= damage;
if (_hp <= 0) {
knockout();
}
}
}
void ActionSprite::knockout()
{
if (_changeState(ACTION_STATE_KNOCKOUT)) {
runAction(_knockoutAction);
}
}
bool ActionSprite::_changeState(ActionState state)
{
bool ret = false;
// 精灵已经被击倒(Game Over),就不能再出发其他动作了!
if (_currentState == ACTION_STATE_KNOCKOUT) {
goto change_state_failed;
}
// 精灵已经处于要改变的状态,就没必要在改变了!
if (_currentState == state) {
goto change_state_failed;
}
// 改变动作之前,先停止所有动作
this->stopAllActions();
_currentState = state;
ret = true;
change_state_failed:
return ret;
}
Animation *ActionSprite::createAnimation(const char *fmt, int count, float fps)
{
Array *frames = Array::createWithCapacity(count);
int i = 0;
for (i = 0; i < count; i++) {
const char *png = String::createWithFormat(fmt, i)->getCString();
SpriteFrame *frame = SpriteFrameCache::getInstance()->getSpriteFrameByName(png);
frames->addObject(frame);
}
return Animation::createWithSpriteFrames(frames, 1 / fps);
}
### 1.2.1 英雄
![] [pic-hero-actions]
Hero部分相对简单,只需要把具体的动作图片序列的加载部分实现,以及“攻击力”啊、“生命值”什么的随便写一个(但是别写负数哦,一个英雄的生命为负。。。这是要闹哪样!)。
不过同样有一点需要注意,我在写动作回调函数的时候发现`CallFunc::create(obj, selector)`这个函数也被废弃了,改用c++11特性的`CallFunc::create(std::function<void()> &cb)`,之前没怎么接触c++11,这个改动把我坑惨了(主要是一开始不知道怎么使用)!!
- Hero.h
```cpp
class Hero : public ActionSprite
{
public:
Hero();
~Hero();
bool init();
CREATE_FUNC(Hero);
};
- Hero.cpp
Hero::Hero() {} Hero::~Hero() {} bool Hero::init() { bool ret = false; do { CC_BREAK_IF(!ActionSprite::initWithSpriteFrameName("hero_idle_00.png")); // 之前的CallFunc::create(this, callfunc_selector(Hero::idle))已经被废弃额 // 改用c++11特性来回调 CallFunc *callbackIdle = CallFunc::create(std::bind(&Hero::idle, this)); // 创建idle(空闲)动画,调用后反复播放 Animation *idle = createAnimation("hero_idle_%02d.png", 6, 12); setIdleAction(RepeatForever::create(Animate::create(idle))); // 创建walk(行走)动画,同样调用后反复播放 Animation *walk = createAnimation("hero_walk_%02d.png", 7, 24); setWalkAction(RepeatForever::create(Animate::create(walk))); // 创建attact(攻击)动画,播放后回调到idle动画 Animation *attack = createAnimation("hero_attack_00_%02d.png", 3, 12); setAttackAction(Sequence::create(Animate::create(attack), callbackIdle, NULL)); // 创建hurt(受伤)动画,播放后回调到idle动画 Animation *hurt = createAnimation("hero_hurt_%02d.png", 3, 12); setHurtAction(Sequence::create(Animate::create(hurt), callbackIdle, NULL)); // 创建knockout(被击倒)动画,播放后不做任何调用 Animation *knockout = createAnimation("hero_knockout_%02d.png", 5, 12); setKnockoutAction(Sequence::create(Animate::create(knockout), NULL)); setATK(20); // 攻击力 setHP(100); // 生命值 setVelocity(1); // 移动速度 setDirection(Point::ZERO); // 移动方向 ret = true; } while (0); return ret; }
Hero
部分的代码基本实现,已经迫不及待想要看看成效了,其实现在万事俱备只欠东风,无非就是在GameLayer
中增加hero的调用即可。因此:
- 尽管之前一直在调用plist文件中的png图片资源,不过实际上它还没被游戏加载进来,因此添加动作图片序列的资源pd_sprites.plist和pd_sprites.pvr.ccz
- 在GameLayer.h中声明hero和robots等精灵角色(robots是多个,所以声明为数组)
- 后期除了hero外还有无数个robots,为了提高渲染效率,这里需要增加一个SpriteBatchNode对象
_actors
(演员列表),把所有动作精灵都放到这个_actors中进行批量渲染。 - 最后把hero对象create出来,编译运行!
GameLayer.h(增加三个私有成员变量):
private:
ActionSprite *_hero;
cocos2d::Array *robots;
cocos2d::SpriteBatchNode *_actors;
GameLayer.cpp中的init做如下改变:
bool GameLayer::init() { bool ret = false; do { CC_BREAK_IF(!Layer::init()); _map = TMXTiledMap::create("pd_tilemap.tmx"); this->addChild(_map); // 加载精灵的动作图片序列资源 // 注意,2.x版本中的sharedSpriteFrameCache以弃用!!! SpriteFrameCache::getInstance()->addSpriteFramesWithFile("pd_sprites.plist"); _actors = SpriteBatchNode::create("pd_sprites.pvr.ccz"); this->addChild(_actors); _hero = Hero::create(); _hero->setPosition(Point(80, 80)); _hero->idle(); _actors->addChild(_hero); ret = true; } while(0); return ret; }
好了,hero终于出现了!
1.2.2 机器人
机器人与hero一样是动作精灵,不考虑AI(人工智能,之后实现),它的实现过程几乎和Hero一模一样,根据plist文件把相关的五个动作动画实现就行了。
Robot.h
class Robot : public ActionSprite { public: Robot(); ~Robot(); bool init(); CREATE_FUNC(Robot); };
Robot.cpp其他没什么好说的,主要就是init函数的实现:
bool Robot::init() { bool ret = false; do { CC_BREAK_IF(!ActionSprite::initWithSpriteFrameName("robot_idle_00.png")); // 之前的CallFunc::create(this, callfunc_selector(Robot::idle))已经被废弃额 // 改用c++11特性来回调 CallFunc *callbackIdle = CallFunc::create(std::bind(&Robot::idle, this)); // 创建idle(空闲)动画,调用后反复播放 Animation *idle = createAnimation("robot_idle_%02d.png", 5, 12); setIdleAction(RepeatForever::create(Animate::create(idle))); // 创建walk(行走)动画,同样调用后反复播放 Animation *walk = createAnimation("robot_walk_%02d.png", 6, 20); setWalkAction(RepeatForever::create(Animate::create(walk))); // 创建attact(攻击)动画,播放后回调到idle动画 Animation *attack = createAnimation("robot_attack_%02d.png", 5, 12); setAttackAction(Sequence::create(Animate::create(attack), callbackIdle, NULL)); // 创建hurt(受伤)动画,播放后回调到idle动画 Animation *hurt = createAnimation("robot_hurt_%02d.png", 3, 12); setHurtAction(Sequence::create(Animate::create(hurt), callbackIdle, NULL)); // 创建knockout(被击倒)动画,播放后不做任何调用 Animation *knockout = createAnimation("robot_knockout_%02d.png", 5, 12); setKnockoutAction(Sequence::create(Animate::create(knockout), NULL)); setATK(20); // 攻击力 setHP(100); // 生命值 setVelocity(1); // 移动速度 setDirection(Point::ZERO); // 移动方向 ret = true; } while (0); return ret; }
同样的,在GameLayer.cpp中,把robots
数组创建出来,并create几个robot对象,就能看到效果了。
GameLayer.cpp的init中增加以下几行,这只是测试,增加5个机器人,并随意放置:
_robots = Array::createWithCapacity(5); for (int i = 0; i < 5; i++) { Robot *robot = Robot::create(); // 随机的放到地图的任何地方 robot->setPosition(Point(CCRANDOM_0_1() * 400, CCRANDOM_0_1() * 100)); robot->idle(); _robots->addObject(robot); _actors->addChild(robot); }
运行效果如下
2. 操作层
在上面一节,hero和robots都已经出现了,不过只是一直出于idle状态,现在首先让hero“动”起来,具体实现OptionLayer
,这个类负责响应触控操作。其实GameLayer与OptionLayer一样都继承于Layer
,完全可以在GameLayer中实现这些功能,只不过我认为把“游戏”和“操作”解耦之后更有利于把这个“操作”复用到不同的“游戏”中,或者让这个“游戏”使用多种不同的“操作”方式(例如纯触控、虚拟按钮等)。原文教程就是用虚拟按钮的方式实现控制的!
But,我比较反感在触屏上设置虚拟按钮的方式进行操控,主要的问题在于,用这种方式不仅占用了游戏空间,而且玩家在操作之间必须先把注意力集中到寻找按钮的位置上,这是中相当糟糕的体验,所以我更喜欢发挥出现代智能手机的潜力,直接用触控手势的方式取代虚拟按钮。回顾上面的内容可以看到,玩家能控制hero的动作也就只有attack
、walk
两种,那么可以把手机屏幕中间“一分为二”变为左屏
和右屏
,左半部分交给玩家的左手用来控制hero的walk,右半部分交给玩家的右手用来控制hero的attack。既然是左右屏,就有左右屏同时操作的可能,所以必须使用多点触控。
2.1 触摸响应
cocos2dx3.0中的多点触控接口貌似就这四个:
ccTouchesBegan | 触控开始事件,手指碰到屏幕 |
ccTouchesMoved | 触控移动事件,手指在屏幕上滑动 |
ccTouchesEnded | 停止触控时间, 手指离开屏幕 |
ccTouchesCancelled | 触控被取消事件,例如手指画出屏幕外 |
真心觉得很悲催啊,如果cocos2dx能加入手势识别不是会方便很多,例如“拖拽”、“滑动”、“点击”什么的。。。。唉,算了,还是老老实实先把这个游戏的操作层实现吧。除了最后一个Cancelled不需要外(大部分场合都用不到它),其他三个我们都需要,思路基本是这样:
- 左屏为一个隐藏的“摇杆”,响应玩家左手的“拖拽”手势,ccTouchesBegan记录按下的屏幕坐标为起点并激活“摇杆”,当ccTouchesMoved的时候根据新坐标与起点的偏移量获取拖拽的向量和距离,来控制hero移动的方向和速度;
- 右屏为一个看不见的按钮,响应玩家右手的“点击”手势,触发hero的攻击动作;
基本思路出来了,开始动工吧!新建OptionLayer
类(如果没有的话),根据前面的分析,OptionLayer中大概需要这么几个东西:
- 响应触摸的
began
、moved
和ended
这三个事件 - 一个“摇杆”,游戏中为
joystick
及它的相关函数(激活、更新什么的) - 委托
OptionDelegate
类,定义onWalk
、onAttack
和onStop
接口(由GameLayer实现)
OptionLayer.h:
class OptionDelegate { public: // 移动,direction为向量,distance是与起点的直线距离 virtual void onWalk(cocos2d::Point direction, float distance) = 0; // 攻击 virtual void onAttack() = 0; // 停止移动 virtual void onStop() = 0; }; class OptionLayer : public cocos2d::Layer { public: OptionLayer(); ~OptionLayer(); virtual bool init(); CREATE_FUNC(OptionLayer); // 触控的三个事件函数重载 void ccTouchesBegan(cocos2d::Set *ts, cocos2d::Event *e); void ccTouchesMoved(cocos2d::Set *ts, cocos2d::Event *e); void ccTouchesEnded(cocos2d::Set *ts, cocos2d::Event *e); // 委托者 CC_SYNTHESIZE(OptionDelegate*, _delegator, Delegator); private: // 摇杆,分为“摇杆”、“摇杆基座”两个部分 cocos2d::Sprite *_joystick; cocos2d::Sprite *_joystick_bg; // 激活“摇杆精灵”,并更新其坐标 void _activityJoystick(cocos2d::Point position); // 隐藏“摇杆精灵”,并将“摇杆”置于“摇杆基座”中心 void _inactivityJoystick(); // 刷新“摇杆”相对于“摇杆基座”的位置(根据触控手势) void _updateJoystick(cocos2d::Point direction, float distance); };
我把OptionLayer和OptionDelegate两个类放在一起声明了(理由是:我很懒!)
2.2 隐藏的摇杆
需要说明的一点,这里的joystick
“虚拟摇杆”是我自己画的,无非就是两个“同心圆”,大圆看做“摇杆基座”,小圆看做“摇杆”,当触控操作发生时,“虚拟摇杆”会被激活(平时未激活是看不到的),“摇杆”会在“摇杆基座”内部发生位移,就像真实世界中的摇杆做的推拉操作一样!!
接下来是具体实现OptionLayer.cpp,步骤是:
-
初始化
joystick
精灵、开启多点触控,在init函数中实现bool OptionLayer::init() { bool ret = false; do { CC_BREAK_IF(!Layer::init()); _joystick = Sprite::create("joystick.png"); _joystick_bg = Sprite::create("joystick_bg.png"); this->addChild(_joystick_bg); this->addChild(_joystick); _inactivityJoystick(); setTouchEnabled(true); ret = true; } while(0); return ret; }
-
实现“摇杆”的“激活”“刷新”等方法 激活/停止很容易,就是把joystick设置为可/不可见,并设置或复位其坐标就可以了,重点是“刷新”这个部分,这里要模拟真实世界中的摇杆的推拉操作,“摇杆”会随着操作发生位移,但不管怎么位移,“摇杆”的坐标是不会超出“基座”的(想象一下我们小时候玩过的街机,或者xbox,再想想圆规画圆的道理!!)
void OptionLayer::_activityJoystick(Point position) { _joystick->setPosition(position); _joystick_bg->setPosition(position); _joystick->setVisible(true); _joystick_bg->setVisible(true); }
void OptionLayer::_inactivityJoystick()
{
_joystick->setPosition(_joystick_bg->getPosition());
_joystick->setVisible(false);
_joystick_bg->setVisible(false);
}
// direction是方向(向量,偏移点与起始点行程的向量)
void OptionLayer::_updateJoystick(Point direction, float distance)
{
// 以“摇杆基座”的圆心为触控起点参考,“摇杆”做相应的便宜
Point start = _joystick_bg->getPosition();
if (distance < 32) {
// 如果移动未超出“摇杆基座”,“摇杆”在“基座”做偏移
_joystick->setPosition(start + (direction * distance));
} else if (distance > 96) {
// 如果移动超出“摇杆基座”,“摇杆”圆心始终在“基座”边缘做偏移
_joystick->setPosition(start + (direction * 64));
} else {
// 如果移动超出“摇杆基座”,“摇杆”边缘始终在“基座”边缘做偏移
_joystick->setPosition(start + (direction * 32));
}
}
- 重载多点触控的“开始”、“结束”和“移动”三个函数
上面的部分已经把“摇杆”实现的差不多了,不过看起来总是云里雾里,`direction`从字面上看知道是方向,但是从何而来,`distance`这个偏移量又是怎么计算出来的,看看下面代码就明白了。
```cpp
void OptionLayer::ccTouchesBegan(Set *ts, Event *e)
{
Size winSize = Director::getInstance()->getWinSize();
SetIterator iter = ts->begin();
while (iter != ts->end()) {
Touch *t = (Touch*)(*iter);
Point p = t->getLocation();
// left,当触控操作的起点小于屏幕宽度的一半,说明触控发生在左屏
if (p.x <= winSize.width / 2) {
_activityJoystick(p);
} else {
// right,否则发生在右屏,会产生“攻击”信号
}
iter++;
}
}
void OptionLayer::ccTouchesMoved(Set *ts, Event *e)
{
Size winSize = Director::getInstance()->getWinSize();
SetIterator iter = ts->begin();
Touch *t = (Touch*)(*iter);
Point start = t->getStartLocation();
// 如果该触控的起点是右屏产生的,则不做“滑动”处理
if (start.x > winSize.width / 2) {
return;
}
Point p = t->getLocation();
// 获取位移点与起始点的偏移量(直线距离)
float distance = start.getDistance(p);
// 获取起始点到位移点的向量(单位为1的坐标)
Point direction = (p - start).normalize();
_updateJoystick(direction, distance);
}
void OptionLayer::ccTouchesEnded(Set *ts, Event *e)
{
_inactivityJoystick();
}
到这里,已经能够看到“虚拟摇杆”了,编译运行一下,如果没错误的话可以看到类似下图的结果(点击和移动左屏才有效,代码中可以看到,右屏的响应还没写呢!)
2.3 来控制hero吧
目前为止,控制hero的部分其实已经完成了,因为思路和上一节的“摇杆”是一样的,现在无非是调用一下“委托”通知GameLayer层,让它改变hero的相关属性,所以首先完善一下手上的代码。
OptionLayer.cpp中
- 在
ccTouchesBegan
函数的right
右屏部分发起“攻击”信号,“左屏”部分暂时不用写什么(该写的前面都已经写了); - 在
ccTouchesMoved
中发起hero的“行走”信号,就之前的思路而言,只有“左屏”会产生“滑动”的手势; - 在
ccTouchesEnded
中发起“停止移动”信号,让hero恢复为“idle”状态。
void OptionLayer::ccTouchesBegan(Set *ts, Event *e) { ... } else { // right,否则发生在右屏,会产生“攻击”信号 _delegator->onAttack(); } } } void OptionLayer::ccTouchesMoved(Set *ts, Event *e) { ... _delegator->onWalk(direction, distance); } void OptionLayer::ccTouchesEnded(Set *ts, Event *e) { ... _delegator->onStop(); }
OptionLayer的代码可以说全部完成了,但如果现在编译运行的话程序肯定会出错!很简单,那个_delegator
还没有一个具体的值呢,而之前也说了这个“委托者”其实就是GameLayer,“游戏层”懒得响应玩家的操控才委托给了OptionLayer,现在OptionLayer的工作完成了,就该轮到GameLayer了:
- 在GameLayer中具体实现
onWalk
onStop
onAttack
;
GameLayer.h中继承OptionDelegate,并声明相应的成员函数:cpp class GameLayer : public cocos2d::Layer, public OptionDelegate { public: ... // 实现OptionDelegate的几个方法 void onWalk(cocos2d::Point direction, float distance); void onAttack(); void onStop(); ... };
GameLayer.cpp:
void GameLayer::onAttack() { _hero->attack(); } void GameLayer::onWalk(Point direction, float distance) { _hero->walk(direction); } void GameLayer::onStop() { _hero->idle(); }
-
还记得GameLayer和OptionLayer是在哪里创建的吗?GameScene!,所以在GameScene中,把GameLayer赋值给_delegator。
GameScene.cppbool GameScene::init() { ... _gameLayer = GameLayer::create(); _optionLayer = OptionLayer::create(); _optionLayer->setDelegator(_gameLayer); ... }
编译运行!看看结果:
-。-!!!逻辑不对呀!有几个问题:
- 摇杆往左,hero往右,摇杆往右,hero往左。。。
- walk的帧率貌似太高了,而attack的动画帧率又太低了
- 摇杆刚开始在左,移动到右的时候,hero的朝向却没有跟着取反(这是个大bug)
赶紧改改!!
回忆下之前的ActionSprite类的实现,我参考教程把walk
和attack
两个方法都添加了参数,其目的就是当“角色”行走的时候会根据参数direction
判断并修改朝向(问题1的错误也在此),当“角色”受伤的时候根据damage
参数,减少“角色”的hp,当hp为0后自动出发角色“死亡”,这种小聪明走到现在发现还真有点画蛇添足!“操作层”发来的onWalk
是持续的,意味着那个“direction”参数随时都在改变,可只要“操作层”没有发送onStop
信号时hero将会一直处于ACTION_STATE_WALK
状态,在ActionSprite类实现中有一点很重要:只要精灵已经处于要改变的状态时,就不会在做改变的处理,所以尽管“direction”一直在变,也一直在调用hero的“walk”方法,在第一次出起“walk”之后,后来的“direction”已经毫无意义了(因为hero已经在walk了,根本不处理)。
我想了想,ActionSprite归根结底只是个“动作精灵”,它只要管好自己动作的相关动画和“角色属性”就好了,至于怎么调用,怎么改变,它的生杀大权还是交给GameLayer吧!
首先要做一个小小的重构,就是删除所有ActionSprite相关的“动作参数”,一律改为void action(void),ActionSprite.h修改如下:
class ActionSprite : public Sprite { ... void idle(); void walk(); void attack(); void hurt(); void knockout(); ... };
对应的ActionSprite.cpp中walk和hurt函数也要修改:
void ActionSprite::walk() { if (_changeState(ACTION_STATE_WALK)) { runAction(_walkAction); } } void ActionSprite::hurt() { if (_changeState(ACTION_STATE_HURT)) { runAction(_hurtAction); } }
还有Hero.cpp与Robot.cpp, 顺便把帧率的问题也改了吧:
bool Hero::init() { ... // 创建walk(行走)动画,同样调用后反复播放 //Animation *walk = createAnimation("hero_walk_%02d.png", 7, 24); Animation *walk = createAnimation("hero_walk_%02d.png", 7, 14); setWalkAction(RepeatForever::create(Animate::create(walk))); // 创建attact(攻击)动画,播放后回调到idle动画 //Animation *attack = createAnimation("hero_attack_00_%02d.png", 3, 12); Animation *attack = createAnimation("hero_attack_00_%02d.png", 3, 20); setAttackAction(Sequence::create(Animate::create(attack), callbackIdle, NULL)); ... setATK(20); // 攻击力 setHP(100); // 生命值 //setVelocity(1); // 移动速度 //setDirection(Point::ZERO); // 移动方向 ... }
修改完成了,现在hero的walk和hurt的具体参数控制交由GameLayer管理,所以修改GameLayer.cpp的onWalk:
void GameLayer::onWalk(Point direction, float distance) { // 根据精灵的x向量,来判断精灵的正面“朝向” _hero->setFlipX(direction.x < 0 ? true : false); _hero->walk(); }
再来看看新的效果
这样应该就没问题了,不过现在只会原地踏步,接下来进一步实现hero真正的移动。
hero在地图上移动是一个持续的过程(当玩家手指滑动后,只要不离开屏幕,hero就不会停下walk的脚步!),处理持续的动作时常常会用到cocos2dx的一个接口update
这个接口为在每帧渲染的时候都被调用一次(所以update的实现务必要快准狠,不然浪费资源)。那么在update中需要做些什么呢?目前为止其实只需要做一点,递增/减hero的坐标值。那这个增量的标准是什么?其实就是hero的移动速度。那移动速度从何而来?想象物理界是如何定义速度的:速度是描述物体运动快慢和方向的物理量,定义为位移与发生这个位移所用的时间之比,看前半句,速度无非要两个东西“快慢”和“方向”,再来看看onWalk
吧,它是带有两个参数的direction
、distance
,顾名思义“方向”“偏移量”,我们姑且把偏移量看做快慢的标准,偏移量越大速度越快;再来看看后半句的定义,“位移”与“时间”之比。真实世界里描述速度一般都是xx米/秒或者xx公里/小时,游戏里则可以用xx像素/帧来衡量,而update每帧都会调用。所以重新回到最开始的问题,update中要做些什么?其实就是把hero当前坐标加上一个速度量。
一切都搞清楚了,具体来实现吧。
GameLayer.h中声明update
函数,并再声明一个_heroVelocity
给update用:
class GameLayer : public cocos2d::Layer, public OptionDelegate { public: ... void update (float dt); ... private: cocos2d::Point _heroVelocity; ... };
GameLayer.cpp中具体实现,这里要注意我让hero的速度分两个档次
void GameLayer::onWalk(Point direction, float distance) { ... // 根据偏移量的大小,确定速度在1档还是2档 _heroVelocity = direction * (distance < 96 ? 1 : 2); } void GameLayer::update(float dt) { // 如果hero处于Walk,则刷新其坐标 if (_hero->getActionState() == ACTION_STATE_WALK) { _hero->setPosition(_hero->getPosition() + _heroVelocity); } }
这样hero就可以在地图上移动了,来看看效果:
确实可以了,不过总感觉哪里不对呀??
- 肿么hero可以跑到墙上,不科学啊
- hero都走出屏幕了这是要闹哪样
- robot肿么把hero给挡住了,他可是男主角啊!
。。。。。。。。。
问章有点长了,剩下的部分留到下一篇写,主要包括完善hero的移动,整个GameLayer的移动,另外就是robot的人工智能部分,最后是碰撞检测。
声明:
- 本文主要参考《如何制作一个横版格斗过关游戏 Cocos2d-x 2.0.4》和《How To Make A Side-Scrolling Beat Em Up Game Like Scott Pilgrim with Cocos2D》
- 本文游戏与上述两篇文章游戏实现上出入较大(拒绝复制粘贴),如有不对的地方还望指正,以免误导!
- 美术素材下载: 下载地址
相关推荐
《cocos2d-x 3.0:游戏开发中的角色移动技术详解》 在游戏开发领域,cocos2d-x是一款广泛使用的开源2D游戏引擎,尤其在移动平台上的应用非常广泛。cocos2d-x 3.0版本带来了许多性能优化和新特性,使得开发者能够更...
Cocos2d-x是一款开源的游戏开发框架,广泛应用于2D游戏、实时渲染应用程序和其他互动内容的制作。在本文中,我们将深入探讨Cocos2d-x3.0版本的关键特性和源码分析,以及如何利用它进行游戏开发。 Cocos2d-x3.0是一...
《Cocos2d-x 3.0的俄罗斯方块代码》是一个基于Cocos2d-x游戏引擎3.0及以上版本实现的经典游戏项目。Cocos2d-x是一个广泛使用的开源跨平台2D游戏开发框架,它允许开发者用C++、Lua或JavaScript编写游戏,并在iOS、...
这是我重新弄的cocos2d-x-3.0的类图.之前别人兄台弄的,有些不全面,有些地方错误.我这个可以说是最新的了.每个类添加了中文的详细注解,同时也添加了中文的类名称翻译.这样对cocos2d-x-3.0的框架比较好上手. 有兴趣的...
10. **API稳定**:尽管是alpha版本,但cocos2d-x 3.0-alpha0已经对API进行了大量调整,确保其稳定性和向前兼容性,以便开发者能顺利迁移到正式版。 解压后的Lib文件包含了上述所有功能的实现,开发者可以通过包含...
《cocos2d-x 3.0中文API详解》 cocos2d-x是一个开源的、跨平台的游戏开发框架,广泛应用于2D游戏、实验性的3D游戏以及各种交互式应用的开发。3.0版本的发布带来了许多新特性和改进,其中中文API文档的推出更是为...
总的来说,animatepacker与cocos2d-x3.0的接口封装是游戏开发中的一个重要环节,它提升了开发效率,降低了资源管理的复杂性,同时也优化了游戏的性能。通过理解和掌握这一技术,开发者可以更好地利用animatepacker的...
#### 四、Cocos2d-x3.0 Final手机游戏开发核心技术 这一部分是整个教程的核心内容,主要包括以下几个方面: 1. **环境搭建**:首先介绍如何安装和配置Cocos2d-x的开发环境,包括编译器、IDE等工具的选择与配置。 ...
《SneakyJoystick-Cocos2d-x 3.0rc0:打造高效游戏控制体验》 Cocos2d-x是一款流行的开源游戏开发框架,它使用C++编写,支持多种平台,包括iOS、Android以及Windows等。在游戏开发中,交互性是至关重要的,而...
Cocos2d-x3.0是一款广泛应用于游戏开发的开源框架,主要支持2D游戏的构建,基于C++,同时也提供了Lua和JavaScript的绑定。它以其高效、跨平台的特性,使得开发者能够轻松地在iOS、Android、Windows等多个平台上发布...
Cocos2d-x是一个开源的游戏开发框架,它被广泛应用于iOS、Android、Windows、MacOS、Linux等平台,为游戏开发者提供了一个快速开发跨平台2D游戏的解决方案。Cocos2d-x版本的更新通常会带来许多新的特性和改进,同时...
这是cocos2d-x 3.0 类关系图, 这个制作者是:gamecocos2dx 他做的Xmind,我导出的PNG格式,我推荐还是看Xmind,里面还有一些注释 gamecocos2dx Xmind下载地址:...
在cocos2d-x 3.0版本中,开发者们经常需要将C++自定义的类暴露给Lua脚本使用,以便在游戏中实现更复杂的逻辑和交互。这篇文档将详细解释如何将C++自定义类导出到Lua,以及如何处理自定义类的命名空间问题,以期帮助...
本篇将深入探讨如何利用cocos2d-x 3.0与cocostudio 1.4来构建一个"奇怪的大冒险"游戏。 一、cocos2d-x 3.0基础 cocos2d-x 3.0是cocos2d-x系列的一个重要版本,它基于C++,提供了丰富的API和优化的性能。主要特点...
下载并解压Cocos2d-x 3.0的压缩包,然后将`[cocos-root]/tools/cocos2d-console/bin/`路径添加到环境变量Path中。这使得你可以从任何目录调用cocos命令行工具。 3. **验证环境**: 打开命令行终端,切换到`cocos...
cocos2d-x 是一个广泛使用的开源游戏开发框架,而3.0版本在原有的基础上引入了骨骼动画功能,大大增强了2D游戏的角色表现力和动态效果。本篇文章将围绕“cocos2d-x 3.0 骨骼动画 demo”进行深入探讨,包括其工作原理...
《cocos2d-x 3.0 离线文档》是针对游戏开发框架cocos2d-x 3.0版本的一份详尽参考资料,它以HTML格式提供,方便开发者在无网络环境下查阅。cocos2d-x是一个开源的游戏开发框架,基于C++,并支持多平台,包括iOS、...
本游戏基于cocos2d-x 3.0版本,可能不适用于cocos2d-x 2.x版本,请看清楚,再参考! 使用方法:将Classes中的类全部拷贝到新建cocos项目的Classes项目中,将Resources下的所有文件拷贝到对应的文件下,运行即可!
《Cocos2d-x3.0final自学教程贪食蛇项目源码》是针对游戏开发初学者的一个宝贵资源,它提供了使用Cocos2d-x3.0final版本开发经典游戏"贪食蛇"的完整代码。Cocos2d-x是一个跨平台的2D游戏开发框架,广泛应用于iOS、...
在cocos2d-x 3.0-alpha0版本中,SDK包含了重要的头文件和动态链接库(DLL)文件,这些文件是构建和运行Cocos2d-x游戏的基础组件。本文将深入探讨这些文件的作用以及它们在游戏开发中的关键角色。 首先,头文件在...