因为我做的时候比较着急,现在写可能思绪有点乱,大家见谅一下呀!过段时间我好好整理一下,再写一次。
写这个app的原始目的,是为了想要把鲸鱼(黄景瑜的照片作为我项目的资料,这个我就有动力好好写啦,花痴一下下),好,现在我把我写代码的过程简单描述一下。
1.使用cocos2d框架(导入cocos2d-android.jar包)
- SurfaceView是一个Android系统为需要绘制复杂画面的程序员提供的强有力的工具视图类,和OpenGL没有直接的关系,它的子类GLSurfaceView负责把OpenGL连接到Android视图系统。
2.设置一个基本的BaseActivity,在他里面设置好屏幕显示的属性,其他类继承它就可以使用它里面设置的全部属性
- 它用于向WindowManager描述Window的管理策略。(全屏、指定竖屏、允许锁屏)
22.在欢迎和引导界面中添加一个小女孩的动画
- 使用定时器和Handler、message来处理(要让每秒进行一次ui更新,如果不使用handler就不能达到更新ui的效果,我的理解是handler中存在一个队列问题,可以保证不产生阻塞。
- timer里面调用scheduleAtFixedRate函数(定时任务、开始时间,周期),用改变图片的方式做动画
- 在Handler里面判断消息的种类,改变ImageView里的图片
-
在更新完图片后,需要刷新整个改变的那个控件,ImageView.invalidate(); 第二:在用完定时器timer后,要在Activity被干掉的同时销毁定时器timer(timer.cancel();)。
3.在MainActivity中实现游戏的处理事件
- 定义一个CCGLSurfaceView和CCScene,用于做游戏动画。它的View可以用过CCGLSurfaceView来实现:,cocos2d引擎会把图形加载该view对象上
- 在销毁这个activity时,获取这个场景对象实例,得到它的操作句柄,结束这个句柄。同时通过移除缓存中的全部单例“
- 暂停、恢复、停止都是通过CCDirector的句柄来操作。摧毁比停止多一个移除所有的单例,就是清除内容
4.设计gamePlay:
- 里面采用CopyOnWriteArrayList<>(); 来存放活动的精灵,因为(一个线程安全的变体,其中所有可变操作(add、set等等)都是通过对底层数组进行一次新的复制来实现的。
//CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。)
5.gamePlay里面需要gameSprite(游戏精灵,就是指在界面上活动的)
- 主要是利用org.cocos2d.nodes.CCSprite;来创建:
//初始化一个精灵对象,sprite()方法会默认到assets目录下去找名为path的文件
- gameSprite有生命值(life),坐标(x,y),间隔多少时间出现,
4.gamePlay中写初始化函数,
- 首先获取屏幕信息,设置屏幕为可触摸状态:
sharedPrefence = CCDirector.sharedDirector().getActivity().getSharedPreferences("sharedPrefence", Context.MODE_PRIVATE);
WinSize = CCDirector.sharedDirector().displaySize();
setIsTouchEnabled(true);
- 设置背景,按屏幕大小比例放大
- 每隔一段时间执行一次this.schedule("GameSmallLove",0.5f);带0.5f的参数调用GameSmallLove函数
- 新建一个GameSprite精灵,给他设置号基本属性(生命值,加载的图片等)
- 写移动的函数(降落等),先要得到它图片的大小和屏幕的大小来计算可以移动的范围,以及可以下降的速度,根据随机性来设置:
- 得到这个精灵,添加到场景中:
addChild(small.getCCSprite());
- 使用CCFiniteTimeAction 类定义好在什么时间范围完成动作,开启这个精灵runAction
是有限次执行类,它是最为普通的行为,就是按时间顺序做一系列事情,做完后行为结束 CCFiniteTimeAction fs_timeAction = CCMoveTo.action(actualDuration,CGPoint.ccp(actualX, (foe.getCCSprite().getContentSize().height / 2)));// 时间内移动
自定义行为,效果为调用一个自定义的方法,通过方法的实现来达到自己想要的效果,会将节点对象作为参数传递给被调用的函数。
CCCallFuncN fs_Over = null;
行为序列(继承自CCActionInterval),即将若干个待执行的行为按顺序组合在一起,然后依次执行,如果中间有持续行为的话则等到前一个行为执行完毕后再执行下一个(CCCallFunc是并行),因此单独的CCSequence对象是没有意义的。
CCSequence fs_actions = CCSequence.actions(fs_timeAction, fs_Over);
foe.getCCSprite().runAction(fs_actions);
现在一个小爱心就会随机产生了。
6.设计小女孩躲避爱心
- 在不同的状态下,有两个图片,不停的交换,做出动画的效果。
- 用手指一动小女孩,计算触屏的位置
//将UIKit坐标系中的坐标转换为OpenGLES坐标系中的坐标。与之前的原点对其
//下面的代码获取了触摸点的坐标,并将其转换成Cocos2D可以使用的坐标。该方法通常用于转换多点触摸点的坐标。
//下面的代码获取了触摸点的坐标,并将其转换成Cocos2D可以使用的坐标。该方法通常用于转换多点触摸点的坐标。
OpenGl坐标系:原点在左下角(0,0),与数据的二维坐标系一致
UIKit坐标系:又称为屏幕坐标系,原点在左上角,X轴越右越大,Y轴越下越大;
CGPoint location = CCDirector.sharedDirector().convertToGL(CGPoint.ccp(event.getX(), event.getY()));
- 覆写cLayout类的触屏事件:
public boolean ccTouchesBegan(MotionEvent event)
public boolean ccTouchesMoved(MotionEvent event)
public boolean ccTouchesEnded(MotionEvent event)
- 获取小女孩的碰撞盒,方便后续进行判断是否发生碰撞CGRect Rect = _FeiJi_Play.getBoundingBox();
7.移动小女孩就是通过控制检测到触屏的位置,用它来设置小女孩的位置
localX = touchPosition.x;
localY = touchPosition.y+girl.getContentSize().height/2+qipao.getContentSize().height/2;
qipao.setPosition(localX,localY);
addChild(qipao);
8.小女孩的气泡碰到爱心就得分(这个时候气泡消失)
- 设置一个碰撞监听器Detection:
刷新频率是在this.schedule("Detection",0.1f);,带参数的调用函数,后面的参数就是刷新时间间隔(0.1秒,刷新一次)
- 用每个气泡的矩形框来判断每个爱心是否被选中,选中了减少生命值,判断生命值是否为0,为0就移除
for(int i=0;i<qipaos.size();i++){
CCSprite qipao = qipaos.get(i);
CGRect rectQipao = qipao.getBoundingBox();
for(int j=0;j<smallLoves.size();j++){
GameSprite small = smallLoves.get(j);
CGRect rectSmall = small.getCCSprite().getBoundingBox();
if(CGRect.intersects(rectQipao, rectSmall)){
- 气泡消失时又动画的,这里使用spritesheet精灵列表来实现,这回提高效率。
因为cocos2d对它进行了优化!如果你使用spritesheet来获取sprite,那么当场景中有许多sprite的时候,如果这些sprite共享一个spritesheet,那么cocos2d就会使用一次OpenGL ES调用来渲染这些sprite。但是,如果是单个的sprite的话,那么就会有N次OpenGL ES call,这个代价是相当昂贵的。
简而言之--使用spritesheet会更快,尤其是当你有很多的sprite的时候!(使用spritesheet还可以减少游戏占用的内存大小
)
Animations是一个实现android UI界面动画效果的API,Animations提供了一系列的动画效果,可以进行旋转、缩放、淡入淡出等,这些效果可以应用在绝大多数的控件中。
先要选取显示框的大小,就是每个爱心的矩形框,然后用spritesheet精灵列表来显示
for(int y=0;y<1;y++){
for(int x=0;x<num;x++){
CCSpriteFrame frame = CCSpriteFrame.frame(boomSheet.getTexture(), CGRect.make(x*cutX,y*cutY,cutX,cutY), CGPoint.ccp(0, 0)); //cpp(0,0)是偏移量
boomAnimFrames.add(frame);
frameCount++;
if(frameCount==num){
break;
}
}
//创建一个CCAnimation对象,并且指定动画播放的速度。我们使用0.1来指定每个动画帧之间的时间间隔
CCAnimation boomAnimation = CCAnimation.animation("", (float) 0.1,boomAnimFrames);
CCAnimate boomAction = CCAnimate.action(boomAnimation);
CCCallFuncN actionAnimateDone = CCCallFuncN.action(this,"SpriteAnimationFinished");
CCSequence actions = CCSequence.actions(boomAction, actionAnimateDone);
sprite.runAction(actions);
9.显示分数
- 听过CCLabel类,在屏幕上设置一个积分器
//分数标签存在,就移除;重新创建,相当于刷新
if(scoreLabel!=null){
scoreLabel.removeSelf();
}
scoreLabel = CCLabel.makeLabel("Score:", scorePath, 40); //字体类型
scoreLabel.setString("Score:"+score);
scoreLabel.setColor(ccColor3B.ccWHITE);
//标签放置的位置,距离左下角50dip
scoreLabel.setPosition(50,50);
this.addChild(scoreLabel);
- 放在碰撞监听器中改变分数(碰撞一次,刷新一次)
10.设置暂停按钮
- 在屏幕点击事件中写,通过控制判断点击位置是否包含暂停开始按钮,来显示按钮的图形已经它是否可见;
CGRect rectPlay = play.getBoundingBox();
CGRect rectPause = pause.getBoundingBox();
if (CGRect.containsPoint(rectPause, location)||CGRect.containsPoint(rectPlay, location)) {
if (isPause) { //暂停
System.out.println("pause");
play.setVisible(true);
pause.setVisible(false);
CCDirector.sharedDirector().resume();
- 使用句柄来完成暂停和开始操作
11.在改变小女孩生命值的时候,如果重新画生命值,监控器检测来不及反应,所以显示把生命值的多少画好,然后直接使用switch来判断
schedule的时间会相互影响的
12.当生命为0时,不能移除小女孩?
- 直接使用StopSchedule();停止刷新()
/**
* 停止持续的方法
*/
private void StopSchedule() {
girl.removeSelf();
this.unschedule("GameSmallLove");
this.unschedule("AddGirl");
this.unschedule("AddQipao");
}
13.生命结束需要弹出对话框,先左一个动画,小女孩变大说话,然后弹出对话框显示分数
- 出现小女孩和鲸鱼,但是没有爱心(这是因为在结束之前小女孩和鲸鱼是隐藏的,但是这个时候爱心移动的动画仍在执行,所以当生命结束,小女孩和鲸鱼出现的时候,爱心已经移走了;而且这里移动的方向也不对,moveBy只能移动直线,现在需要移动斜线)
- 显示对话框,通过场景的Handler开启一个线程,在一个场景中添加了VIew
14.在菜单页的Score按钮中查看历史记录的分数
有两种方式:
- 设计一个数据库表,放在数据库表中(这里只需要保存一个分数值,所以不用数据库这么麻烦)
- 保存到配置文件中:
在游戏场景中是通过导演获取activity,然后再得到配置文件:前面一个参数是配置文件的名称
sceneActivity = CCDirector.sharedDirector().getActivity();
sp_wel = sceneActivity.getSharedPreferences("sharedPrefence",Context.MODE_PRIVATE);
然后在从sharedPrefence.xml文件中用关键词读取分数,通过冒泡排序,不断更新里面的分数排列
String scoreArray = sp_wel.getString("scoreArray", "0;0;0;0;0");
最后提交:
sp_wel.edit().putString("scoreArray", s).commit();
- scoreActivity可以通过读取配置文件的信息得到分数,在用listView显示出来
sp = getSharedPreferences("sharedPrefence", Context.MODE_PRIVATE);
String s = sp.getString("scoreArray", "0;0;0;0;0");
- 在scoreActivity中,采用SimpleAdapter适配器来进行填充分数条目
list = new ArrayList<HashMap<String, Object>>();
adapter = new SimpleAdapter(this, list, R.layout.score_item, new String[] { "score" },
new int[] { R.id.tv_score });
myListView.setAdapter(adapter);
通过往list中添加Map集合(键值对),来填充适配器
HashMap<String, Object> map = new HashMap<String, Object>();
map.put(name, score);
list.add(map);
// 方法进行通知该SimpleAdapter内容已经发生改变。
adapter.notifyDataSetChanged();
15.添加背景音乐以及按钮的按键声,动画的声音
- 背景音乐长,采用MediaPlayer播放
music = MediaPlayer.create(this, R.raw.wel);
music.start();
- 按钮的按键声,动画的声音是短促而且多,使用SoundPool来操作,它可以同时进行多个声音的播放
public SoundPool soundPool;//声明一个SoundPool
public int musicId;//定义一个整型用load();来设置suondID
装载声音
soundPool= new SoundPool(10, AudioManager.STREAM_SYSTEM, 5);
//第一个参数为同时播放数据流的最大个数,第二数据流类型,第三为声音质量
musicId = soundPool.load(this, R.raw.button_sound, 1);
//播放声音
soundPool.play(musicId, 1, 1, 0, 0, 1);
//第一个参数指定播放哪个声音; leftVolume 、 rightVolume 指定左、右的音量: priority 指定播放声音的优先级,数值越大,优先级越高; loop 指定是否循环, 0 为不循环, -1 为循环; rate 指定播放的比率,数值可从 0.5 到 2 , 1 为正常比率。
15.在游戏的结束的时候,点back按钮,回到菜单页面,再点退出游戏,它又回到菜单页面,所以菜单Activity需要使用单任务模式SingleTask
16.修改结束页面对话框的布局
- 它可以通过配置文件的shape来定义背景等(实心,边框,圆角等)
- 在显示对话框的文件中得到Window的属性,修改:
Window dialogWindow = myDialog.getWindow();
WindowManager.LayoutParams lp = dialogWindow.getAttributes();
dialogWindow.setGravity(Gravity.RIGHT | Gravity.TOP);
lp.x = 65; // 新位置X坐标 ,相对于Gravity的偏移量,都是正的
lp.y = 400;
dialogWindow.setAttributes(lp);
17.提升游戏难度,当分数导到2000分时,进入第二关,加快小爱心的下落速度,同时加大fish
- 创建一个新的场景
- 当分数大于一定程序时,出现一棵树,四面发射子弹
19.代码优化
页面间的切换有黑屏,代码整个,还有一个free按钮的功能没有做好