<cocos2d-x for wp7>使用cocos2d-x制作一个太空射击游戏
本次教程参考文章:http://www.cnblogs.com/zilongshanren/archive/2011/06/09/2074962.html
本次教程做一个空战游戏,这个游戏类型相信大家玩小霸王(FC)的时候都玩过,就是一个滚动的画面,画面上差不多半屏都是怪物,玩家控制的飞机可以自由移动并且发射面条枪。当然,最后有一个BOSS,虽然BOSS有时候相当弱智(没办法,太难小时候也玩不过)。这次做的示例,用到了一些前面教程没用过的东西,重力感应控制飞机,视差滚动制作,粒子系统的简单使用。
另外,做教程有一段时间了。估计不少朋友也是刚入门,什么Action这类东西怎么写还真是不怎么懂,推荐去研究官方安装包自带的Test工程,那里有详细的运用方法。这个没有在一开始做教程的时候提到,还真是有点失误。我这里主要想提供些实例教程,这些怎么做还不想去详细讲,毕竟有Test工程这个很好的实例了。
程序截图:
本次教程假定你已经学习过前面的《用cocos2d-x做一个简单的windows phone 7游戏》系列教程,或者你拥有同等的经验。
下载这次教程需要的资源包(http://dl.dbank.com/c0h3bugf3a)。这个资源包括所有的图片,一些音效,并且粒子系统用到的三个plist文件。
那么开始吧。
添加一个太空船
现在新建一个工程,命名为cocos2dSpaceGameDemo,当然,毕竟是练习项目,所以OpenXLive不用。新建完项目之后,修复DLL是必须的。然后添加一个类SpaceBattleScene到Classes文件夹。并且修改代码如下:
class SpaceBattleScene:CCScene { public SpaceBattleScene() { CCLayerColor colorLayer = CCLayerColor.layerWithColor(new ccColor4B(Color.Black)); this.addChild(colorLayer, -1); this.addChild(SpaceBattleLayer.node()); } } class SpaceBattleLayer : CCLayer { public override bool init() { if (!base.init()) return false; return true; } public static new SpaceBattleLayer node() { SpaceBattleLayer layer = new SpaceBattleLayer(); if (layer.init()) return layer; return null; } }
并且修改AppDelegate类的Launch事件,如下:
//CCScene pScene = cocos2dSpaceGameDemoScene.scene(); SpaceBattleScene pScene = new SpaceBattleScene(); //run pDirector.runWithScene(pScene);
上面新建了一个空的工程,并且在战斗场景中添加了一个黑色的背景层,毕竟太空还是黑色的。
现在来添加一个spaceship吧。这次用的是CCSpriteBatchNode来添加和管理精灵,而且这个可以优化精灵。具体请看<cocos2d-x for wp7>在cocos2d-x使用spritesheet和用spritesheet创建动画
那么把AllSprite.plist文件放置在resource目录下,AllSprite.png图片放置在resource/images目录下。并且修改AllSprite.plist的属性中的contentProcessor和ContentImporter分别为TextProcessor和TextImporter。
添加一些声明到SpaceBattleLayer类中:
const int shipTag = 1; const int attackShipTag = 2; const int bulletTag = 3; const int bossTag = 4; const int lifefoodTag = 5;CCSize winSize = CCDirector.sharedDirector().getWinSize();
CCSpriteBatchNode batchNode;
CCSprite ship;
这些是一些TAG的变量。下面要使用到。
添加如下代码到SpaceBattleLayer的init方法中,
batchNode = CCSpriteBatchNode.batchNodeWithFile(@"resource/images/Allsprite"); this.addChild(batchNode); CCSpriteFrameCache.sharedSpriteFrameCache().addSpriteFramesWithFile(@"resource/Allsprite"); ship = CCSprite.spriteWithSpriteFrameName(@"SpaceShip.png"); ship.position = new CCPoint(winSize.width / 10, winSize.height / 2); ship.tag = shipTag; batchNode.addChild(ship, 1);
让我们一句一句地解释上面的代码:
- 使用一张大的图片创建一个CCSpriteBatchNode对象来批处理所有的对象的描绘操作。
- 把CCSpriteBatchNode添加到当前层里面去,这样就可以绘制它的所有的孩子对象。
- 加载Sprites.plist文件,它里面包含了这张大图里面的所有的小图的位置坐标信息。这样,你以后可以非常方便地使用 spriteWithSpriteFrameName来提取一张张小图片来初使化一些精灵。
- 使用 SpaceShip.png图片来创建一个精灵,注意这张图片是大图里的一个子图。
- 使用CCDirector来获得屏幕的大小---我们接下来会用到这个大小。
- 设置飞船的位置在屏幕宽度的10%,高度的50处。注意,飞船的中心点位置默认是飞船的中心。
- 把ship当作batchNode的一个孩子添加进去,这样的话,这些精灵就会被批处理显示出来。
编译运行就可以看到spaceship停靠在左边了。
添加视差滚动
我们已经有一个很酷的飞船在屏幕上了,但是,它看起来就好像坐在那里一样,毫无生气!我们可以通过往里面添加视差滚动背景来解决这个问题。
但是,等一下,到底什么是视差滚动了?
视差滚动,简单来说,就是“移动背景中的一些图片比其它图片慢一点点”,打个比方,一个背景中的物体有远有近,近的背景移动地快(比如地面),远的背景移动地慢(比如天空),这样子就会形成景深不一样的视差效果出来。
不过,在XNA现版本的cocos2d-x中,CCParallaxNode这个做视差滚动的类貌似没完成,Test工程中也没有相关例子。这样情况下,我也不打算去使用这个类,因为我不打算去修改其源代码。
那么,视差滚动怎么做呢,不过想想,就是一些不一致的移动来引起视差效果,简单来说就是一些欺骗眼睛的玩意,学过物理的都知道,参照物的问题,如果一个相向移动的物体,以其移动的物体为参照物,那么不动的东西我们都感觉其动起来了。
所以,我们只要添加一些东西其相对移动的效果就好了。
添加bg_galaxy.png和bg_planetsunrise.png这两个文件到background文件夹。然后添加一些方法到SpaceBattleLayer中。
void initBackground() { CCSprite galaxy = CCSprite.spriteWithFile(@"background/bg_galaxy"); galaxy.position = new CCPoint(1500, winSize.height / 2); galaxy.runAction(CCSequence.actions(CCMoveTo.actionWithDuration(20, new CCPoint(0, winSize.height * 0.3f)), CCCallFuncN.actionWithTarget(this, galaxyMoveDone))); this.addChild(galaxy, -1); CCSprite planetsunrise = CCSprite.spriteWithFile(@"background/bg_planetsunrise"); planetsunrise.position = new CCPoint(900, 0); planetsunrise.runAction(CCSequence.actions(CCMoveTo.actionWithDuration(12, new CCPoint(0, 0)), CCCallFuncN.actionWithTarget(this, planetMoveDone))); this.addChild(planetsunrise, -1); } void galaxyMoveDone(object sender) { CCSprite sprite = sender as CCSprite; sprite.position = new CCPoint(1500, winSize.height * 0.3f); sprite.runAction(CCSequence.actions(CCMoveTo.actionWithDuration(20, new CCPoint(0, winSize.height * 0.3f)), CCCallFuncN.actionWithTarget(this, galaxyMoveDone))); } void planetMoveDone(object sender) { CCSprite sprite = sender as CCSprite; sprite.position = new CCPoint(900, 0); sprite.runAction(CCSequence.actions(CCMoveTo.actionWithDuration(12, new CCPoint(0, 0)), CCCallFuncN.actionWithTarget(this, planetMoveDone))); }
上面做了些什么呢,添加了两个背景精灵,两个距离屏幕挺遥远的,不过它们在移动,移动进入屏幕,出去屏幕后重新设定其为原来位置重新移动,这样,这两个精灵(黑洞和星球)就无限次的从屏幕右边移动到左边。
在init方法最下面添加代码:
initBackground();
编译运行,当然,现在看起来飞船感觉已经在动了。
添加星星
现在我们来添加星星,用到一个我们以前教程从来没用过的东西---粒子系统。这个系统有什么优势呢,可以实现实现一些真实的效果,这些效果(如,火焰,雪花等)都是由大量微粒组合而形成的。cocos2d-x也提供了挺多的封装,比如火焰,学坏等效果,具体可以看Test工程关于例子系统的部分。在这里只是简单使用下。
另外,粒子系统里面有太多的参数,这些要理解起来挺费劲的。这里提供两张列表来参考理解。
现在,先把Particle文件夹添加到Content工程中,和上面的一样,那些plist文件要设置其属性的ContentProcessor和ContentImporter。这些plist文件是干什么用的呢,在plist文件里面,定义了粒子系统所需要的属性,这些plist文件是用粒子系统编辑器做的,可惜的是,我没找到在window下用的粒子系统编辑器,如果有朋友有,麻烦提供个。
这些plist文件原来是子龙山人翻译的文章原作者ray做的。我没有编辑器,我就直接打开plist文件硬编码我需要修改的数据。不过,图片是我画的,PS水平在会用的程度就是这个水平了。没办法,没有美工的日子。。。。
那么,现在在InitBackground方法添加代码:
List<string> starsList = new List<string>() { "Particles/Stars1", "Particles/Stars2", "Particles/Stars3" }; foreach (string stars in starsList) { CCParticleSystemQuad starsEffect = CCParticleSystemQuad.particleWithFile(stars); this.addChild(starsEffect, -1); }
上面的代码,就是根据plist文件初始化了粒子系统,然后添加到层中。如果你查看plist文件里面的定义,其实也挺简单的定义,就是定义了些粒子发射的速度,生命周期等内容。速度这类都是以像素为单位。剩下的设定可以参照上面列表来理解应该不难了。
现在编译运行,可以看到满天的星星了。
满天的怪物
曾经玩FC的这类射击游戏的时候,不知道大家有没有思考这么一个问题,怪物是怎么每次都在一定的地点出现?是背景的关系?其实细想,背景的滚动,也是时间的流逝,那么规定在特定的时间怪物出现,和在一定背景怪物出现的设定其实是一样的。所以,在这个游戏中,怪物的出现是有时间来决定的。而且,这个游戏的时长设定为60秒,60秒后,BOSS出现,打倒BOSS战斗就胜利了。
那么来添加怪物吧。
先添加一个声明:
CCSprite BOSS;
下面添加几个方法:
void initMonster1() { for (int i = 0; i < 5; i++) { CCSprite monster1 = CCSprite.spriteWithSpriteFrameName(@"monster1.png"); monster1.tag = attackShipTag; monster1.position = new CCPoint(winSize.width + monster1.contentSize.width / 2 + i * (monster1.contentSize.width + 30), winSize.height * 0.7f); float time = (monster1.position.x - 200) / 200; monster1.runAction(CCSequence.actions(CCMoveTo.actionWithDuration(time, new CCPoint(winSize.width / 4, winSize.height * 0.7f)), CCDelayTime.actionWithDuration(0.2f), CCMoveTo.actionWithDuration(4, new CCPoint(winSize.width * 0.75f, -monster1.contentSize.height / 2)), CCCallFuncN.actionWithTarget(this, spriteMoveDone))); batchNode.addChild(monster1); CCSprite monster2 = CCSprite.spriteWithSpriteFrameName(@"monster1.png"); monster2.tag = attackShipTag; monster2.position = new CCPoint(winSize.width + monster2.contentSize.width / 2 + i * (monster2.contentSize.width + 30), winSize.height * 0.3f); monster2.runAction(CCSequence.actions(CCMoveTo.actionWithDuration(time, new CCPoint(winSize.width / 4, winSize.height * 0.3f)), CCDelayTime.actionWithDuration(0.2f), CCMoveTo.actionWithDuration(4, new CCPoint(winSize.width * 0.75f, winSize.height + monster2.contentSize.height / 2)), CCCallFuncN.actionWithTarget(this, spriteMoveDone))); batchNode.addChild(monster2); } } void initMonster2() { Random random = new Random(); for (int i = 0; i < 5; i++) { CCSprite monster2 = CCSprite.spriteWithSpriteFrameName(@"monster2.png"); int randomSize = random.Next() % 10; monster2.tag = attackShipTag; float firstPosY = (winSize.height * randomSize / 10 + 150 + i * 70) % winSize.height; float secondPosY = (firstPosY + i * 80) % winSize.height; monster2.position = new CCPoint(winSize.width + monster2.contentSize.width / 2, winSize.height * randomSize / 10); monster2.runAction(CCSequence.actions(CCMoveTo.actionWithDuration(4, new CCPoint(100, firstPosY)), CCDelayTime.actionWithDuration(0.2f), CCMoveTo.actionWithDuration(3, new CCPoint(winSize.width + monster2.contentSize.width / 2, secondPosY)))); batchNode.addChild(monster2); } } void initBOSS() { BOSS = CCSprite.spriteWithSpriteFrameName(@"EnemySpaceShip.png"); BOSS.position = new CCPoint(winSize.width + BOSS.contentSize.width / 2, winSize.height / 2); BOSS.tag = bossTag; float x = winSize.width - BOSS.contentSize.width / 2; batchNode.addChild(BOSS); var move = CCSequence.actions( CCMoveTo.actionWithDuration(0.6f, new CCPoint(x, winSize.height * 0.7f)), CCCallFunc.actionWithTarget(this, bossShoot), CCMoveTo.actionWithDuration(0.4f, new CCPoint(x, winSize.height - BOSS.contentSize.height / 2)), CCMoveTo.actionWithDuration(0.6f, new CCPoint(x, winSize.height * 0.7f)), CCCallFunc.actionWithTarget(this, bossShoot), CCMoveTo.actionWithDuration(0.4f, new CCPoint(x, winSize.height / 2)), CCMoveTo.actionWithDuration(0.6f, new CCPoint(x, winSize.height * 0.3f)), CCCallFunc.actionWithTarget(this, bossShoot), CCMoveTo.actionWithDuration(0.4f, new CCPoint(x, BOSS.contentSize.height / 2)), CCMoveTo.actionWithDuration(0.6f, new CCPoint(x, winSize.height * 0.3f)), CCCallFunc.actionWithTarget(this, bossShoot), CCMoveTo.actionWithDuration(0.4f, new CCPoint(x, winSize.height / 2)) ); BOSS.runAction(CCSequence.actions( CCMoveTo.actionWithDuration(2, new CCPoint(x, winSize.height / 2)), CCDelayTime.actionWithDuration(0.5f), CCRepeat.actionWithAction(move,1000) )); } void bossShoot() { CCSprite bossBullet = CCSprite.spriteWithSpriteFrameName(@"BOSSbullet1.png"); bossBullet.position = new CCPoint(BOSS.position.x, BOSS.position.y); bossBullet.tag = attackShipTag; bossBullet.runAction(CCSequence.actions( CCMoveTo.actionWithDuration(3, new CCPoint(-bossBullet.contentSize.width / 2, bossBullet.position.y)), CCCallFuncN.actionWithTarget(this, spriteMoveDone))); batchNode.addChild(bossBullet); }
在BOSS之前,我们有两种怪物,其实就是前面用到的两个简单怪物。
想想以前的游戏,怪物当然不只是前进一个动作,往往都有一些其他的动作,所以上面的代码就做了这么一些动作。
在initMonster1方法中,定义了两组怪物,每组5个,动作都是从右边屏幕进入直线运动到一定位置,然后一定角度折线返回。当然,为了让人觉得是从右边屏幕进入,那么其位置的X值要大于屏幕的值。而且,为了其不会重叠并且排队进入,定义了一样的速度并且两个之间间隔一些距离。这样的两组怪物,大家可以直接在init方法中添加initMonster1()来测试其效果。
在initMonster2方法中,用到的是和monster1一样的颜色不一样的怪物。他做的动作从屏幕右边随机一个点进到一定距离的一个随机点然后折返回到屏幕右边的一个随机点,求余是为了其高度不至于超过屏幕高度。当然,你可以直接在init()方法中测试其效果。
在initBOSS()方法中,这里的Action看起来有点复杂,其实做的效果也挺简单的,BOSS一开始要从屏幕右边进入到靠在屏幕右边上,然后做一些上下移动的动作,然后在两个位置发射红色的大面条枪。并且重复这些动作。当然,也能够在init()方法中测试其效果。
现在,添加一些变量:
float gameTime = 0; int hp = 10; int BOSShp = 20; List<CCSprite> hpSprites;
gameTime是计算时间,另外hpSprites来保存血条的。
添加一个方法:
void initShipHP() { hpSprites = new List<CCSprite>(); for (int i = 0; i < 10; i++) { CCSprite hpSpriteBlock = CCSprite.spriteWithSpriteFrameName(@"life.png"); hpSpriteBlock.position = new CCPoint(hpSpriteBlock.contentSize.width / 2 + i * hpSpriteBlock.contentSize.width, winSize.height - hpSpriteBlock.contentSize.height / 2); batchNode.addChild(hpSpriteBlock); hpSprites.Add(hpSpriteBlock); } }
在init方法添加
initShipHP();
这样,在屏幕的左上角就能看到10个小方块,代表血条。
接下来添加一个方法:
void initHpfood() { CCSprite hpfood = CCSprite.spriteWithSpriteFrameName(@"lifefood.png"); hpfood.tag = lifefoodTag; hpfood.position = new CCPoint(winSize.width + hpfood.contentSize.width / 2, winSize.height * 0.7f); hpfood.runAction(CCSequence.actions( CCMoveTo.actionWithDuration(10.0f, new CCPoint(0, winSize.height * 0.7f)), CCCallFuncN.actionWithTarget(this, spriteMoveDone))); batchNode.addChild(hpfood); }
这个方法是干什么的呢,我们在游戏中往往有一些东西,可以增加些东西,在这个游戏中,我们设定了这个图标代码血,只要能够吃到就能够增加一滴血,这个只是初始化。血块也是从屏幕右边往左边移动。
下面添加一个代码来管理游戏流程:
void gameSchedule(float dt) { gameTime += dt; if ((int)gameTime >= 60) { initBOSS(); } else if ((int)gameTime % 6 == 0) { initMonster1(); } else if ((int)gameTime % 10 == 0) { initMonster2(); } else if ((int)gameTime == 11 || (int)gameTime == 33) { initHpfood(); } }
这个方法定义每6秒怪物1出现一次,每10秒怪物2出现一次,在11秒和33秒的时候,血块出现,60秒BOSS出现。
然后在init()添加:
this.schedule(gameSchedule, 1.0f);
设置为一秒运行一次。
下面添加一行在initBOSS开始处:
this.unschedule(gameSchedule);
定义在BOSS出现的时候,游戏进行到了最后,那么游戏流程函数就取消周期执行了。
现在运行, 就可以看到我们定义的游戏流程。大批的怪物,傻逼的BOSS。而且在BOSS出现后,小怪都不会出现了
飞船射击
当然,不能一个劲的挨打(虽然没打中),我们来添加射击吧。虽然是可恶的面条枪(我印象中,面条枪在这么多射击游戏是最菜的)。
添加代码:
void shoot() { CCSprite bullet = CCSprite.spriteWithSpriteFrameName(@"weapon.png"); bullet.tag = bulletTag; bullet.position = new CCPoint(ship.position.x, ship.position.y); bullet.runAction(CCSequence.actions(CCMoveTo.actionWithDuration(3, new CCPoint(winSize.width, ship.position.y)), CCCallFuncN.actionWithTarget(this, spriteMoveDone))); batchNode.addChild(bullet); } public override void ccTouchesEnded(List<CCTouch> touches, CCEvent event_) { shoot(); } //in the init method this.isTouchEnabled = true;
上面的代码,我们注册了触摸事件,并且在触摸结束的时候,发射面条,当然,子弹都是从Spaceship位置水平射向右边。
现在,编译运行,太空船就能射击了。
添加重力感应
单靠射击并不能完全防卫,打不过还得跑。现在添加重力感应,用重力加速器来移动Spaceship。在下面,进行的都是真机测试,没有机子的朋友,虽然电脑也能模拟重力感应,但是控制起来相当麻烦。我也没其他办法用模拟器来完成这个测试。
重力加速器,在cocos2d-x这个版本中并没有封装,那么我们只有用基础的XNA的方法了。
添加Microsoft.Devices.Sensors的引用。
添加一个声明:
Accelerometer gSensor;
然后在init方法添加代码:
if (Accelerometer.IsSupported) { gSensor = new Accelerometer(); gSensor.CurrentValueChanged += new EventHandler<SensorReadingEventArgs<AccelerometerReading>>(gSensor_CurrentValueChanged); gSensor.Start(); }
上面我们先判断加速器是否可用,然后注册检测事件,然后让加速器启动。
下面添加方法:
void gSensor_CurrentValueChanged(object sender, SensorReadingEventArgs<AccelerometerReading> e) { Vector3 vector3 = e.SensorReading.Acceleration; CCPoint newPos = new CCPoint(ship.position.x, ship.position.y); if (vector3.Y > 0.3) { newPos.x -= 10; if (newPos.x <= ship.contentSize.width / 2) { newPos.x = ship.contentSize.width / 2; } ship.DisplayFrame = CCSpriteFrameCache.sharedSpriteFrameCache().spriteFrameByName("SpaceShip.png"); ship.position = newPos; } else if (vector3.Y >= -0.3 && vector3.Y <= 0.3) { ship.DisplayFrame = CCSpriteFrameCache.sharedSpriteFrameCache().spriteFrameByName("SpaceShip.png"); } else { newPos.x += 10; if (newPos.x >= winSize.width - ship.contentSize.width / 2) { newPos.x = winSize.width - ship.contentSize.width / 2; } ship.DisplayFrame = CCSpriteFrameCache.sharedSpriteFrameCache().spriteFrameByName("SpaceShipAccel.png"); ship.position = newPos; } if (vector3.X < -0.15) { newPos.y -= 7; if (newPos.y < ship.contentSize.height / 2) { newPos.y = ship.contentSize.height / 2; } ship.position = newPos; } else if (vector3.X >= -0.15 && vector3.X < 0.15) { } else { newPos.y += 7; if (newPos.y > winSize.height - ship.contentSize.height / 2) { newPos.y = winSize.height - ship.contentSize.height / 2; } ship.position = newPos; } }
上面,我们做的是在加速器值改变的时候改变Spaceship的位置,上面还有一个特点就是在Spaceship往右边移动的时候,换了一张图片。一张显示船加速的图片。在不是加速的情况下换回来原来的图片。
这个,如果有图片的话,我们所有方向的动作都可以有不同的显示了。
再添加一个方法:
public override void onExit() { if(gSensor != null) gSensor.Stop(); base.onExit(); }
当离开的时候,把加速器关了,虽然不知道是否是必要的,但是关了还是安全些的。
现在运行,在真机上面,可以不错的移动了。并且,可以随意的射击了。
碰撞检测
现在,虽然能够射击了,并且已经有大量的怪物,但是,是不是少了点什么,子弹碰到怪物它居然不死亡,并且,怪物碰到Spaceship也不掉血。现在我们就来完成这个吧。碰撞检测。
在这里,我打算用BOX2D做碰撞检测,毕竟现在我们拥有了大量的怪物,大量的子弹,并且是不定性的参与战斗,如果用普通的方法来做碰撞检测的话,精灵管理起来就相当麻烦,检测的时候一遍遍的遍历我也觉得挺麻烦的,容易出错。不过,用BOX2D就不会发生这个情况。
如果您对BOX2D不了解,或者从来也没有使用过,可以到我博客看<cocos2d-x for wp7>在cocos2d-x里面使用BOX2D和<cocos2d-x for wp7>使用box2d来做碰撞检测(且仅用来做碰撞检测)这类文章。
现在,先添加BOX2D.XNA.DLL的引用。
添加声明:
const double PTM_RATIO = 32.0; World world;
在init方法开头初始化world。
Vector2 gravity = new Vector2(0, 0); bool doSleep = false; world = new World(gravity, doSleep);
这些东西在<cocos2d-x for wp7>使用box2d来做碰撞检测(且仅用来做碰撞检测)都做过解释,在这里就不多说了。创建了一个世界,使之重力为0;
添加方法:
void addBoxBodyForSprite(CCSprite sprite) { BodyDef spriteBodyDef = new BodyDef(); spriteBodyDef.type = BodyType.Dynamic; spriteBodyDef.position = new Vector2((float)(sprite.position.x / PTM_RATIO), (float)(sprite.position.y / PTM_RATIO)); spriteBodyDef.userData = sprite; Body spriteBody = world.CreateBody(spriteBodyDef); PolygonShape spriteShape = new PolygonShape(); spriteShape.SetAsBox((float)(sprite.contentSize.width / PTM_RATIO / 2), (float)(sprite.contentSize.height / PTM_RATIO / 2)); FixtureDef spriteShapeDef = new FixtureDef(); spriteShapeDef.shape = spriteShape; spriteShapeDef.density = 10.0f; spriteShapeDef.isSensor = true; spriteBody.CreateFixture(spriteShapeDef); }
上面的方法是为精灵创建body。具体解释碰撞检测的文章说过,这个不多说。
现在,要为添加过的精灵创建body了。我们想想,我们要为那些添加碰撞呢,Spaceship,monster1,Monster2,BOSS,BOSS的大面条枪,Spaceship的小面条枪,血块。应该就是这些吧。
那么,就在init()创建Spaceship的时候,initMonster1()方法,initMonster2()方法,initBOSS()方法,bossShoot()方法,shoot(),initHpfood()方法的设置tag下面添加这么一行
this.addBoxBodyForSprite(sprite);注意,这里的参数是要创建body的精灵引用。另外,initMonster1方法中,那里可是两队怪物哦,不要漏了。
我们的sprites被销毁的时候,我们需要销毁Box2d的body。因此,把你的spriteMoveDone方法改写成下面的形式:
void spriteMoveDone(object sender) { CCSprite sprite = sender as CCSprite; Body spriteBody = null; for (Body b = world.GetBodyList(); b != null; b = b.GetNext()) { if (b.GetUserData() != null) { CCSprite curSprite = (CCSprite)b.GetUserData(); if (sprite == curSprite) { spriteBody = b; } } } if (spriteBody != null) { world.DestroyBody(spriteBody); } sprite.visible = false; }
现在,最重要的一部分,我们需要根据sprite的位置改变来更新box2d的body。因此,在init方法里面添加下面一行代码:
this.schedule(tick);
添加一个tick方法,我们现在是基于cocos2d的精灵位置来更新box2d的body位置。
void tick(float dt) { world.Step(dt, 10, 10); for (Body b = world.GetBodyList(); b != null; b = b.GetNext()) { if (b.GetUserData() != null) { CCSprite sprite = (CCSprite)b.GetUserData(); Vector2 position = new Vector2((float)(sprite.position.x / PTM_RATIO), (float)(sprite.position.y / PTM_RATIO)); float angle = -1 * MathHelper.ToRadians(sprite.rotation); b.SetTransform(position, angle); } } }
我们将往world对象里面注册一个contact listener对象。写法和之前的一样,如果你没写过,请直接到<cocos2d-x for wp7>使用cocos2d-x和BOX2D来制作一个BreakOut(打砖块)游戏(二)里面复制代码即可。把MyContactListener类添加到Classes文件夹。
往SpaceBattleLayer类中添加声明:
MyContactListener contactListener;
然后,在init方法中的world创建后面加入下面的代码:
contactListener = new MyContactListener(); world.ContactListener = contactListener;
最后,在tick方法的底部加入下面的代码:
List<Body> toDestroy = new List<Body>(); //foreach (var item in contactListener.contacts) for (int i = 0; i < contactListener.contacts.Count;i++ ) { MyContact item = contactListener.contacts[i]; Body bodyA = item.fixtureA.GetBody(); Body bodyB = item.fixtureB.GetBody(); if (bodyA.GetUserData() != null && bodyB.GetUserData() != null) { CCSprite spriteA = (CCSprite)bodyA.GetUserData(); CCSprite spriteB = (CCSprite)bodyB.GetUserData(); if ((spriteA.tag == shipTag && spriteB.tag == attackShipTag) || (spriteA.tag == attackShipTag && spriteB.tag == shipTag)) { if (spriteB.tag == attackShipTag) toDestroy.Add(bodyB); else toDestroy.Add(bodyB); hp--; hpSprites.LastOrDefault().visible = false; hpSprites.Remove(hpSprites.LastOrDefault()); if (hp <= 0) { GameOverScene pScene = new GameOverScene(false); CCDirector.sharedDirector().replaceScene(pScene); } } else if ((spriteA.tag == bulletTag && spriteB.tag == attackShipTag) || (spriteB.tag == bulletTag && spriteA.tag == attackShipTag)) { toDestroy.Add(bodyA); toDestroy.Add(bodyB); } else if ((spriteA.tag == bulletTag && spriteB.tag == bossTag) || (spriteB.tag == bulletTag && spriteA.tag == bossTag)) { BOSShp--; if (spriteA.tag == bulletTag) toDestroy.Add(bodyA); else toDestroy.Add(bodyB); if (BOSShp <= 0) { GameOverScene pScene = new GameOverScene(true); CCDirector.sharedDirector().replaceScene(pScene); } } else if ((spriteA.tag == shipTag && spriteB.tag == lifefoodTag) || (spriteA.tag == lifefoodTag && spriteB.tag == shipTag)) { if (spriteB.tag == lifefoodTag) toDestroy.Add(bodyB); else toDestroy.Add(bodyB); hp++; CCSprite hpSprite = CCSprite.spriteWithSpriteFrameName(@"life.png"); hpSprite.position = new CCPoint(hp * hpSprite.contentSize.width - hpSprite.contentSize.width / 2, winSize.height - hpSprite.contentSize.height / 2); batchNode.addChild(hpSprite); hpSprites.Add(hpSprite); } } foreach (var itemToDestroy in toDestroy) { if (itemToDestroy.GetUserData() != null) { CCSprite sprite = (CCSprite)itemToDestroy.GetUserData(); sprite.visible = false; } world.DestroyBody(itemToDestroy); } }
上面的tick方法我们做了什么呢,遍历所有碰撞点,然后判断两个碰撞物体的类型,在这里,你就知道我们在上面设置的tag的作用了。上面判断了各种碰撞的结果。我觉得代码应该是可以看懂的,就不解释了。
现在添加一个类,GameOverScene这个类。来显示胜利/失败界面。
代码如下:
class GameOverScene:CCScene { public GameOverScene(bool isWin) { string msg; if (isWin) msg = "YOU WIN!"; else msg = "YOU LOSE"; CCLabelBMFont label = CCLabelBMFont.labelWithString(msg, @"resource/Arial"); label.position = new CCPoint(400, 240); this.addChild(label); this.runAction(CCSequence.actions(CCDelayTime.actionWithDuration(3.0f), CCCallFunc.actionWithTarget(this, gotoPlay))); } void gotoPlay() { SpaceBattleScene pScene = new SpaceBattleScene(); CCDirector.sharedDirector().replaceScene(pScene); } }
这里用了一个以前从来没有使用过的label。这个label可以使用特殊的字体。我们把Arial.fnt放在resource文件夹,Arial.png放在resource/images里面。另外,设置Arial.fnt的属性ContentImporter和ContentProcessor分别为TextImporter和TextProcessor。
这个fnt格式,大家打开看的话,其实就是一堆数据。关于坐标x,y,每个字符大小等相关数据,我也没发现有相关编辑器,里面的数据我也不是全部懂。觉得大概的就是读取数据,然后获取字符的位置,然后用Texture的方式取出显示出来。如果有谁有编辑器或者资料,可以提供。
现在,我们已经拥有了一个不错的游戏,不错的射击,满屏的怪物,运动的场景,傻逼的BOSS。现在来添加一些音效吧。添加CocosDenshion引用。
把laser_ship.wav和laser_enemy.wav添加到music文件夹。
在shoot方法最后添加:
SimpleAudioEngine.sharedEngine().playEffect(@"music/laser_ship");
在bossShoot方法最后添加:
SimpleAudioEngine.sharedEngine().playEffect(@"music/laser_enemy");
好了,到这里,本次教程就结束了。我们已经拥有了一个不错的游戏,不错的射击,满屏的怪物,运动的场景,傻逼的BOSS,WIN/LOSE界面。。。
细心的朋友应该发现了。其实碰撞没有想象中好,主要原因是MyContactListener类的原因,在EndContact的时候,应该是找到那个contact然后移除并不是直接clear。直接clear的话,就会把有些碰撞点忽略掉了。所以有些碰撞没有实现,但是现在,我并没有找到更好的方式来写这个类,主要原因我在<cocos2d-x for wp7>使用cocos2d-x和BOX2D来制作一个BreakOut(打砖块)游戏(二)做过说明。如果有好方法的朋友,麻烦提供个。
示例代码下载:http://dl.dbank.com/c08fkji48i
何去何从:
- 把玩家,怪物等信息重构出来,添加多关卡
- 添加更多的怪物,更多的战斗方式
posted on 2012-05-11 14:53 fengyun1989 阅读(3306) 评论(1) 编辑 收藏 举报
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库