这次我準备写一个简单的射击遊戏作为练习, 遊戏里可以控制主角移动, 按钮发射子弹射击飞过来的敌人, 被敌人撞到就 Game Over.
通过这个练习可以熟识一些基本的东西, 像:
– 遊戏框架
操控
– 声效
– 粒子效果

4.1 SneakyInput

先说一下关於操控吧, 之前买了两本 cocos2d 的参考书, 都推荐用一个第叁方cocos2d 库叫 SneakyInput, 所以我就拿来试用一下, 结果觉得果然不错, 为遊戏加入虚拟操控杆和按钮非常方便!
原装cocos2d 版大家可以在这里下载:
https://github.com/sneakyness/SneakyInput
当然我们需要的是 cocos2d-x 版:
https://github.com/Ntran013/SneakyInput
下载後把里边的源码都加到自己的工程里就可以:
Cocos2d-X
在作这个练习时, 正好赶上 cocos2d-x 2.0 第一版的发佈, 我比较喜欢试新东西, 所以就第一时间转过去了, 2.0 底层换了用 OpenGL ES 2.0, API 也作出了不少的改动, 正好我刚开始学习 cocos2d, 弄的都是小项目, 说换就换不头痛.
在用 SneakyInput 时, 要有两个小改动才能在 2.0 上编译, 其中一个是 1.0 时已经要改的了:
在SneakyButton和 SneakyJoystick 的 ccTouchBegan(), ccTouchMoved() 里, 要把这句:
CCPoint location = CCDirector::sharedDirector()->convertToGL(touch->locationInView(touch->view()));
改为:
CCPoint location = CCDirector::sharedDirector()->convertToGL(touch->locationInView());
另一个改动, 是在SneakyButton和 SneakyJoystick 的 onEnterTransitionDidFinish() 和 onExit() 里, CCTouchDispatcher 在 1.0 时是一个 Singleton, 但在2.0 被放进入CCDirector, 所以本来的CCTouchDispatcher::sharedDispatcher()
要在遊戏里加上一个 操控杆, 首先我们要準备一个底座和一个操控杆的图像, 接下来, 先要建立一个 SneakyJoystickSkinnedBase:
SneakyJoystickSkinnedBase *joystickBase = new SneakyJoystickSkinnedBase();
joystickBase->autorelease();
joystickBase->init();
joystickBase->setBackgroundSprite(CCSprite::spriteWithSpriteFrameName("circleBig.png")); //

底座
joystickBase->setThumbSprite(CCSprite::spriteWithSpriteFrameName("circleSmall.png")); //
joystickBase->setPosition(System::CCPointMake(48, 48));
然後我们要建立一个SneakyJoystick 并设置到 SneakyJoystickSkinnedBase 里:
SneakyJoystick *joystick = new SneakyJoystick();
joystick->autorelease();
joystick->initWithRect(CCRectMake(0,0,64,64));
joystickBase->setJoystick(joystick);
this->addChild(joystickBase);

用 SneakyInput 加遊戏按钮也是同样简单, 但用的是 SneakyButtonSkinnedBase 和 SneakyButton:
SneakyButtonSkinnedBase *buttonBase = new SneakyButtonSkinnedBase();
buttonBase->autorelease();
buttonBase->init();
buttonBase->setDefaultSprite(CCSprite::spriteWithSpriteFrameName("buttonBlue.png"));
buttonBase->setActivatedSprite(CCSprite::spriteWithSpriteFrameName("buttonOrange.png"));
buttonBase->setPressSprite(CCSprite::spriteWithSpriteFrameName("buttonOrange.png"));
buttonBase->setPosition(CCPointMake(480-48, 48));
SneakyButton *button = new SneakyButton();
button->autorelease();
button->initWithRect(btnRect);
button->setIsToggleable(false);
button->setIsHoldable(true);
buttonBase->setButton(button);
this->addChild(buttonBase);

最後就是在我们的 update(ccTime dt) 里, 拿SneakyInput 传回来的数值对我们的角色等作出相关的操作:
CCPoint scaledV = ccpMult(joystick->getVelocity(), 480);
CCPoint pos = mSprite->getPosition();
pos.x += scaledV.x*dt;
pos.y += scaledV.y*dt;
mSprite->setPosition(pos);
if (button->getIsActive())
{
// Fire !!!
}

在工程里为了方便使用, 我拿 SneakyInputEx 把 SneakyInput 封包了一下.

4.2 遊戏/程序架构

基本上我们把这个小遊戏分作3 个场景: 菜单, 主遊戏塲景和遊戏终结画面, 为了方便场景之间的调用, 我弄了一个叫 GameManager 的 singleton, 我们可以运用 GameManger 切换至3个场景中任何一个:
切换场景
切换场景
切换场景
3个场景中最重要的当然是 GameScene, 这个场景再细分为两层, 下层是背景, 上层是玩家角色和敌人等等.
近来喜欢上了”组合好过继承”(Prefer Composition over Inheritance)的理论, 这个理论简单来说就是当我们设定一个新类时, 会把有关的类以组件(component)方式加到新类里而不是让新类继承他们. 举个例子, 遊戏里的 Player 类一般我们都习惯把它设定为继承於 CCSprite, 但仔细地想一下, CCSprite 所代表的其实只是一个图像(或动画), 可以说只是 Player 的”样子”, 而 Player 除了有个样子, 还会有其他的特性, 比如移动功能, 比如能源宝箱等等…当我们把 Player 继承於 CCSprite 即是意味著它”是” CCSprite, 但我们之後加在 Player 的功能郤不是一般 CCSprite 所需要的功能, 所以更好的做法, 是把 CCSprite 设为 Player 的一个组件, 而 Player 除了有一个样子(CCSprite)的组件, 还会有一个控制它移动的组件或一个储存了玩家在遊戏拾到的武器的宝箱组件等等… “组合而成”的类的最大好处, 是每个组件都互不相干, 各自负责自己的功能, 而我们可以任意把组件加入或拿走.
当然在这个小练习我并没有时间弄一整套组件系统出来(推荐大家看一下 Game Coding Complete 第4版, 其中有一章专门介绍怎样写一套完整的组件系统), 这里我只把概念简单地运用了一下在 Entity 类的设计, Player, Monster 和 Bullet 都是 Entity, 分别只在於:
Player 的组件是 CCSprite + JoystickController (或 KeyboardController, 见4.5)
Monster 和 Bullet 的组件是 CCSprite + SimpleMoveController
假如我们在遊戏里需要不同移动方法的怪兽, 只要再弄几个不同的Controller 放到不同的怪兽里就可以.
同样的道理, 我们的遊戏可能会允许玩家选择不同的操控方法, 那只要我们把不同的操控Controller 类放到 Player 里就行, 非常方便.

4.3 Manager 的运用

写遊戏我们通常都会把执行速度放在很重要的位置, 而在遊戏进行中用 new 来生成物件会比较慢, 有机会影响遊戏的流畅度, 所以这里我用了 MonsterManger 和 BulletManger 来预先生成在遊戏里可能会用到最多数目的怪兽和子弹, 一开始先把它们”藏”起来, 在需要时才把他们设好位置和显示出来. 当然用这两个manager 的另一个好处是可以顺便在它们的update 里进行碰撞测试, 看看子弹有没有打中 怪兽 或 怪兽 有没有撞到玩家.
mMonsterList = CCArray::arrayWithCapacity(MAX_MONSTER);
mMonsterList->retain();
for (int i=0;iSetActive(false);
mMonsterList->addObject(monster);
parentNode->addChild(monster);
}

4.4 捲动的背景

一个静止的背景不是太有趣, 所以我们让它动起来吧!
如果只是把现在的背景向左捲动, 背景很快会被捲出画面, 右边会出现空白. 解决这个问题, 我们可以用两张背景, 当一张向左捲动的时候, 另一张就去填补右边出现的空白. 而为了使到两个背景可以”无缝”连接, 第二张背其实是用了跟第一张一样的图像但左右对调了一下.
切换场景

4.5 在 Windows 上测试

我们是用 SneakyInput 在画面上弄了一个操控杆和一个发射子弹的按钮, 在Windows 上测试郤很是坑爹, 因为没办法同时的去点操空杆和按钮! 所以我另外写了一个用键盘操控的Controller 在 Windows 环境上用:
void KeyboardController::update(ccTime dt)
{
if (mListener)
{
float xDelta = 0;
float yDelta = 0;
if ((GetAsyncKeyState(VK_LEFT) & 0x8000)==0x8000) // 上下左右键移动
xDelta = -mMovingSpeed;
if ((GetAsyncKeyState(VK_RIGHT) & 0x8000)==0x8000)
xDelta = mMovingSpeed;
if ((GetAsyncKeyState(VK_UP) & 0x8000)==0x8000)
yDelta = mMovingSpeed;
if ((GetAsyncKeyState(VK_DOWN) & 0x8000)==0x8000)
yDelta = -mMovingSpeed;
mListener->UpdatePosition(dt, xDelta, yDelta);
if ((GetAsyncKeyState(VK_LCONTROL) & 0x8000)==0x8000) // 左边CTRL键射击
{
if (!mFireKeyDown)
{
mFireKeyDown = true;
mListener->FirePrimary();
}
}
else
mFireKeyDown = false;
}
}

这里马上又可以看到”组件” 的好处了, 在玩全不影响 Player 类的情况下, 我们只要把新的 KeyboardController 放进去, 就可以用键盘操控它的移动了!
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32)
KeyboardController *controller = KeyboardController::controllerWithParentNode(parent);
#else
JoystickController *controller = JoystickController::controllerWithParentNode(parent);
#endif
CC_BREAK_IF (!controller);
SetController(controller);

Cocos2d-X射击遊戏, 框架, 操控, 视窗键盘运用插图(5)

4.6 粒子系统和声效

我觉得 cocos2d 的粒子系统不是太完美, 只是单发射口(emitter), 似乎很难做出很好很真实的粒子效果(比如 iFighter 2 里的爆炸效果), 这次只是随便的用了一下, 以後有时间再研究研究.
声效方面我也弄了一个 SoundManager 来统一处理, 是个 singleton, 方便任意调用. 在播放音乐时, 有个有趣的发现, SimpleAudioEngine::preloadBackgroundMusic() 里竟然是空的? 不知道引擎小组最後会不会加进这个功能呢?
[dl href=”http://vdisk.weibo.com/s/7cIYF”]点此下载DemoGame_01[/dl]
[dl href=”http://vdisk.weibo.com/s/7cJ7p”]点此下载DemoGame_FINAL_2[/dl]