植物大战僵尸一直是一个很受欢迎的经典的小游戏,我主要用cocos2d-android做了一个类似的小demo,在这里主要介绍一下我做给这个小demo。
开发前各种准备工作
做一个小游戏我们首先要有一个地图吧,所以我用tiled这个软件来制作地图,安装和使用都挺简单了,画好后用notepad++打开看一下图片路径对不对,然后把图片、字体文件、地图文件.ttf放到工程的assets目录下,然后我们就可以在后面使用这些资源了。
当然我,我们先来了解一下一些其他相关知识点
加载地图
CCTMXTiledMap map=CCTMXTiledMap.tiledMap("map.tmx");this.addChild(map);
解析地图
//解析地图
private void parseMap() {roadPoints=new ArrayList<CGPoint>();CCTMXObjectGroup objectGroupNamed=map.objectGroupNamed("road");ArrayList<HashMap<String,String>> objects=objectGroupNamed.objects;for(HashMap<String,String> hashMap:objects){int x=Integer.parseInt(hashMap.get("x"));int y=Integer.parseInt(hashMap.get("y"));CGPoint cgPoint=ccp(x,y);roadPoints.add(cgPoint);}}
//展示僵尸
int position=3;
private void loadZombies() {CCSprite sprite=CCSprite.sprite("z_1_01.png");sprite.setPosition(roadPoints.get(position));sprite.setAnchorPoint(0.5f,0);sprite.setScale(0.65f);this.addChild(sprite);}
粒子系统
eg:飘雪:CCParticleSnow
private void loadParticle() {system = CCParticleSnow.node();// 设置雪花的样式system.setTexture(CCTextureCache.sharedTextureCache().addImage("f.png"));this.addChild(system, 1);
}system.stopSystem();// 停止粒子系统
自定义效果,后缀.plis
声音引擎
SoundEngine engine=SoundEngine.sharedEngine();// 1 上下文 2. 音乐资源的id 3 是否循环播放engine.playSound(CCDirector.theApp, R.raw.psy, true);
暂停和继续
1.onExit();
2.onEnter();
@Override
public boolean ccTouchesBegan(MotionEvent event) {this.onExit(); // 暂停this.getParent().addChild(new PauseLayer());// 让场景添加新的图层 return super.ccTouchesBegan(event);
}// 专门用来暂停的图层
private class PauseLayer extends CCLayer{private CCSprite heart;public PauseLayer(){this.setIsTouchEnabled(true);// 打开触摸事件的开关heart = CCSprite.sprite("heart.png");// 获取屏幕的尺寸CGSize winSize = CCDirector.sharedDirector().getWinSize();heart.setPosition(winSize.width/2, winSize.height/2);// 让图片再屏幕的中间this.addChild(heart);}// 当点击PauseLayer的时候 @Overridepublic boolean ccTouchesBegan(MotionEvent event) {CGRect boundingBox = heart.getBoundingBox();// 把Android坐标系中的点 转换成Cocos2d坐标系中的点 CGPoint convertTouchToNodeSpace = this.convertTouchToNodeSpace(event);if(CGRect.containsPoint(boundingBox, convertTouchToNodeSpace)){// 确实点击了心this.removeSelf();// 回收当前图层DemoLayer.this.onEnter();//游戏继续}return super.ccTouchesBegan(event);}
}
项目正式开始
首先要有一个logo,logo下面有一个背景图片,需要加载一个进度条,一步步跳转就可以了,说到这个进度条,其实就是一个帧动画
用下面这段代码来看一下,当然这里之后我抽取了一个CommonUtils工具类,
private void loading() {CCSprite loading=CCSprite.sprite("image/loading/loading_01.png");loading.setPosition(winSize.width/2, 30);this.addChild(loading);CCAction animate = CommonUtils.getAnimate("image/loading/loading_%02d.png", 9, false);loading.runAction(animate);start = CCSprite.sprite("image/loading/loading_start.png");start.setPosition(winSize.width/2, 30);start.setVisible(false);// 暂时不可见this.addChild(start);}
当然我们可以来看一下这个工具类,这在之后的开发中有很多的实用价值:
`public class CommonUtils {/*** 切换图层 * @param newLayer 新进入的图层*/public static void changeLayer(CCLayer newLayer){CCScene scene=CCScene.node();scene.addChild(newLayer);CCFlipXTransition transition=CCFlipXTransition.transition(2, scene, 0);CCDirector.sharedDirector().replaceScene(transition);//切换场景 ,参数 新的场景}/*** 解析地图上 对象的所有点* @param map 地图* @param name 对象的名字* @return*/public static List<CGPoint> getMapPoints(CCTMXTiledMap map,String name){List<CGPoint> points = new ArrayList<CGPoint>();// 解析地图CCTMXObjectGroup objectGroupNamed = map.objectGroupNamed(name);ArrayList<HashMap<String, String>> objects = objectGroupNamed.objects;for (HashMap<String, String> hashMap : objects) {int x = Integer.parseInt(hashMap.get("x"));int y = Integer.parseInt(hashMap.get("y"));CGPoint cgPoint = CCNode.ccp(x, y);points.add(cgPoint);}return points;}/*** 创建了序列帧的动作* @param format 格式化的路径* @param num 帧数* @param isForerver 是否永不停止的循环* @return*/public static CCAction getAnimate(String format,int num,boolean isForerver){ArrayList<CCSpriteFrame> frames=new ArrayList<CCSpriteFrame>();//String format="image/loading/loading_%02d.png";for(int i=1;i<=num;i++){CCSpriteFrame spriteFrame = CCSprite.sprite(String.format(format, i)).displayedFrame();frames.add(spriteFrame);}CCAnimation anim=CCAnimation.animation("", 0.2f, frames);// 序列帧一般必须永不停止的播放 不需要永不停止播放,需要指定第二个参数 falseif(isForerver){CCAnimate animate=CCAnimate.action(anim);CCRepeatForever forever=CCRepeatForever.action(animate);return forever;}else{CCAnimate animate=CCAnimate.action(anim,false);return animate;}}
}`
至于其他小的细节我就不一一啰嗦了,只说一下僵尸和植物对战需要的几个关键代码:
/*** 处理游戏开始后的操作* * */
public class GameCotroller {private GameCotroller() {}private static GameCotroller cotroller = new GameCotroller();public static GameCotroller getInstance() {return cotroller;}public static boolean isStart; // 游戏是否开始private CCTMXTiledMap map;private List<ShowPlant> selectPlants;private static List<FightLine> lines; // 管理了五行private List<CGPoint> roadPoints;static {lines = new ArrayList<FightLine>();for (int i = 0; i < 5; i++) {FightLine fightLine = new FightLine(i);lines.add(fightLine);}}/*** 开始游戏* * @param map* 游戏的地图* @param selectPlants* 玩家已选植物的集合*/public void startGame(CCTMXTiledMap map, List<ShowPlant> selectPlants) {isStart = true;this.map = map;this.selectPlants = selectPlants;loadMap();// 添加僵尸// 定时器// 参数1 方法名(方法带float类型的参数) 参数2 调用方法的对象 参数3 间隔时间 参数4 是否暂停CCScheduler.sharedScheduler().schedule("addZombies", this, 1,false);// CCCallFunc.action(target, selector)// addZombies();// 安放植物// 僵尸攻击植物// 植物攻击僵尸progress();}CGPoint[][] towers = new CGPoint[5][9];private void loadMap() {roadPoints = CommonUtils.getMapPoints(map, "road");for (int i = 1; i <= 5; i++) {List<CGPoint> mapPoints = CommonUtils.getMapPoints(map,String.format("tower%02d", i));for (int j = 0; j < mapPoints.size(); j++) {towers[i - 1][j] = mapPoints.get(j);}}}/**** 添加僵尸* * @param t*/public void addZombies(float t) {Random random = new Random();int lineNum = random.nextInt(5);// [0-5)PrimaryZombies primaryZombies = new PrimaryZombies(roadPoints.get(lineNum * 2), roadPoints.get(lineNum * 2 + 1));map.addChild(primaryZombies,1);// 让僵尸一直在植物的上面lines.get(lineNum).addZombies(primaryZombies);// 把僵尸记录到行战场中progress+=5;progressTimer.setPercentage(progress);//设置新的进度}public void endGame() {isStart = false;}private ShowPlant selectPlant; // 玩家选择的植物private Plant installPlant;/*** 当游戏开始后处理点击事件的方法* * @param point* 点击到的点*/public void handleTouch(CGPoint point) {CCSprite chose = (CCSprite) map.getParent().getChildByTag(FightLayer.TAG_CHOSE);if (CGRect.containsPoint(chose.getBoundingBox(), point)) {// 认为玩家有可能选择了植物if (selectPlant != null) {selectPlant.getShowSprite().setOpacity(255);selectPlant = null;}for (ShowPlant plant : selectPlants) {CGRect boundingBox = plant.getShowSprite().getBoundingBox();if (CGRect.containsPoint(boundingBox, point)) {// 玩家选择了植物selectPlant = plant;selectPlant.getShowSprite().setOpacity(150);int id = selectPlant.getId();switch (id) {case 1:installPlant =new PeasePlant();break;case 4:installPlant = new Nut();break;default:break;}}}} else {// 玩家有可能安放植物if (selectPlant != null) {int row = (int) (point.x / 46) - 1; // 1-9 0-8int line = (int) ((CCDirector.sharedDirector().getWinSize().height - point.y) / 54) - 1;// 1-5// 0-4// 限制安放的植物的范围if (row >= 0 && row <= 8 && line >= 0 && line <= 4) {// 安放植物// selectPlant.getShowSprite().setPosition(point);// installPlant.setPosition(point); // 坐标需要修改installPlant.setLine(line);// 设置植物的行号installPlant.setRow(row); // 设置植物的列号installPlant.setPosition(towers[line][row]); // 修正了植物的坐标FightLine fightLine = lines.get(line);if (!fightLine.containsRow(row)) { // 判断当前列是否已经添加了植物 如果添加了 就不能再添加了fightLine.addPlant(installPlant);// 把植物记录到了行战场中map.addChild(installPlant);}}installPlant = null;selectPlant.getShowSprite().setOpacity(255);selectPlant = null;// 下次安装需要重新选择}}}CCProgressTimer progressTimer;int progress=0;private void progress() {// 创建了进度条progressTimer = CCProgressTimer.progressWithFile("image/fight/progress.png");// 设置进度条的位置 progressTimer.setPosition(CCDirector.sharedDirector().getWinSize().width - 80, 13);map.getParent().addChild(progressTimer); //图层添加了进度条 progressTimer.setScale(0.6f); // 设置了缩放 progressTimer.setPercentage(0);// 每增加一个僵尸需要调整进度,增加5progressTimer.setType(CCProgressTimer.kCCProgressTimerTypeHorizontalBarRL); // 进度的样式CCSprite sprite = CCSprite.sprite("image/fight/flagmeter.png");sprite.setPosition(CCDirector.sharedDirector().getWinSize().width - 80, 13);map.getParent().addChild(sprite);sprite.setScale(0.6f);CCSprite name = CCSprite.sprite("image/fight/FlagMeterLevelProgress.png");name.setPosition(CCDirector.sharedDirector().getWinSize().width - 80, 5);map.getParent().addChild(name);name.setScale(0.6f);}}
还有一个很关键的就是:
/*** 对战界面的图层* */
public class FightLayer extends BaseLayer {public static final int TAG_CHOSE = 10;private CCTMXTiledMap map;private List<CGPoint> zombilesPoints;private CCSprite choose; // 玩家可选植物的容器private CCSprite chose; // 玩家已选植物的容器public FightLayer() {init();}private void init() {loadMap();parserMap();showZombies();moveMap();}// 加载展示用的僵尸private void showZombies() {for (int i = 0; i < zombilesPoints.size(); i++) {CGPoint cgPoint = zombilesPoints.get(i);ShowZombies showZombies = new ShowZombies();showZombies.setPosition(cgPoint);// 给展示用的僵尸设置了位置map.addChild(showZombies);// 注意: 把僵尸加载到地图上}}private void parserMap() {zombilesPoints = CommonUtils.getMapPoints(map, "zombies");}// 移动地图private void moveMap() {int x = (int) (winSize.width - map.getContentSize().width);CCMoveBy moveBy = CCMoveBy.action(3, ccp(x, 0));CCSequence sequence = CCSequence.actions(CCDelayTime.action(4), moveBy, CCDelayTime.action(2),CCCallFunc.action(this, "loadContainer"));map.runAction(sequence);}private void loadMap() {map = CCTMXTiledMap.tiledMap("image/fight/map_day.tmx");map.setAnchorPoint(0.5f, 0.5f);CGSize contentSize = map.getContentSize();map.setPosition(contentSize.width / 2, contentSize.height / 2);this.addChild(map);}// 加载两个容器public void loadContainer() {chose = CCSprite.sprite("image/fight/chose/fight_chose.png");chose.setAnchorPoint(0, 1);chose.setPosition(0, winSize.height);// 设置位置是屏幕的左上角this.addChild(chose,0,TAG_CHOSE);choose = CCSprite.sprite("image/fight/chose/fight_choose.png");choose.setAnchorPoint(0, 0);this.addChild(choose);loadShowPlant();start = CCSprite.sprite("image/fight/chose/fight_start.png");start.setPosition(choose.getContentSize().width/2, 30);choose.addChild(start);}private List<ShowPlant> showPlatns; // 展示用的植物的集合// 加载展示用的植物private void loadShowPlant() {showPlatns = new ArrayList<ShowPlant>();for (int i = 1; i <= 9; i++) {ShowPlant plant = new ShowPlant(i); // 创建了展示的植物CCSprite bgSprite = plant.getBgSprite();bgSprite.setPosition(16 + ((i - 1) % 4) * 54,175 - ((i - 1) / 4) * 59);choose.addChild(bgSprite);CCSprite showSprite = plant.getShowSprite();// 获取到了展示的精灵// 设置坐标showSprite.setPosition(16 + ((i - 1) % 4) * 54,175 - ((i - 1) / 4) * 59);choose.addChild(showSprite); // 添加到了容器上showPlatns.add(plant);}setIsTouchEnabled(true);}public void unlock(){isLock=false;}private List<ShowPlant> selectPlants = new CopyOnWriteArrayList<ShowPlant>();// 已经选中植物的集合boolean isLock;boolean isDel; // 是否删除了选中的植物private CCSprite start;@Overridepublic boolean ccTouchesBegan(MotionEvent event) {// 需要把Android坐标系中的点 转换成Cocos2d坐标系中的点CGPoint point = this.convertTouchToNodeSpace(event);if(GameCotroller.isStart){// 如果游戏开始了 交给GameCtoller 处理GameCotroller.getInstance().handleTouch(point);return super.ccTouchesBegan(event);}CGRect boundingBox = choose.getBoundingBox();CGRect choseBox = chose.getBoundingBox();// 玩家有可能反选植物if(CGRect.containsPoint(choseBox, point)){isDel=false;for(ShowPlant plant:selectPlants){CGRect selectPlantBox = plant.getShowSprite().getBoundingBox();if(CGRect.containsPoint(selectPlantBox, point)){CCMoveTo moveTo=CCMoveTo.action(0.5f, plant.getBgSprite().getPosition());plant.getShowSprite().runAction(moveTo);selectPlants.remove(plant);// 走到这一步 确实代表反选植物了isDel=true;continue;// 跳出本次循环,继续下次循环}if(isDel){CCMoveBy ccMoveBy=CCMoveBy.action(0.5f, ccp(-53, 0));plant.getShowSprite().runAction(ccMoveBy);}}}else if (CGRect.containsPoint(boundingBox, point)) {if(CGRect.containsPoint(start.getBoundingBox(), point)){// 点击了一起来摇滚ready();}else if (selectPlants.size() < 5&&!isLock) { //如果已经选择满了 就不能再选择了// 有可能 选择植物for (ShowPlant plant : showPlatns) {CGRect boundingBox2 = plant.getShowSprite().getBoundingBox();if (CGRect.containsPoint(boundingBox2, point)) {// 如果点恰好落在植物展示的精灵矩形之中// 当前植物被选中了isLock=true;// System.out.println("我被选中了...");CCMoveTo moveTo = CCMoveTo.action(0.5f,ccp(75 + selectPlants.size() * 53,255));CCSequence sequence=CCSequence.actions(moveTo, CCCallFunc.action(this, "unlock"));plant.getShowSprite().runAction(sequence);selectPlants.add(plant);}}}}return super.ccTouchesBegan(event);}/*** 点击了一起来摇滚 做的操作*/private void ready() {// 缩小玩家已选植物容器chose.setScale(0.65f);// 把选中的植物重新添加到 存在的容器上for(ShowPlant plant:selectPlants){plant.getShowSprite().setScale(0.65f);// 因为父容器缩小了 孩子一起缩小plant.getShowSprite().setPosition(plant.getShowSprite().getPosition().x * 0.65f,plant.getShowSprite().getPosition().y+ (CCDirector.sharedDirector().getWinSize().height - plant.getShowSprite().getPosition().y)* 0.35f);// 设置坐标this.addChild(plant.getShowSprite());}choose.removeSelf();// 回收容器// 地图的平移int x = (int) (map.getContentSize().width-winSize.width);CCMoveBy moveBy = CCMoveBy.action(1, ccp(x, 0));CCSequence sequence=CCSequence.actions(moveBy, CCCallFunc.action(this, "preGame"));map.runAction(sequence);}private CCSprite ready;public void preGame(){ready=CCSprite.sprite("image/fight/startready_01.png");ready.setPosition(winSize.width/2, winSize.height/2);this.addChild(ready);String format="image/fight/startready_%02d.png";CCAction animate = CommonUtils.getAnimate(format, 3, false);CCSequence sequence=CCSequence.actions((CCAnimate)animate, CCCallFunc.action(this, "startGame"));ready.runAction(sequence);}public void startGame(){ready.removeSelf();// 移除中间的序列帧GameCotroller cotroller=GameCotroller.getInstance();cotroller.startGame(map,selectPlants);}
}
在做这个的过程中总是遇到空指针的异常,例如这次:后来发现是因为我ShowPlant.java这个地方写错了
做媒一个项目都需要一些成长和一些经验,在之前的CCSequence,CGPoint,CCSprite,CCTMXTiledMap
后面又学会了CCScheduler.sharedScheduler().schedule(“attackPlant”, this, 0.5f, false),ready.removeSelf();// 移除中间的序列帧等内容,让我对这整个架构有了初步了了解了实践,学习之路很长,我们一起加油!