知易游戏开发教程cocos2d-x移植版006
在上一节中,我们使用经典FC游戏《坦克大战》的元素设计了一张地图,来演示Tiled Map Editor工具的基本用法,并在cocos2d-x程序中完成了tmx地图加载、查看以及动态修改地图元素的功能。
这一节,我们将对示例5进一步扩充、完善,使其成为能“玩”一下的游戏。
为了这个目标,我们需要做以下调整:
1)增加双方坦克,我方一辆,敌方八辆。
2)坦克在地图上行走时,需要完成基本的碰撞检测,不可以穿墙而过。
3)坦克可以开炮,炮弹可以摧毁砖墙和敌对方坦克。
4)视角锁定在我方坦克上,显示区域跟随我方坦克的移动而改变。
5)敌方(电脑)坦克拥有基本的AI来增加点儿乐趣。
增加坦克
我们的目标只是一个简单的示例游戏,所以我准备用同一个类来表示双方坦克,通过一个布尔型变量来区分敌我。
1 class TankSprite : public CCSprite 2 { 3 public: 4 TankSprite(void); 5 virtual ~TankSprite(void); 6 7 bool bIsEnemy; 8 }
在地图中漫游
作为一个“完整”的游戏,我们需要实现在游戏地图中漫游的功能。这包含两方面的内容,下面我们来分别描述一下。
1)坦克精灵在地图上的移动
游戏讲究一个代入感。是谁在玩游戏?不是上帝,我只是一辆小小的坦克。当操作指令下达时,应该是一辆坦克去完成它,而不是通过上帝的眼睛来观察。所以,现在需要完成的功能就是让坦克在tmx地图上移动。
要实现这样的功能并不困难,因为CCSprite精灵类为我们准备好的大部分内容。我们要做的只是为坦克增加当前的状态、移动速度以及四个方向的移动函数。
1 // TankAction 是一个枚举类型,用来表示坦克当期的状态 2 TankAction kAct; 3 float moveStep; 4 void moveLeft(GameLayer *layer); 5 void moveRight(GameLayer *layer); 6 void moveUp(GameLayer *layer); 7 void moveDown(GameLayer *layer);
在实现四方向移动函数时,因为坦克是tmx地图的子节点,所以坐标不需要太多计算。
1 void TankSprite::moveLeft(GameLayer *layer) 2 { 3 kAct = kLeft; 4 setRotation(-90.0f); 5 CCPoint tankPos = getPosition(); 6 tankPos.x -= moveStep; 7 CCSize tankSize = getTextureRect().size; 8 9 // 越界检测 10 if (tankPos.x - layer->mapX < tankSize.width / 2) 11 setPosition(ccp(tankSize.width / 2, tankPos.y)); 12 else 13 setPosition(tankPos); 14 }
2)视口跟随
完成上面的功能,坦克就可以在地图上行走了。但是我们只能看见屏幕大小的地图,坦克很容易就走到屏幕之外去了。我们不愿意做一只“井底之蛙”,眼睛要跟上坦克。
我们前面介绍过视口这个概念,它就是整个游戏世界向我们打开的一扇窗子。而且,在示例5中我们也尝试了移动视口来观察整个游戏地图。我们现在要做的就是加一个视口跟随的功能,让视口跟随主角(我方)坦克车移动,将合适的地图区域展示给我们。
纵观大多数人的设计,对于视口跟随,一个普遍的做法是这样的。
大部分时间都将主角精灵固定在视口中心,主角的移动是通过反方向移动背景地图模拟的。只有当视口已经到达地图边缘,再没有更多地图供我们移动时,才移动主角本身。
句子有点儿拗口,没理解的朋友请看下面的演示。
将上面的过程写成代码:
1 void GameLayer::setWorldPosition(void) 2 { 3 CCSize tankSize = tank->getTextureRect().size; 4 CCPoint tankPos = tank->getPosition(); 5 6 if (tankPos.x < (screenWidth - tankSize.width) / 2) 7 mapX = 0; 8 else if (tankPos.x > gameWorldWidth() - screenWidth / 2) 9 mapX = -gameWorldWidth(); 10 else 11 mapX = -(tankPos.x - screenWidth / 2 + tankSize.width / 2); 12 13 if (tankPos.y < (screenHeight - tankSize.height) / 2) 14 mapY = 0; 15 else if (tankPos.y > gameWorldHeight() - screenHeight / 2) 16 mapY = -gameWorldHeight(); 17 else 18 mapY = -(tankPos.y - screenHeight / 2 + tankSize.height / 2); 19 20 // 越界复位 21 if (mapX > 0) 22 mapX = 0; 23 if (mapY > 0) 24 mapY = 0; 25 if (mapX < -(gameWorldWidth() - screenWidth)) 26 mapX = -(gameWorldWidth() - screenWidth); 27 if (mapY < -(gameWorldHeight() - screenHeight)) 28 mapY = -(gameWorldHeight() - screenHeight); 29 gameWorld->setPosition(mapX, mapY); 30 }
碰撞检测
坦克也不是说是无坚不摧的,所以遇到河啊什么的,还是从桥上走方便一些。所以我们就得判断是不是没路了,是不是撞墙了,于是“碰撞检测”的概念就引入进来了。
试想,有8辆敌方坦克正在地图上游荡,他们撞墙要检测,互相之间也要检测,他们偶尔还会发射炮弹,每个炮弹的飞行和爆炸也都需要检测。而且这些都是并发进行的。哇!多么混乱的一个场面啊!
呵呵,其实“碰撞检测”并没有大家想的那么复杂,不是说要做碰撞检测就要弄个物理引擎进来,只要不是在实现逼真的物理效果,我们完全可以用自己的方法来检测碰撞。
大家知道,什么同时啊,并行啊,这些统统都是理论上的,或者说是在一定精度范围上的东西。即便是多核CPU,在共享同一资源时,它们也要分优先级的。所以,在小游戏这类简单的应用上,我们完全可以认为,一次碰撞发生时,世界是静止的。
这样一来,我们只需要按照一定的优先级,顺次为每一个需要检测碰撞的对象进行检测即可。
对于坦克的行走来说,我们需要做的检测只有地形一个因素,又因为是在同一坐标系下,事情灰常简单。
考虑到坦克是有体积的,这里取3个采样点检测碰撞,以避免其边缘进入墙里。
1 // 这是坦克左移时的碰撞检测代码 2 CCPoint nextPos = ccp(tankPos.x - tankSize.width / 2, tankPos.y); 3 unsigned int tid = layer->tileIDFromPosition(nextPos); 4 if (tid != 4) 5 return; 6 nextPos = ccp(tankPos.x - tankSize.width / 2, tankPos.y + tankSize.height / 4); 7 tid = layer->tileIDFromPosition(nextPos); 8 if (tid != 4) 9 return; 10 nextPos = ccp(tankPos.x - tankSize.width / 2, tankPos.y - tankSize.height / 4); 11 tid = layer->tileIDFromPosition(nextPos); 12 if (tid != 4) 13 return;
开火射击
瞧,那辆破坦克摇头摆尾地开过来了,让我们干掉他。Fire! BOOM...
好吧,既然你想开火,那就给每辆坦克都配上炮弹吧。
1 BulletSprite* BulletSprite::bulletWithLayer(GameLayer *layer) 2 { 3 BulletSprite *sprite = new BulletSprite(); 4 if (sprite && sprite->initWithFile("bullet.png", CCRectMake(0, 0, 16, 16))) 5 { 6 sprite->autorelease(); 7 layer->gameWorld->addChild(sprite, 100); 8 sprite->setIsVisible(false); 9 sprite->setGameLayer(layer); 10 return sprite; 11 } 12 CC_SAFE_DELETE(sprite); 13 return NULL; 14 }
虽然炮弹是坦克的配备,但是为了方便坐标计算,我们将炮弹归为地图的子节点。换个思路想,炮弹发射后就跟坦克分离了,所以我们这么设计也是可以接受的。
理所当然的,我们还需要为每一辆坦克增加一个开火按钮。
1 void TankSprite::onFire(GameLayer *layer) 2 { 3 CCPoint tankPos = getPosition(); 4 CCSize tankSize = getTextureRect().size; 5 CCPoint bulletPos, bulletTarget; 6 switch (kAct) 7 { 8 case kUp: 9 bulletPos = ccp(tankPos.x, tankPos.y + tankSize.height / 2); 10 bulletTarget = ccp(bulletPos.x, bulletPos.y + 1024); 11 break; 12 case kDown: 13 bulletPos = ccp(tankPos.x, tankPos.y - tankSize.height / 2); 14 bulletTarget = ccp(bulletPos.x, bulletPos.y - 1024); 15 break; 16 case kLeft: 17 bulletPos = ccp(tankPos.x - tankSize.width / 2, tankPos.y); 18 bulletTarget = ccp(bulletPos.x - 1024, bulletPos.y); 19 break; 20 case kRight: 21 bulletPos = ccp(tankPos.x + tankSize.width / 2, tankPos.y); 22 bulletTarget = ccp(bulletPos.x + 1024, bulletPos.y); 23 break; 24 default: 25 break; 26 } 27 bullet->setPosition(bulletPos); 28 bullet->setIsVisible(true); 29 CCShow *ac1 = CCShow::action(); 30 CCMoveTo *ac2 = CCMoveTo::actionWithDuration(5.0f, bulletTarget); 31 bullet->runAction(CCSequence::actions(ac1, ac2, NULL)); 32 // 启动炮弹的碰撞检测 33 this->schedule(schedule_selector(TankSprite::checkExplosion), 1.0f / 30.0f); 34 }
大家可以看到,炮弹发射是用系统内置的MoveTo动作模拟的,最后一行开启每秒30次的炮弹碰撞检测。检测的方法与行走时的检测类似,这里不再重复。
小结
至此,一个简单的坦克大战演示就初步完成了。为了给大家一个更形象的认识,上一张层结构示意图。
本章示例代码基于cocos2d-1.0.1-x-0.13.0-beta编写。虽然cocos2d-2.0-rc0a-x-2.0已经发布了,但是变动比较大,主要是得换模板向导,我比较懒,所以本系列结束之前,我先不换版本了。
下载地址