Cocos2d-android是Cocos2dx家族中的一员,优点是使用Java语言进行游戏代码的编写,不像Cocos2dx需要使用C++ 、Lua,方便安卓程序员上手。缺点也显而易见,Cocos2dx本身使用C++开发的,Cocos2dx-android相当于做了一次Java到C的本地调用封装,因此执行效率上肯定会比较差。
作为快速上手文章(get start),使用一个小案例来演示一下Cocos2d-android的基本用法。
开发环境:Eclipse + SDK17
需要导入Cocos2d-android的相关jar文件。
案例的资源文件涉及几张图片和音乐与音效文件,可以通过下载案例的源代码去使用。
案例的效果如图所示:
屏幕左侧会有一个“忍者”,从屏幕右侧会跑出许多“敌人”向屏幕左侧移动。忍者发射飞镖射击敌人。如果忍者击毙了30个敌人则游戏成功结束,如果有敌人一直跑到屏幕左侧都没有被飞镖打死,则游戏结束。
接下来我们就使用Cocos2d-android来开发这个小游戏。
使用Cocos2d-android开发游戏,主要分为以下几个步骤:
1. Cocos2d-android的一些全局初始化
2. 场景的搭建
3. 事件的处理
4. 音乐、音效
5. 场景的切换
step1 创建安卓项目
step2 在MainActivity的onCreate方法中完成Cocos2d-android的初始化。
protected CCGLSurfaceView _glSurfaceView;
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
_glSurfaceView = new CCGLSurfaceView(this);
setContentView(_glSurfaceView);
}
这段代码中绝大部分都是使用的android原生代码。设定MainActivity界面不显示TitleBar,完全全屏且保持亮度。比较不一样的代码是声明了一个属性_glSurfaceView,属性类型为CCGLSurfaceView,CC前缀代表这是一个Cocos2d-android中的类型,该类继承自安卓的GLSurfaceView。通过调用构造器创建了_glSurfaceView将其传入了setContentView方法中。因此MainActivity连布局文件都不需要了。
step3 在MainActivity的相关生命周期方法中调用Cocos2d-android的相关方法
@Override
public void onPause()
{
super.onPause();
CCDirector.sharedDirector().pause();
}
@Override
public void onResume()
{
super.onResume();
CCDirector.sharedDirector().resume();
}
@Override
public void onStop()
{
super.onStop();
CCDirector.sharedDirector().end();
}
这里反复出现了一个CCDirector类,通过sharedDirector方法获取CCDirector的单例对象,并执行相关方法。
CCDirector顾名思义就像一个导演,而它导演的大片就是我们的游戏项目。一部大片都会有很多幕(Scene),我们的游戏项目同样也是由很多幕组成的,每一幕都用一个CCScene对象表示。你也可以将CCScene视为Android中的Activity。Android中,Activity上呈现的是一个一个按一定顺序摆放的视图(View),而在幕中呈现的是一个一个精灵(Sprite)。
Android的普通Activity里,视图要放在一个视图容器(DecoView)中,视图容器再放入Activity里进行呈现。Cocos2d-andriod中,精灵需要放到一个层(Layer)中,再将层放到幕(Scene)中显示。
public void onStart()
{
super.onStart();
CCDirector.sharedDirector().attachInView(_glSurfaceView);
CCDirector.sharedDirector().setDeviceOrientation(CCDirector.kCCDeviceOrientationLandscapeLeft);
CCDirector.sharedDirector().setDisplayFPS(true);
CCDirector.sharedDirector().setAnimationInterval(1.0f / 60.0f);
CCScene scene = GameLayer.scene();
CCDirector.sharedDirector().runWithScene(scene);
}
在Activity的onStart生命周期方法中,CCDirector类继续进行一些常规性的设置,这些都是常规性的代码,包括添加_glSurfaceView,屏幕横屏显示,显示FPS信息,设置刷新率为每秒60次。
下面的两行代码就是涉及到幕(Scene)和层(Layer)了。GameLayer是我们自己写的一个类,在scene方法中完成Layer的创建,Scene的创建,以及把Layer添加到Scene中。然后CCDirector将scene方法的返回值作为游戏的第一个界面显示在屏幕上。
接下来看一下GameLayer类:
public class GameLayer extends CCColorLayer
{
......
public static CCScene scene()
{
//获取幕(CCScene)对象
CCScene scene = CCScene.node();
//创建GameLayer对象
CCColorLayer layer = new GameLayer(ccColor4B.ccc4(255, 255, 255, 255));
//将Layer放到Scene中
scene.addChild(layer);
//返回Scene
return scene;
}
//构造器
protected GameLayer(ccColor4B color)
{
super(color);
//打开触屏事件
this.setIsTouchEnabled(true);
//视口大小
CGSize winSize =
CCDirector.sharedDirector().displaySize();
//精灵(忍者)
CCSprite player =
CCSprite.sprite("Player.png");
//设定忍者精灵在视口中的位置
player.setPosition(
CGPoint.ccp(
player.getContentSize().width / 2.0f,
winSize.height / 2.0f));
//将忍者精灵添加到Layer上
addChild(player);
......
}
......
}
我们的GameLayer通过继承CCColorLayer而成为一个Layer,可以添加各种的精灵。但Layer不能单独的显示,最终要把Layer放到一个Scene中才可以。
首先看构造方法,构造器需要提供一个颜色作为整个Layer的背景,这是父类CCColorLayer所要求的。
super(color);
然后允许Layer可触摸。这样一旦手指在Layer上产生触摸,会触发Layer的相应回调方法。
this.setIsTouchEnabled(true);
接下来获取显示区域的大小:
CGSize winSize = CCDirector.sharedDirector().displaySize();
创建一个精灵:
CCSprite player = CCSprite.sprite("Player.png");
该精灵的样子就是Player.png,而Player.png需要放在我们安卓项目的assets文件夹下:
这些图片都将是精灵的形象。Player是忍者,Target是敌人,Projectile是忍者发射的飞镖,fps_images是用来显示帧数(FPS)信息的。需要注意的是,根据下载或导入的Cocos2d-android的来源不同,有的Cocos2d-android中已经包含了fps_images图片了,就不需要额外导入了。
有了精灵后接下来就需要设置精灵的位置:
player.setPosition(
CGPoint.ccp(
player.getContentSize().width / 2.0f,
winSize.height / 2.0f));
这里需要注意两点:
1. Cocos2d中有自己一套坐标系,该坐标系的原点为左下角,这与安卓中默认的坐标系不一样,安卓中的坐标系原点为左上角。Cocos2d-android利用CGPoint工具类进行Cocos2d坐标系下坐标的生成。
2. 调用精灵的setPosition方法时指定的坐标点位置所对应的是精灵的中心位置。
所以,如果要让精灵恰好贴在屏幕的左边缘时,横坐标不能指定为0,而是要指定为精灵宽度的一半。为了让精灵在屏幕上垂直居中显示,将纵坐标指定为整个可显示区域的一半。
最后将已经生成好的精灵添加到Layer中
addChild(player);
这样Layer的构造器就算完成了,在创建Layer对象的时候我们还设置了Layer的背景并添加了一个精灵。
接下来再看静态的scene方法:
public static CCScene scene()
{
CCScene scene = CCScene.node();
CCColorLayer layer =
new GameLayer(ccColor4B.ccc4(
255, 255, 255, 255));
scene.addChild(layer);
return scene;
}
因为Layer不可以单独显示,所以scene方法就是创建Scene对象,通过调用GameLayer的构造器创建Layer对象后放到Scene中,并将该Scene对象作为方法的返回值返回。
现在运行程序后,应该在手机屏幕上显示如下内容:
屏幕强制以横屏方式显示,白色背景,左下角显示的是FPS帧数,忍者精灵靠屏幕左侧居中显示。
接下来就用已经掌握的知识来添加更多的精灵,这一次添加敌人精灵。
在GameLayer的构造器已有代码的最后添加这样一行代码:
this.schedule("gameLogic", 1.0f);
该代码的作用就是利用计时器(Timer),每间隔1秒钟执行一次gameLogic方法。
接下来在GameLayer中添加gameLogic方法:
public void gameLogic(float dt)
{
addTarget();
}
gameLogic回调方法必须有一个float类型的参数,该参数的值为本次回调距离上一次该方法被回调时的时间间隔是多少。所以对于gameLogic方法来说,每一次被调用时dt参数的值应该都是在1左右。
接下来看一下addTarget方法:
protected void addTarget()
{
Random rand = new Random();
//添加精灵
CCSprite target =
CCSprite.sprite("Target.png");
CGSize winSize =
CCDirector.sharedDirector().displaySize();
int minY = (int)(target.getContentSize().height / 2.0f);
int maxY = (int)(winSize.height - target.getContentSize().height / 2.0f);
int rangeY = maxY - minY;
int actualY = rand.nextInt(rangeY) + minY;
target.setPosition(winSize.width + (target.getContentSize().width / 2.0f), actualY);
addChild(target);
target.setTag(1);
......
int minDuration = 2;
int maxDuration = 4;
int rangeDuration = maxDuration - minDuration;
int actualDuration =
rand.nextInt(rangeDuration) + minDuration;
CCMoveTo actionMove =
CCMoveTo.action(
actualDuration,
CGPoint.ccp(
-target.getContentSize().width / 2.0f,
actualY));
CCCallFuncN actionMoveDone =
CCCallFuncN.action(this, "spriteMoveFinished");
CCSequence actions =
CCSequence.actions(actionMove, actionMoveDone);
target.runAction(actions);
}
addTarget方法中完成了两件事情:
1. 添加敌人精灵到屏幕上的随机位置
2. 让精灵动起来
创建并添加敌人精灵到相应位置的代码与之前添加忍者精灵的代码非常类似。首先利用Target.png创建精灵,然后调用精灵的setPosition方法设定精灵在屏幕上的位置。这次的位置相对灵活一些,纵坐标方面先计算一个minY,minY的位置为敌人精灵恰好踩在屏幕的最底边时的纵坐标,而maxY的位置为敌人精灵恰好顶在屏幕的最上边时的纵坐标。然后利用Random在minY和maxY之间生成一个随机的值作为最终敌人精灵所在位置的纵坐标。横坐标方面利用屏幕的最大宽度加上敌人精灵宽度的一半,这样让敌人精灵的最左侧恰好贴在屏幕最右侧位置,刚好看不见。
另外在创建了敌人精灵后比之前多了一行代码:
target.setTag(1);
这里为创建的敌人精灵设置一个tag属性,以后可以通过该tag值判定一个精灵的类型是什么。如果取到一个精灵的tag值为1,则可以知道该精灵为敌人精灵。
接下来要让精灵动起来。这里要使用Cocos2d-android中提供的一Action类型对象。这里用到了3中Action对象:
1. CCMoveTo:它的作用是在规定的时间内,让精灵从当前位置移动到参数指定位置。
CCMoveTo actionMove =
CCMoveTo.action(
actualDuration,
CGPoint.ccp(
-target.getContentSize().width / 2.0f,
actualY));
actualDuration为利用Random生成的完成移动的时间,第二个参数是移动结束时希望敌人精灵所处的位置。终止位置的纵坐标与敌人精灵初始位置的纵坐标一致,横坐标为敌人精灵移出屏幕最左侧一半身位的位置,也就是敌人精灵的最右侧恰好贴在屏幕的最左侧边缘,刚好看不见。
2. CCCallFuncN:也是Action类型,它的作用是用来回调指定的方法,例子中被回调的方法名称是spriteMoveFinished方法,且spriteMoveFinished方法会有一个Object类型的参数传入。
3. CCSequence:Action类型,它的作用顾名思义就是将参数中传入的若干个Action组成一个序列按顺序执行。
代码中,我们的敌人精灵实际最终运行的Action是CCSequence类型的actions,而创建actions的时候我们传入了两个action对象,一个是CCMoveTo类型的actionMove对象,另一个是CCCallFuncN类型的actionMoveDone对象。这样最终的执行内容就是先执行actionMove,在随机时间内将敌人精灵从屏幕的右侧移动到左侧,当移动完毕后执行actionMoveDone,回调spriteMoveFinished方法。
public void spriteMoveFinished(Object sender)
{
CCSprite sprite = (CCSprite)sender;
if (sprite.getTag() == 1)
{
......
}
......
this.removeChild(sprite, true);
}
之前说过spriteMoveFinished有一个Object类型的参数,该参数就是触发该回调的精灵。我们通过调用该精灵的getTag方法来判定该精灵究竟是什么精灵,如果tag为1则该精灵是敌人精灵,我们可以针对敌人精灵在做更多的操作。最后将该精灵从Layer上去掉。
this.removeChild(sprite, true);
此时再运行我们的代码,就会看到从屏幕右侧每隔一秒钟会产生一个水平移动的敌人精灵,因为移动的时长是随机数,因此敌人精灵从屏幕右侧向左侧移动时的速度是不一样的。
OK,接下来来处理用户的手指触摸。一旦用户在屏幕上进行了点击,则会触发Layer中的响应回调方法,我们只需要在GameLayer中重写这些方法即可。这些方法包括ccTouchesBegan、ccTouchesMoved、ccTouchesEnded、ccTouchesCancelled方法,从名字就可以知道哪些方法会在触摸发生的什么阶段被回调。
在游戏中当触摸屏幕时,希望忍者精灵向触摸位置方向发射一枚飞镖。根据设计可以重写ccTouchesBegan刚按下时就发送飞镖,也可以重写ccTouchesEnded手指抬起时发送飞镖。
public boolean ccTouchesEnded(MotionEvent event)
{
// 将坐标点从安卓坐标系转为Cocos2d坐标系
CGPoint location =
CCDirector.sharedDirector().
convertToGL(CGPoint.ccp(
event.getX(), event.getY()));
CGSize winSize =
CCDirector.sharedDirector().displaySize();
CCSprite projectile =
CSprite.sprite("Projectile.png");
projectile.setPosition(20, winSize.height / 2.0f);
//计算飞镖的终点坐标
int offX =
(int)(location.x - projectile.getPosition().x);
int offY =
(int)(location.y - projectile.getPosition().y);
if (offX <= 0)
return true;
addChild(projectile);
projectile.setTag(2);
......
int realX = (int)(winSize.width + (projectile.getContentSize().width / 2.0f));
float ratio = (float)offY / (float)offX;
int realY = (int)((realX * ratio) + projectile.getPosition().y);
//飞镖的终点坐标
CGPoint realDest = CGPoint.ccp(realX, realY);
int offRealX =
(int)(realX - projectile.getPosition().x);
int offRealY =
(int)(realY - projectile.getPosition().y);
float length =
(float)Math.sqrt(
(offRealX * offRealX) +
(offRealY * offRealY));
float velocity =
480.0f / 1.0f; // 480 pixels / 1 sec
float realMoveDuration = length / velocity;
//移动飞镖
projectile.runAction(CCSequence.actions(
CCMoveTo.action(
realMoveDuration,realDest),
CCCallFuncN.action(
this, "spriteMoveFinished")));
......
return true;
}
当手指抬起后,会从初始位置飞镖精灵的初始位置(20,屏幕高度一半)向手指抬起的方向进行移动,但是移动时需要注意的是飞镖移动的终点并不是手指抬起的点,而是应该顺着手指抬起的点一直延伸到屏幕之外:
这里的计算只需用到一点简单的比值关系就可以计算出飞镖的终点坐标。在得到终点坐标后利用勾股定理得到飞镖飞行的距离。然后利用飞行距离除以一个人为设定的飞行速度(480)计算出来完成这段移动所需要的时长。有了时长和终点坐标就可以构建CCMoveTo来移动飞镖了。同样我们也通过构建CCSequence在飞镖移动完毕后回调spriteMoveFinished方法,这样我们就需要在spriteMoveFinished增加一些针对飞镖精灵的处理逻辑了:
public void spriteMoveFinished(Object sender)
{
CCSprite sprite = (CCSprite)sender;
if (sprite.getTag() == 1)
{
//敌人精灵的处理逻辑
......
}
else if (sprite.getTag() == 2)
//飞镖精灵的处理逻辑
.....
//无论是什么精灵,移动完毕就从Layer上移除
this.removeChild(sprite, true);
}
接下来,处理飞镖击中敌人时的逻辑。飞镖击中敌人实际就是飞镖精灵和敌人精灵发生了碰撞,此时应该让敌人精灵从屏幕上消失,当然击中了敌人的飞镖精灵也应该消失掉。
首先我们在构造器的最后再加上一句代码:
this.schedule("update");
这与之前添加的this.schedule(“gameLogic”, 1.0f);类似,也是每间隔一段时间就调用一次update方法,只不过这一次的调用间隔与我们在构造器中设置的刷新率是一致的,也就是1/60秒就调用一次update方法。
接下来就是在update方法中处理飞镖精灵和敌人精灵的碰撞:
public void update(float dt)
{
......
CGRect projectileRect =
CGRect.make(projectile.getPosition().x -
(projectile.getContentSize().width / 2.0f),
projectile.getPosition().y -
(projectile.getContentSize().height / 2.0f),
projectile.getContentSize().width,
projectile.getContentSize().height);
......
CGRect targetRect =
CGRect.make(target.getPosition().x -
(target.getContentSize().width),
target.getPosition().y -
(target.getContentSize().height),
target.getContentSize().width,
target.getContentSize().height);
......
if (CGRect.intersects(projectileRect, targetRect))
{
......
removeChild(target, true);
removeChild(projectile, true);
......
}
......
}
update方法中保留了碰撞检测的核心代码:
首先利用CGRect勾勒出飞镖精灵的外边框,CGRect需要提供四个参数,分别是边框左下角的横坐标和纵坐标,以及边框的宽度和高度。然后如法炮制的勾勒出敌人精灵的外边框。然后利用CGRect提供的静态方法intersects来判定两个边框是否有相交,如果一旦相交就意味着产生了碰撞,于是将碰撞的双方敌人精灵和飞镖精灵从Layer上去除。
注意:update方法中仅仅保留了碰撞检测的核心代码,完整的代码请下载后自行参考。
接下来,我们再为游戏添加上背景音乐和音效。背景音乐和音效的声音文件可以保存到项目res文件夹下的raw文件夹中,读取和加载声音文件时可以使用R.raw.资源文件名来进行。
这里background_music_acc为背景音乐文件,而pew_pew_lei为音效文件。Cocos2d-android提供了响应的工具类非常方便我们来进行音乐或音效的播放。
同样在GameLayer的构造器里面添加如下代码:
// 获取上下文对象
Context context = CCDirector.sharedDirector().getActivity();
SoundEngine.sharedEngine().preloadEffect(context, R.raw.pew_pew_lei);
SoundEngine.sharedEngine().playSound(context, R.raw.background_music_aac, true);
使用SoundEngine的preloadEffect方法来缓冲音效文件,用playSound方法来设定循环播放背景音乐。
注意:这里在实际测试的时候,在执行playSound方法之前应该先preloadSound一下,否则可能因为MediaPlayer的不正确状态导致代码异常。另外再实际测试中,即使设置了第二个参数为循环播放(true),还是因为MediaPlayer的状态问题导致背景音乐只能播放一次。
后面凡是需要播放音效的地方,只要调用SoundEngine.sharedEngine().playEffect即可。
比如,我们在发射飞镖的时候需要音效,就可以在ccTouchesEnded方法中添加上一行:
public boolean ccTouchesEnded(MotionEvent event)
{
......
// 播放音效
Context context =
CCDirector.sharedDirector().getActivity();
SoundEngine.sharedEngine().playEffect(
context, R.raw.pew_pew_lei);
......
return true;
}
这样每当发射飞镖时,都会伴随着pew pew的音效声了。
最后,我们再为游戏增添一个结束场景。我们设定,如果敌人在从屏幕右侧向左侧的移动过程中没有被飞镖击中,顺利的从屏幕左侧逃脱则游戏进入结束场景。
上面曾经写过敌人精灵顺利完成移动(从屏幕左侧逃脱)后会回调spriteMoveFinished方法,因此我们将逻辑加入到方法中:
public void spriteMoveFinished(Object sender)
{
CCSprite sprite = (CCSprite)sender;
if (sprite.getTag() == 1)
{
_targets.remove(sprite);
_projectilesDestroyed = 0;
CCDirector.sharedDirector().replaceScene(GameOverLayer.scene("You Lose :("));
}
else if (sprite.getTag() == 2)
......
this.removeChild(sprite, true);
}
如果是敌人精灵逃脱后触发的spriteMoveFinished方法,则利用CCDirector进行幕的切换,从当前幕切换到由GameOverLayer.scene()方法产生的幕。
看一下GameOverLayer类:
public class GameOverLayer extends CCColorLayer
{
protected CCLabel _label;
public static CCScene scene(String message)
{
CCScene scene = CCScene.node();
GameOverLayer layer = new GameOverLayer(
ccColor4B.ccc4(255, 255, 255, 255));
layer.getLabel().setString(message);
scene.addChild(layer);
return scene;
}
public CCLabel getLabel()
{
return _label;
}
protected GameOverLayer(ccColor4B color)
{
super(color);
this.setIsTouchEnabled(true);
CGSize winSize =
CCDirector.sharedDirector().displaySize();
//参数为:文字内容,字体名称,文字大小
_label = CCLabel.makeLabel(
"Won't See Me", "DroidSans", 32);
_label.setColor(ccColor3B.ccBLACK);
_label.setPosition(
winSize.width / 2.0f, winSize.height / 2.0f);
addChild(_label);
this.runAction(CCSequence.actions(
CCDelayTime.action(3.0f),
CCCallFunc.action(this, "gameOverDone")));
}
public void gameOverDone()
{
CCDirector.sharedDirector().
replaceScene(GameLayer.scene());
}
@Override
public boolean ccTouchesEnded(MotionEvent event)
{
gameOverDone();
return true;
}
}
GameOverLayer的代码与GameLayer的代码有很多类似的地方,首先也是继承自CCColorLayer因此可以设定白色背景,在构造器中也创建了一个精灵,只不过这次的精灵即不是一个图片甚至也不是一个CCSprite类型,而是一个显示文字的CCLabel对象。但是前面提到过任何显示在屏幕上的东西都是一个精灵,因此CCLabel对象也是一个精灵。
在构造器中,精灵创建完毕后会继续执行Action,这里的Action也是由CCSequence组成的Action序列,序列中先执行CCDelayTime让场景停留三秒,随后执行CCCallFunc中指定的gameOverDone回调方法。需要注意的是,CCCallFunc中的回调方法是不带参数的,而前面用过的CCCallFuncN中的回调方法是携带一个Object参数的。在回调方法gameOverDone中通过CCDirector进行幕的切换,从当前的游戏结束幕再次切换到GameLayer.scene方法返回的幕。而且当用户触摸屏幕时,当用户手指离开会触发重写的ccTouchesEnded方法,该方法中会立即触发gameOverDone方法马上完成幕的切换。
GameOverLayer的scene方法与GameLayer的scene方法基本一致,创建scene对象,通过构造器创建layer对象,将layer对象添加到scene中并将scene对象返回。
至此,一个用Cocos2d-android创建的小游戏就算写完了,我们再来回顾一下使用Cocos2d-android开发的整体步骤:
1. Cocos2d-android的一些全局初始化
2. 场景的搭建(scene,layer,sprite)
3. 事件的处理(触摸,碰撞)
4. 音乐、音效(sound,effect)
5. 场景的切换