当前位置: > 华清远见教育科技集团 > 嵌入式学习 > 讲师博文 > 游戏开发实例《小鱼快跑》
游戏开发实例《小鱼快跑》
时间:2016-12-12作者:华清远见

本章要点:

本章我们将通过在Android平台完成一个小游戏《小鱼快跑》来学习Android游戏开发。

6.1. 游戏策划

在开发一款游戏时,我们的第一阶段是进行游戏策划,这是一项比编写代码更为重要的工作。在这个过程中,我们需要确定这款游戏的基本特征,比如是游戏类型、用户类型、游戏风格以及游戏可玩性等等,这将直接决定该游戏的后期制作和玩家的喜好程度。

游戏的开发过程已经随着游戏行业的飞速发展被赋以更多的元素。在游戏开发的初期,优秀的作品往往只是代码工作者短期的投入,不需要前期的准备、调查等。但是目前游戏行业的状况已经不同于当年,游戏平台的多元化、用户类型的增加以及市场竞争的激烈化等都需要我们将正规的商业流程纳入游戏的开发当中,如果我们依然按照早期的开发模式应对当前的游戏市场,那么开发出的游戏将会由于缺少一方面或多方面的特性而无法引起用户的兴趣。那么如何才能开发一款受广大用户喜爱的游戏呢?这就需要我们在策划之前进行调查,首先确定市场的需求和目标客户。在明确了目标客户之后,是否需要考虑游戏类型呢?从大的方面来说,游戏是单机的还是联网的?是单人的还是多人的?是动作类型的还是角色扮演类型的?等等。在确定了游戏的类型之后,还需要考虑游戏的可玩性,总不可能说游戏玩家十多分钟就通关了,这就需要包括游戏的难度设置、关卡控制以及后期的版本控制。现在我们就可以根据上面的目标客户和游戏类型来定义游戏的风格了,比如想以三国为题材做一个即时战略游戏,就不可能定义为现代风格,游戏中就不会出现坦克、飞机的道具。同时这也体现了游戏的真实性,但是游戏本身是一个虚幻的东西,玩家就是需要将自己放到虚拟的世界中,所以也不能过度真实,让玩家觉得枯燥乏味。风格确定之后,还可以根据游戏的风格来配置游戏音效。由于玩家经常会接触到很多游戏,所以他在玩游戏时会对一些没有新意的游戏感到厌倦,反正我玩过类似的游戏,没什么好玩的。如果得到玩家这样的评论,那么大家都知道这款游戏的成败了。虽然游戏创新并不是一件很容易的事情,但是为了吸引玩家,我们不得不大胆地创新。

这些问题都解决了,就可以准备写策划案了。在写策划案的同时还需要考虑到美工和程序在技术上的实现以及硬件的支持,不能设计技术达不到的效果。了解了这些问题,开始编写策划案。

其实策划是一个非常广泛的领域,有很多东西需要自己在实践中证明,这里只是列举了常用的、值得注意的地方。

6.2.游戏资源

在策划阶段完成了游戏的前期准备之后,我们就可以按照策划文档开始游戏的实施。首先准备游戏的资源,比如音效、界面,这些在公司内都由专门的人士负责,他们需要根据策划文档的描述来发挥自己的想象力,在保持和策划文档一致的情况下,进行创新。同样,为了保证美工的图片适合程序的要求,还需要多和程序员进行交流,以确保程序员能够很清楚地理解自己的设计,使游戏效果达到好。

6.3.游戏开发

程序员在得到文档和资源后并不能马上打开编辑器,新建工程开始写代码,而是要仔细查看文档和资源,根据这些来确定所要使用的知识和所要实现的功能,然后构建一个整体的框架。这个整体的框架很重要,一个优秀的程序员会在框架的设计上花很多时间,因为一个好的框架可以使后面的开发、调试等更加简单,同时一个好的框架还能提高游戏的运行效率。为了保证质量,每个程序员写的程序都有Bug,所以我们需要不断地测试、修改,再测试、再修改,从而给玩家一个好的体验。

《小鱼快跑》包括了游戏开发中的大部分技术,包括背景、精灵、图层、音效等。下面是本章将完成的游戏在Android上的运行效果。

6.3.1.游戏框架设计

前面已经介绍了框架在游戏开发中的重要地位,如何才能实现一个适合该游戏的框架呢?首先我们需要了解游戏的内容,游戏中包括了地图、主角、整个屏幕界面,显示了地图和主角的属性,地图上还有道具,至少需要一个视图来显示,并且需要更新界面的显示和一个控制游戏逻辑及事件的类。下面我们来构建该游戏的整体框架。

在Android中要显示一个视图类就必须继承自View类,在本例中我们使用SurfaceView,SurfaceView可以直接从内存或者DMA等硬件接口取得图像数据,因此是个非常重要的绘图容器类。在Android开发中,布局资源通过setContentView被设置在res文件夹下,刷新率通过UI主线来控制。但是在游戏开发中,这种刷新率远远不能满足我们的需求,所以我们应该自己掌控刷新率,当然Android也想到了这一点,所以提供了一个类SurfaceView,使用SurfaceView我们可以通过自己的线程去控制屏幕的刷新频率,以达到游戏中的效果。SurfaceView中包括一主要的绘制方法onDraw和一些事件的处理,本例中我们将所有显示在屏幕上的对象通过属性的不同归类到不同的图层,并通过哈希表来存储这些图层,后通过图层之间的顺序、图层中对象的顺依次在屏幕上绘制。在构建这个类时还可以加入我们自己的一些方法,比如更新图层(updatePicLayer)、对图层的其他操作(removeDrawablePic、updateLayrIds)等。在SurfaceView中,整个图层的绘制全部都是基于SurfaceView来实现的,我们可以从SurfaceView来获得图层并对其进行绘制。有了这些内容,下面构建一个用于显示游戏界面的视图类MainSurface。MainSurface类的代码如代码清单6-1所示:

代码清单6-1.MainSurface.java

public class MainSurface extends SurfaceView implements SurfaceHolder.Callback {
        /**
        * 修改图层的操作定义
        */
        //更新图层
        private final static int CHANGE_MODE_UPDATE = 0;
        //添加元素到图层
        private final static int CHANGE_MODE_ADD = 1;
        //删除元素从图层
        private final static int CHANGE_MODE_REMOVE = 2;
        // 图片的图层分布
        private HashMap<Integer, ArrayList<Drawable>> picLayer =new HashMap<Integer, ArrayList<Drawable>>();
        // 修改后的图片的图层分布,这里根据操作分为了两个图层,分别是添加的元素,和删除的元素
        private HashMap<Integer, ArrayList<Drawable>> addPicLayer = new HashMap<Integer, ArrayList<Drawable>>(),removePicLayer = new HashMap<Integer, ArrayList<Drawable>>();
        // 是否修改过图层
        private boolean changeLayer = false;
        private int picLayerId[] = new int[0]; // 定义一个图层ID,加速获取图层绘制(省去了从map中获取各个图层排序问题)
        private Paint paint; // 画笔
        private OnDrawThread odt; // 屏幕绘制线程,用于控制绘制帧数,周期性调用onDraw方法
        private Typeface typeface;
        public MainSurface(Context context) {
        super(context);
        typeface = Typeface.createFromAsset(context.getAssets(),"texttype/WhatsHappened.ttf");
        this.getHolder().addCallback(this);
        paint = new Paint();
        paint.setTypeface(typeface); // 设置Paint的字体
        paint.setAntiAlias(true);
        paint.setFilterBitmap(true);
        paint.setDither(true);
        paint.setTextSize(15); // 根据不同分辨率设置字体大小
        paint.setColor(Color.WHITE);
        odt = new OnDrawThread(this);
        }
         public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
        // TODO Auto-generated method stub
        }
        public void surfaceCreated(SurfaceHolder arg0) {
        // TODO Auto-generated method stub
         odt.start();
       nbsp; }
        public void surfaceDestroyed(SurfaceHolder arg0) {
        paint = null;
        typeface = null;
        picLayerId = null;
        picLayer = null;
        addPicLayer = null;
        removePicLayer = null;
        }
        @Override
        /**
        * 绘图方法,这个方法是由线程控制,周期性调用的
        */
        public void onDraw(Canvas canvas) {
        //更新图层内容
        updatePicLayer(CHANGE_MODE_UPDATE,0,null);
        // 遍历所有图层,按图层先后顺序绘制
        for (int id : picLayerId) {
        for (Drawable drawable : picLayer.get(id)) {
        drawable.onDraw(canvas, paint);
            }
        }
        //绘制LOGO
        canvas.drawText("farsight android game demo. by XiloerFan", 0, 20, paint);
        }
        /**
        * 更新图层,这里分为三种操作,分别是更新临时图层中的内容到绘制图层中,删除绘制图层中的元素,添加绘制图层中的元素
        * 这里加了个线程锁,保证多线程下操作图层的安全性
        * @param mode 对绘制图层的操作类型,对应当前类的CHANGE_MODE常量
        * @param layerId 操作的图层ID
        * @param draw 操作的图层元素
        */
        private synchronized void updatePicLayer(int mode,int layerId,Drawable draw){
        switch(mode){
        //将临时图层中的内容更新至绘制图层中
        case CHANGE_MODE_UPDATE:
        //如果有修改
        if(changeLayer){
        //向图层添加新的元素
        for(Integer id:addPicLayer.keySet()){
        for(Drawable d:addPicLayer.get(id)){
        //如果要添加的元素所处图层不存在,则创建这个图层,并更新图层ID数组
        if(this.picLayer.get(id)==null){
        this.picLayer.put(id, new ArrayList<Drawable>());
        updateLayerIds(id);
                }
        this.picLayer.get(id).add(d);
            }
        }
        addPicLayer.clear();
        //删除图层中的元素
        for(Integer id:removePicLayer.keySet()){
        for(Drawable d:removePicLayer.get(id)){
        try {
        this.picLayer.get(id).remove(d);
            } catch (Exception e) {
        System.out.println("图层内容不存在:"+id);
            }
            }
        }
        removePicLayer.clear();
        changeLayer = false;
        }
        break;
        /**
        * 无论是向绘图图层中添加还是删除元素,都不是直接操作绘制图层,都是存放在对应的临时图层中,等待绘制方法绘制周期中将变化的内容更新到绘制图层中
        * 保证多线程操作情况下的安全性
        */
        //添加一个元素
        case CHANGE_MODE_ADD:
        ArrayList<Drawable> al = addPicLayer.get(layerId);
        if(al==null){
        al = new ArrayList<Drawable>();
        addPicLayer.put(layerId, al);
        }
        al.add(draw);
        changeLayer = true;
        break;
        //删除一个元素
      nbsp;  case CHANGE_MODE_REMOVE:
        ArrayList<Drawable> al1 = removePicLayer.get(layerId);
        if(al1==null){
        al1 = new ArrayList<Drawable>();
            removePicLayer.put(layerId, al1);
        }
        al1.add(draw);
        changeLayer = true;
        break;
        }
        }
        /**
        * 将一个可绘制的图放入图层中
        *
        * @param layer
        *图层号 图层号虽然是int,但是实际上只支持到byte,原因是图层没有必要那么多
        * @param pic
   &nbnbsp;    *可绘制的图
        */
        public void putDrawablePic(int layer, Drawable pic) {
        if(pic==null){
        System.out.println("图层内容不能为空:对应图层:"+layer);
        return;
        }
        updatePicLayer(CHANGE_MODE_ADD,layer,pic);
        }
        /**
        * 将一个可绘制的图从图层中移除
        *
        * @param layer
        * @param pic
        */
        public void removeDrawablePic(int layer, Drawable pic) {
        if(pic==null){
       &nbsnbsp;System.out.println("图层内容不能为空:对应图层:"+layer);
        return;
        }
        updatePicLayer(CHANGE_MODE_REMOVE,layer,pic);
        }
        /**
        * 更新图层Id
        *
        * @param newLayerId
        */
        private void updateLayerIds(int newLayerId) {
        // 初始化图层
        if (picLayerId.length == 0) {
        picLayerId = new int[1];
        picLayerId[0] = newLayerId; // 将新的图层ID添加到初始化的图层ID数组中
        } else {
        // 创建一个新的图层数组,长度比原来的大1位
        int picLayerIdFlag[] = new int[picLayerId.length + 1];
        for (int i = 0; i < picLayerId.length; i++) {
        // 排序操作,如果新的图层ID小于当前图层ID,讲新的图层ID插入其中
        if (picLayerId[i] > newLayerId) {
        for (int f = picLayerIdFlag.length - 1; f > i; f--) {
        picLayerIdFlag[f] = picLayerId[f - 1];
        }
        picLayerIdFlag[i] = newLayerId;
        break;
        } else {
        picLayerIdFlag[i] = picLayerId[i];
        }
        // 如果到了后,都没有比新图层ID大的,就将新的图层ID存入后
        if (i == picLayerId.length - 1) {
        picLayerIdFlag[picLayerIdFlag.length - 1] = newLayerId;
            }
        }
        // 将新的图层ID数组覆盖原有的
        this.picLayerId = picLayerIdFlag;
                }
            }
        }

在创建和控制了图层显示之后,要让游戏能够动起来,需要开启一个线程来实时更新图层显示界面并刷新。下面我们将为游戏创建一个绘图线程,可以通过sh.lockCanvas(null)方法来取得当前显示的图层,然后根据不同的图层来进行游戏更新。线程的开启在MainSurface继承的接口SurfaceHolder.CallBack中体现:SurfaceCreated图层创建时开启。代码清单6-2所示为绘制图层的线程:

代码清单6-2. 控制绘图的线程

public class OnDrawThread extends Thread{
        private MainSurface surface;
        private SurfaceHolder sh;
        private int drawSpeed; &nbnbsp;  //每次绘制后的休息毫秒数,这个值是根据常量中的绘制帧数决定的
        public OnDrawThread(MainSurface surface){
        super();
        this.surface = surface;
        sh = surface.getHolder();
        drawSpeed = 1000/Constant.ON_DRAW_SLEEP;
        }
        public void run(){
        super.run();
        Canvas canvas = null;
        while(GamingInfo.getGamingInfo().isGaming()){
        try{
        canvas = sh.lockCanvas(null);
        if(canvas!=null){
        surface.onDraw(canvas);
        }
        }catch(Exception e){
        Log.e(this.getName(), e.toString());
        e.printStackTrace();
        }finally{
        try{
        if(sh!=null){
        sh.unlockCanvasAndPost(canvas);
            }
        }catch(Exception e){
        Log.e(this.getName(), e.toString());
            }
        }
        try{
        Thread.sleep(drawSpeed);
        }catch(Exception e){
                    }
                }
    nbsp;        }
        }

在完成了这些模块之后,就需要通知一个Activity来控制游戏的运行,游戏开始(onCreate)、游戏重置(onResume)、游戏暂停(onPause)、事件处理(onTouchEvent)等。整个游戏界面的显示通过绘图线程来控制:当创建一个MainSurface对象时,在MainSurface的构造方法里面创建一个绘图线程odt,同时SurfaceHolder.Callback监听到MainSurface的创建时自动调用surfaceCreate方法,在surfaceCreate中开启线程,开始以双缓冲模式绘制图层。代码清单6-3为GameActivity类的处理:

代码清单6-3. GameActivity.java

public class GameActivity extends Activity {
        private MainSurface surface;
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.requestWindowFeature(Window.FEATURE_NO_TITLE);//设置屏幕显示没有title
        this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        }
        @Override
        protected void onResume() {
        super.onResume();
        init();    //开始初始化
        }
        @Override
        public void onWindowFocusChanged(boolean hasFocus) {
        // TODO Auto-generated method stub
        super.onWindowFocusChanged(hasFocus);
        if(hasFocus){
        /**
        * 创建一个线程来异步初始化游戏内容
        */
  &nbsnbsp;     new Thread(new Runnable(){
        public void run() {
        //使用游戏初始化管理器初始化游戏
        GameInitManager.getGameInitManager().init();
                }
            }).start();
        }
        }
        /**
        * 初始化操作
        */
      &nbsnbsp; private void init(){
        /**
        * 初始化绘图层
        */
        GamingInfo.clearGameInfo();
        GamingInfo.getGamingInfo().setGaming(true);
        GamingInfo.getGamingInfo().setActivity(this);
        //获得手机的宽度和高度像素单位为px
        DisplayMetrics dm = new DisplayMetrics();
        this.getWindowManager().getDefaultDisplay().getMetrics(dm);
        if(dm.widthPixels<dm.heightPixels){
        GamingInfo.getGamingInfo().setScreenWidth(dm.heightPixels);
        GamingInfo.getGamingInfo().setScreenHeight(dm.widthPixels);
        }else{
        GamingInfo.getGamingInfo().setScreenWidth(dm.widthPixels);
        GamingInfo.getGamingInfo().setScreenHeight(dm.heightPixels);
            }
        surface = new MainSurface(this);
        GamingInfo.getGamingInfo().setSurface(surface);
        this.setContentView(surface);
        }
        @Override
        protected void onPause() {
        //停止游戏相关活动
        GameInitManager.getGameInitManager().stop();
        //清除共享数据对象
        GamingInfo.clearGameInfo();
        super.onPause();
        }
        @Override
        public boolean onTouchEvent(MotionEvent event) {
        if(GameInitManager.getGameInitManager().isIniting()){
        return super.onTouchEvent(event);
        }
        //屏幕被触摸
        if(event.getAction()==MotionEvent.ACTION_DOWN){
        //先看布局管理器是否有相应
        if(LayoutManager.getLayoutManager().onClick(event.getRawX(), event.getRawY())){
        return true;
        }
        //发射子弹
        CannonManager.getCannonManager().shot(event.getRawX(), event.getRawY());
        return true;
                }
        return super.onTouchEvent(event);
            }
        }

到这里我们基本完成了一个游戏的整体框架,值得一提的是,整个程序采用MVC架构,后面的所有游戏对象都只需要继承自我们自定义的实体类DrawableAdapter,然后在相应的管理类中判断和更改当前的游戏状态,程序便自动找到我们需要更新和释放的游戏对象进行操作。

6.3.2.背景设计

小鱼快跑的游戏背景设计比较简单,仅仅是一张背景图的显示,没有参与游戏逻辑的处理。代码清单6-4为游戏背景的实体类:

代码清单6-4. BackGround.java

public class BackGround extends DrawableAdapter{
        private Bitmap background;
        public void setCurrentPic(Bitmap background){
        this.background = background;
        }
        public Bitmap getCurrentPic() {
        // TODO Auto-generated method stub
        return background;
        }
        public int getPicWidth() {
        // TODO Auto-generated method stub
        return background.getWidth();
        }
        public int getPicHeight() {
        // TODO Auto-generated method stub
       nbsp; return background.getHeight();
            }
        }

6.3.3.精灵设计

游戏中的对象称为精灵,当然精灵的范围很广,包括NPC、道具等。既然是精灵,必然有很多动画,比如,小鱼在游动和被捕时应该有不同的动画,动画本身就是将图片一帧一帧地连接起来,循环地播放每一帧形成的。一般做游戏会把精灵作为一个单独的类,本例中由于我们使用MVC架构,因此精灵的属性和活动分开操作。我们定义一个可绘制接口Drawable,用一个实体类DrawableAdapter来实现该接口,表示精灵的实体,后用相应的管理类来实现精灵的活动。Drawable是个用来显示图像的类,是由一个图像(可以有好几帧,但是一次只有一个显示)组成的(当然DrawableAdapter还有其他的特性,每次只能使用一个图像而不是多个图像来填充屏幕是它的主要特征)。下面我们看看Drawable和DrawableAdapter类:

代码清单6-5.Drawable.java

public interface Drawable {
        public Matrix getPicMatrix();//获取图片旋转的矩阵表示
        public Bitmap getCurrentPic();//获取当前动作图片的资源
        public int getPicWidth();//返回图片的宽度
        public int getPicHeight();//返回图片的高度
        public void onDraw(Canvas canvas,Paint paint);//绘制的回调方法
        }

代码清单6-6.DrawableAdapter.java

public abstract class DrawableAdapter implements Drawable{
        private Matrix matrix = new Matrix();
        public Matrix getPicMatrix() {
        // TODO Auto-generated method stub
        return matrix;
        }
        public void onDraw(Canvas canvas, Paint paint) {
        canvas.drawBitmap(this.getCurrentPic(),
        this.getPicMatrix(), paint);
                }
        }

在Drawable类中,getCurrentPic()得到游戏对象当前的动作图片,getPicMatrix()得到处理游戏对象的矩阵。在DrawableAdapter类中,onDraw方法用来实现游戏对象的绘制。

6.3.3.1.游戏对象构造

本游戏中的对象包括小鱼、子弹、金币等。下面我们以小鱼为例介绍游戏对象的构造:

1. 常量、属性定义

代码清单6-7. Fish类常量、属性定义

/**
        * 常量定义
*/
        public static final int ROTATE_DIRECTION_LEFT = 1; //左转
        public static final int ROTATE_DIRECTION_RIGHT = 2; //右转
        /**
        * 引用类型属性定义
        */
        private FishInfo fishInfo;        //当前鱼的细节配置信息
        private Bitmap[] fishActs;        //当前鱼的所有动作
        private Bitmap[] fishCatchActs;         //当前鱼的所有被捕获动作
        private PicActThread picActThread;         // 创建当前鱼的动作线程
        /**
        * 简单类型属性定义
        */
        private int currentPicAct = 0;        //当前动作索引值
        private int currentCatchPicAct = 0;         //当前被捕捉动作索引值
        private boolean isAlive = true;        //当前鱼是否活着
        private float distanceHeadFishX;        //距领头鱼X偏移量
        private float distanceHeadFishY;        //距领头鱼Y偏移量
        private HeadFish headFish;        //领头鱼
        private boolean canRun;        //鱼是否可以移动
        private int[] fishOutlinePoint = new int[4];          //鱼的外接矩形,x的小值,大值,Y的小值,大值

常量定义中定义小鱼游动的方向——左、右;引用类型中,定义小鱼的活动信息——当前鱼的细节配置信息、当前鱼的所有动作、当前与的所有被捕动作、控制当前鱼动作的线程。

2. 事件处理

当小鱼被捕时触发一个事件,程序由此做出响应的处理——捕捉动作变换、小鱼总数变换等。

代码清单6-8.Fish类事件处理

/**
        * 触发已被捕捉事件的响应方法
        * 当调用了这个方法,说明这条鱼已经被捕捉了
        */
        public void onCatched(Ammo ammo,final float targetX,final float targetY){
        this.setAlive(false);
        new Thread(new Runnable() {
        public void run() {
        try{
        float fishX = getHeadFish().getFish_X()-getDistanceHeadFishX();
        float fishY = getHeadFish().getFish_Y()-getDistanceHeadFishY();
        GamingInfo.getGamingInfo().getFish().remove(Fish.this);
        Thread.sleep(1800);
        //调用增加分数方法
        ScoreManager.getScoreManager().addScore(getFishInfo().getWorth(), fishX, fishY);
        Thread.sleep(200);
        Fish.this.getPicActThread().stopPlay();
        GamingInfo.getGamingInfo().getSurface().removeDrawablePic(Fish.this.getFishInfo().getFishInLayer(), Fish.this);
        }catch(Exception e){
        Log.e("Fish_onCatched", e.toString());
                }
        }
        }).start();
        }

3. 基本信息处理

代码清单6-9.Fish类基本信息处理

public Fish(){
        }
        public Fish(Bitmap[] fishActs,Bitmap[] fishCatchActs,FishInfo fishInfo){
        this.fishActs = fishActs;
        this.fishCatchActs = fishCatchActs;
        this.fishInfo = fishInfo;
        this.getPicMatrix().setTranslate(-500, -500);
        }
        //是否处于活动状态(在屏幕中游着)
        public boolean isAlive() {
        // TODO Auto-generated method stub
        return isAlive;
        }
        //设置是否处于活动状态
        public void setAlive(boolean isAlive) {
        // TODO Auto-generated method stub
        this.isAlive = isAlive;
        }
        /**
        * 鱼旋转点的X坐标
        */
        public int getFishRotatePoint_X() {
        return getCurrentPic().getWidth()/2;
        }
        /**
        * 鱼旋转点的Y坐标
        */
        public int getFishRotatePoint_Y() {
        return getCurrentPic().getHeight()/2;
        }
        public PicActThread getPicActThread() {
        return picActThread;
        }
        public void setPicActThread(PicActThread picActThread) {
        this.picActThread = picActThread;
        }
        public float getDistanceHeadFishX() {
        return distanceHeadFishX;
        }
        public void setDistanceHeadFishX(float distanceHeadFishX) {
        this.distanceHeadFishX = distanceHeadFishX;
        }
        public float getDistanceHeadFishY() {
        return distanceHeadFishY;
        }
        public void setDistanceHeadFishY(float distanceHeadFishY) {
        this.distanceHeadFishY = distanceHeadFishY;
        }
        /**
        * 获取所有的动作数量
        * @return
        */
        public int getFishActs() {
        // TODO Auto-generated method stub
        if(isAlive()){
        return fishActs.length;
        }else{
        return fishCatchActs.length;
        }
        }
        /**
        * 设置当前动作图片的资源ID
        * @param picId
        */
        public void setCurrentPicId(int picId) {
        if(isAlive()){
        this.currentPicAct = picId;
        }else{
        this.currentCatchPicAct = picId;
                }
        }
        public int getCurrentPicId() {
        if(isAlive()){
        return currentPicAct;
        }else{
        return currentCatchPicAct;
                }
        }
        public Bitmap getCurrentPic() {
        // TODO Auto-generated method stub
        if(isAlive()){
        return fishActs[currentPicAct];
        }else{
        return fishCatchActs[currentCatchPicAct];
                }
        }
        public int getPicWidth() {
        // TODO Auto-generated method stub
        return getCurrentPic().getWidth();
        }
        public int getPicHeight() {
        // TODO Auto-generated method stub
        return getCurrentPic().getHeight();
        }
        /**
        * 设置鱼的所有动作
        * @param fishActs
        */
        public void setFishActs(Bitmap[] fishActs) {
        // TODO Auto-generated method stub
        this.fishActs = fishActs;
        }
        /**
        * 设置鱼的所有被捕获动作
        * @param fishCatchActs
        */
        public void setFishCatchActs(Bitmap[] fishCatchActs) {
        // TODO Auto-generated method stub
        this.fishCatchActs = fishCatchActs;
        }
        public FishInfo getFishInfo() {
        return fishInfo;
        }
        public void setFishInfo(FishInfo fishInfo) {
        this.fishInfo = fishInfo;
        }
        /**
        * 根据当前鱼获取同类鱼实例
        * @return
        */
        public Fish getFish(){
        return new Fish(this.fishActs,this.fishCatchActs,this.fishInfo);
        }
        /**
        * 触发捕捉事件的响应方法
        */
        public void onCatch(Ammo ammo,final float targetX,final float targetY){
        // System.out.println("鱼被捕捉了,但是没有捕捉到");
        }
        public HeadFish getHeadFish() {
        return headFish;
        }
   &nbsnbsp;    public void setHeadFish(HeadFish headFish) {
        this.headFish = headFish;
        }
        public int[] getFishOutlinePoint() {
        return fishOutlinePoint;
        }
        public boolean isCanRun() {
        return canRun;
        }
        public void setCanRun(boolean canRun) {
        this.canRun = canRun;
        }

6.3.3.2.游戏对象管理

仍然以小鱼为例,我们使用单例模式对鱼对象进行管理。以下只列出管理类的属性和方法的定义:

代码清单6-10.FishManager.java

/**
        * 鱼的管理器
        * @author Xiloerfan
        *
        */
        public class FishManager {
        /**
        * 单利模式
        */
        private static FishManager fishManager;
        private FishManager();
        public static FishManager getFishMananger();
        /**
        * 根据名字保存所有鱼的配置信息
        */
        private HashMap<String,FishInfo> allFishConfig = new HashMap<String,FishInfo>();
        /**
        * 根据名字保存所有鱼的动作配置信息
        */
        private HashMap<String,ActConfig[]> allFishActConfigs = new HashMap<String,ActConfig[]>();
        /**
        * 根据名字保存所有鱼的捕获动作配置信息
        */
        private HashMap<String,ActConfig[]> allFishCatchActConfigs = new HashMap<String,ActConfig[]>();
        /**
        * 根据名字缓存的鱼的动作图片
        */
        private HashMap<String,Bitmap[]> allFishActs = new HashMap<String,Bitmap[]>();
        /**
        * 根据名字缓存的鱼的捕获动作图片
        */
        private HashMap<String,Bitmap[]> allFishCatchActs = new HashMap<String,Bitmap[]>();
        /**
        * 鱼的种类
        */
    &nbsnbsp;   private ArrayList<String> allFish = new ArrayList<String>();
        /**
        * 根据XML配置文件,初始化所有鱼
        * 这里的配置文件还没定义,只是写在代码里了,以后可以改成通过读取配置文件来加载不同
        * 资源的鱼
        * @param initXml
        */
        /**
        * 是否可以创建新的鱼
        * 这个值的改变在以下会发生:
        * 每当调用updateFish方法时,会将这个值设置为false
        * updateFish方法执行完毕时,会将这个值在改变回true
        */
        private boolean createable = false;
        /**
        * 初始化管理器
        * 这里会读取fish文件夹下的FishConfig.plist文件,来加载所有其他配置信息
        */
        public void initFish();
        /**
        * 根据鱼的名字获取一条鱼的实例
        * @param fishName
        * @return
    nbsp;    */
        public Fish birthFishByFishName(String fishName);
        /**
        * 更新加载的鱼
        * @param fish
        */
        public void updateFish(String []fish);
        /**
        * 获取所有鱼的名字
        * @return
        */
&nnbsp;       public ArrayList<String> getAllFishName();
        /**
        * 销毁释放资源
        */
        public static void destroy();
        /**
        * 设置鱼的动作到管理器鱼动作结构中
        * @param fishName
        * @param fishActs
        * @return true:放置成功 false:放置失败
        */
private boolean getFishByName(String fishName,HashMap<String,ActConfig> configs);
        /**
        * 获取鱼的游动图片集
        * @param fishName
        * @return
        */
        private Bitmap[] getFishActByFishName(String fishName);
        /**
        * 获取鱼的被捕获图片集
        * @param fishName
        * @return
        */
        private Bitmap[] getFishCatchActsByFishName(String fishName);
        /**
        * 初始化鱼的配置信息
        * @param config
        */
        private void initFishInfo(String config);
        /**
        * 初始化鱼的动作信息
        * @param configs 将解析出来的每个配置文件放入这个Map中
        * @param fishActConfiges 所有的配置文件名称
        */
     &nbnbsp;  private void initFishAct(HashMap<String,ActConfig> configs,String fishActConfiges[]);

6.3.3.3.游戏对象逻辑处理

游戏核心的部分是玩家体验的过程,而对这一过程的处理属于游戏的逻辑部分。我们在Android游戏开发中通常使用线程来实现。

1.移动鱼群

a) 移动

鱼群的移动在FishRunThread中实现,下面是实现该功能的代码:

代码清单6-11.移动鱼群

/**
        * 移动鱼群
        */
        private void moveShoal(){
        try{
        if(fish.getShoal()==null){
        return;
        }
        for(Fish fishFlag:fish.getShoal()){
        if(!fishFlag.isCanRun()||!fishFlag.isAlive()){
        continue;
        }
        fishFlag.getFishOutlinePoint()[0] = (int)(fish.getFishOutlinePoint()[0]-fishFlag.getDistanceHeadFishX());
        fishFlag.getFishOutlinePoint()[1] = (int)(fish.getFishOutlinePoint()[1]-fishFlag.getDistanceHeadFishX());
        fishFlag.getFishOutlinePoint()[2] = (int)(fish.getFishOutlinePoint()[2]-fishFlag.getDistanceHeadFishY());
        fishFlag.getFishOutlinePoint()[3] = (int)(fish.getFishOutlinePoint()[3]-fishFlag.getDistanceHeadFishY());
        fishFlag.getPicMatrix().setTranslate(fish.getFish_X()-fishFlag.getDistanceHeadFishX(), fish.getFish_Y()-fishFlag.getDistanceHeadFishY());
        fishFlag.getPicMatrix().preRotate(fish.getCurrentRotate(),fishFlag.getFishRotatePoint_X(),fishFlag.getFishRotatePoint_Y());
                        }
                }catch(Exception e){
              nbsp;  }
        }

对鱼群移动的处理包括根据给定长度走直线(goStraight)、旋转鱼的角度并移动(rotateRightFish、rotateLeftFish)、设置鱼的外接矩形(setFishOutlinePoint),具体实现见工程。

b)碰撞检测

当鱼群超出屏幕边界时,程序也作出相应的操作:isAtOut和checkFishAtOut用于检测,isAtOut判断鱼是否部分在屏幕外,checkFishAtOut检测鱼是否完全在屏幕外。setFishAtOut用于进行当鱼群处于边界外的操作,以下给出setFishOut函数的代码:

代码清单6-12. setFishAtOut

/**
        * 处理鱼出了边界后的操作
        */
        private void setFishAtOut() {
        fishIsOut = true;
        new Thread(new Runnable(){
        public void run() {
        try{
        // TODO Auto-generated method stub
        //如果领头鱼有鱼群
        for(Fish fishFlag:fish.getShoal()){
        while(GamingInfo.getGamingInfo().isGaming()){
        if(checkFishAtOut(fish,fishFlag)){
        GamingInfo.getGamingInfo().getFish().remove(fishFlag);
        GamingInfo.getGamingInfo().getSurface().removeDrawablePic(fishFlag.getFishInfo().getFishInLayer(), fishFlag);
        fishFlag.getPicActThread().stopPlay();//停止动作
        break;
        }
        try{
        Thread.sleep(10);
        }catch(Exception e){
        e.printStackTrace();
                        }
                }
     nbsp;   }
        //让鱼群移动线程停掉
        setRun(false);
        //通知鱼群管理器,这条鱼已经离开屏幕
        if(GamingInfo.getGamingInfo().isGaming()){
        GamingInfo.getGamingInfo().getShoalManager().notifyFishIsOutOfScreen();
                        }
                }catch(Exception e){
                        LogTools.doLogForException(e);
                }
        }
        }).start();
        }

2.子弹

子弹的处理在ShotTread类中实现:

代码清单6-13.ShotTread.java

public class ShotThread extends Thread {
        private float targetX;
        private float targetY;
        private float currentX;
        private float currentY;
        private float ammoRotateX;
        private float ammoRotateY;
        private float speed_x;        // 取一个近似值,代表每帧移动的像素数
        private float speed_y;
        private int ammo_speed = 1000 / Constant.ON_DRAW_SLEEP;        // 子弹绘制速度,这个与屏幕刷新速度一样
        private Ammo ammo;        //子弹
        private boolean ammoActIsRun;        //子弹动画是否播放
  &nbsnbsp;     public ShotThread(float targetX, float targetY, Ammo ammo,float fromX,float fromY) {
        this.ammo = ammo;
        currentX = fromX;
        currentY = fromY;
        ammoRotateX = ammo.getPicWidth()/2;
        ammoRotateY = ammo.getPicHeight()/2;
        this.targetX = targetX;
        this.targetY = targetY;
        float x = Math.abs(this.targetX - fromX);        // 获取目标距离子弹始发的X坐标长度
        float y = Math.abs(this.targetY - fromY);        // 获取目标距离子弹始发的Y坐标长度
        float len = (float) Math.sqrt(x * x + y * y);        // 目标和始发点之间的距离
        float time = len / (Constant.AMMO_SPEED / Constant.ON_DRAW_SLEEP);    &nbnbsp;    // 计算目标与始发之间子弹需要行走的帧数
        speed_x = x / time;         // 计算子弹沿X轴行进的增量
        speed_y = y / time;         // 计算子弹沿Y轴行进的增量
        if (targetX < fromX) {
        speed_x = -speed_x;
        }
        if (targetY < fromY) {
        speed_y = -speed_y;
                }
        }
        public void run() {
        try{
        //如果子弹帧数多于1,就播放子弹动画
        if(ammo.getAmmoPicLenght()>1){
        new Thread(this.playAmmoAct()).start();
        }
        // 计算子弹需要的旋转角度
        float angle = Tool.getAngle(targetX, targetY, currentX, currentY);
        AmmoParticleEffect effect = ParticleEffectManager.getParticleEffectManager().getAmmoEffect();
        int ammoRedius = ammo.getPicHeight()/2;//这个半径的作用是用于计算子弹尾巴处出现粒子使用
        effect.playEffect((float)(ammoRedius*Math.cos(Math.toRadians(angle+180)))+ammoRotateX,-(float)(ammoRedius*Math.sin(Math.toRadians(angle+180)))+ammoRotateY,currentX, currentY, speed_x, speed_y);
        // 计算子弹的旋转(原理与大炮一样)
        if (angle >= 90) {
        angle = -(angle - 90);
        } else {
        angle = 90 - angle;
        }
        // 创建变换矩阵
     nbsp;   Matrix matrix = ammo.getPicMatrix();
        matrix.setTranslate(currentX, currentY);
        matrix.preRotate(angle,ammoRotateX,ammoRotateY);
        GamingInfo.getGamingInfo().getSurface()
        .putDrawablePic(Constant.AMMO_LAYER, ammo); // 将子弹放入图层,等待被绘制
        // 根据增量移动子弹
        while (GamingInfo.getGamingInfo().isGaming()) {
        while(!GamingInfo.getGamingInfo().isPause()){
        matrix.reset();
        matrix.setTranslate(currentX, currentY);
        matrix.preRotate(angle,ammoRotateX,ammoRotateY);
        currentX += speed_x;
        currentY += speed_y;
        effect.setEffectMatrix(currentX,currentY);
        if (checkHit()) {
        effect.stopEffect();
        // 命中后删除这个子弹
        GamingInfo.getGamingInfo().getSurface()
        .removeDrawablePic(Constant.AMMO_LAYER, ammo);
        CatchFishManager.getCatchFishManager().catchFishByAmmo(currentX, currentY, ammo);
        // 如果超出屏幕,从图层中删除子弹
        GamingInfo.getGamingInfo().getSurface()
        .removeDrawablePic(Constant.AMMO_LAYER, ammo);
        this.ammoActIsRun = false;//停止子弹动画
        break;
        } else if (currentX - 100 >= GamingInfo.getGamingInfo().getScreenWidth()
        || currentX + 100 <= 0 || currentY + 100 <= 0) {
        // 如果超出屏幕,从图层中删除子弹
        effect.stopEffect();
        GamingInfo.getGamingInfo().getSurface()
        .removeDrawablePic(Constant.AMMO_LAYER, ammo);
        this.ammoActIsRun = false;//停止子弹动画
        break;
        }
        try {
        Thread.sleep(ammo_speed);
        } catch (Exception e) {
                }
        }
        break;
                }
        }catch(Exception e){
        LogTools.doLogForException(e);
                }
        }
        private Runnable playAmmoAct(){
        Runnable runnable = new Runnable(){
        public void run() {
        ammoActIsRun = true;
        int picIndex = 0;
        try {
        while(GamingInfo.getGamingInfo().isGaming()){
        while(!GamingInfo.getGamingInfo().isPause()&&ammoActIsRun){
        ammo.setCurrentId(picIndex);
        picIndex++;
        if(picIndex==ammo.getAmmoPicLenght()){
        picIndex=0;
        }
        Thread.sleep(200);
        }
        break;
                }
        } catch (Exception e) {
        // TODO: handle exception
                        }
                }
        };
        return runnable;
        }
        private boolean checkHit() {
        try{
        ArrayList<Fish> allFish = (ArrayList<Fish>)GamingInfo.getGamingInfo().getFish().clone();
        for (Fish fish : allFish) {
        if (currentX > fish.getFishOutlinePoint()[0]
        && currentX < fish.getFishOutlinePoint()[1]
        && currentY >gt; fish.getFishOutlinePoint()[2]
        && currentY < fish.getFishOutlinePoint()[3]) {
        return true;
                        }
                }
        }catch(Exception e){
        LogTools.doLogForException(e);
        }
        return false;
                }
        }

6.3.4. 游戏特效

除了游戏对象的处理,我们还添加了简单的粒子系统、水纹效果来增强玩家的游戏体验。

6.3.4.1.粒子系统

粒子系统主要体现子弹的粒子效果、渔网的粒子效果、金币的粒子效果,子弹的粒子效果在AmmoParticleEffect类中实现,渔网的粒子效果在在NetParticleEffect类中实现,金币粒子效果在GoldParticleEffect中实现,粒子管理器是ParticleEffectManager类,以下只给出子弹粒子效果的实现代码:

代码清单6-14.NetParticleEffect.java

/**
        * 子弹粒子效果
        * @author Xiloer
        *
        */
        public class AmmoParticleEffect extends DrawableAdapter{
        private static byte ADD = 1;
        private static byte REMOVE = 2;
        private static byte UPDATE = 3;
        //粒子彩色图
        private Bitmap effectImgs[];
        private ArrayList<Particle> effects = new ArrayList<Particle>();
        private ArrayList<Particle> news = new ArrayList<Particle>();
        private ArrayList<Particle> removes = new ArrayList<Particle>();
        private int indexByDraw;//这个值用于绘制方法循环使用
        private Particle particle;//这个值用于绘制方法循环使用
        private boolean isPlay;//是否播放粒子效果
        private float targetOffsetX,targetOffsetY;//距离当前坐标的偏移量,这两个值加上currentX,currentY来得到粒子初始位置
        private float currentX,currentY;
        public AmmoParticleEffect(Bitmap effectImgs[]){
        this.effectImgs = effectImgs;
        }
        /**
        * 播放一次粒子效果
        * @param x 粒子的生成位置X
        * @param y 粒子的生成位置Y
        * @param offX 粒子偏移量X 这两个值是生成粒子时的行动路线,这个应该和给定的物体的偏移量相反
        * @param offY 粒子偏移量Y
        */
        public void playEffect(float targetOffsetX,float targetOffsetY,float x,float y,float offX,float offY){
        try{
        isPlay = true;
        this.targetOffsetX = targetOffsetX;
        this.targetOffsetY = targetOffsetY;
        startCreateEffectThread(x,y,offX,offY);
        GamingInfo.getGamingInfo().getSurface().putDrawablePic(Constant.PARTICLE_EFFECT_LAYER, this);
        }catch(Exception e){
        LogTools.doLogForException(e);
                }
        }
        private void updateEffect(byte mode,Particle p){
        if(mode==ADD){
        news.add(p);
        }else if(mode==REMOVE){
        removes.add(p);
        }else if(mode == UPDATE){
        if(news.size()>0){
        effects.addAll(news);
        news.clear();
        }
        if(removes.size()>0){
        effects.removeAll(removes);
        removes.clear();
                        }
                }
        }
        /**
        * 启动产生粒子的线程
        */
        private void startCreateEffectThread(final float x,final float y,final float offX,final float offY){
        this.currentX = x;
        this.currentY = y;
        new Thread(new Runnable() {
        public void run() {
        try{
        while(GamingInfo.getGamingInfo().isGaming()){
        while(!GamingInfo.getGamingInfo().isPause()&&isPlay){
        updateEffect(ADD,new Particle(currentX,currentY,offX,offY,0.5f,effectImgs[(int)(Math.random()*effectImgs.length)]));
        Thread.sleep((long)(Math.random()*201));
        }
        break;
                }
        }catch(Exception e){
        LogTools.doLogForException(e);
                }
       nbsp;         }
        }).start();
        }
        @Override
        public void onDraw(Canvas canvas, Paint paint) {
        updateEffect(UPDATE,null);
        indexByDraw = 0;
        while(GamingInfo.getGamingInfo().isGaming()){
        while(!GamingInfo.getGamingInfo().isPause()&&isPlay&&indexByDraw<effects.size()){
        particle = effects.get(indexByDraw);
        canvas.drawBitmap(particle.effect, particle.matrix, paint);
        indexByDraw++;
        }
        break;
                }
        }
        /**
        * 停止播放粒子
        */
        public void stopEffect(){
        this.isPlay = false;
        GamingInfo.getGamingInfo().getSurface().removeDrawablePic(Constant.PARTICLE_EFFECT_LAYER, this);
        }
        /**
        * 设置粒子位置
        */
        public void setEffectMatrix(float currentX,float currentY){
        this.currentX = currentX;
        this.currentY = currentY;
        Particle particle;
        for(int i =0;i<effects.size();i++){
        particle = effects.get(i);
        particle.offX -=particle.offX*0.05f;
        particle.offY -=particle.offY*0.05f;
        particle.scale -=particle.scale*0.05f;
        particle.currentX = particle.currentX+particle.offX;
        particle.currentY = particle.currentY+particle.offY;
        particle.matrix.setTranslate(particle.currentX, particle.currentY);
        particle.matrix.preScale(particle.scale, particle.scale);
        if(particle.scale<0.1){
        updateEffect(REMOVE,particle);
                        }
                }
        }
        public Bitmap getCurrentPic() {
        // TODO Auto-generated method stub
        return null;
        }
        public int getPicWidth() {
        // TODO Auto-generated method stub
        return 0;
        }
        public int getPicHeight() {
        // TODO Auto-generated method stub
        return 0;
        }
        /**
        * 粒子对象
        * @author Xiloer
        *
        */
        private class Particle{
        private Bitmap effect;
        /**
        * 当前粒子坐在坐标X
        */
        public float currentX;
        /**
        * 当前粒子坐在坐标Y
        */
        public float currentY;
        /**
        * 偏移量X
        */
        public float offX;
        /**
        * 偏移量Y
        */
        public float offY;
        /**
     nbsp;   * 缩放
        */
        public float scale;//缩放基数
        /**
        * 粒子矩阵
        */
        public Matrix matrix = new Matrix();
        /**
        *
        * @param currentX
        * @param currentY
        * @param offX
        * @param offY
        */
        public Particle(float currentX,float currentY,float offX,float offY,float scale,Bitmap effect){
        this.offX = offX;
        this.offY = offY;
        this.scale = scale;
        this.currentX = currentX-effect.getWidth()/2*scale+targetOffsetX;
        this.currentY = currentY-effect.getHeight()/2*scale+targetOffsetY;
        this.matrix.setTranslate(this.currentX, this.currentY);
        this.matrix.preScale(scale, scale);
        this.effect = effect;
                        }
                }
        }

6.3.4.2. 水纹效果

水波纹的实现在3D游戏中比较复杂,涉及到各种光线的处理。在2D游戏中相对简单,我们只需要对图片进行简单的处理即可。小鱼快跑中的水波纹在WaterRipper类中实现:

代码清单6-15.水波纹

/**
        * 水波纹
        * @author Xiloer
        *
        */
        public class WaterRipple extends DrawableAdapter{
        private Bitmap[] ripple;
        private int currentId;
        public WaterRipple(Bitmap[] ripple){
        this.ripple = ripple;
        }
        public void setCurrentId(int currentId) {
        this.currentId = currentId;
        }
        public Bitmap getCurrentPic() {
        // TODO Auto-generated method stub
        return ripple[currentId];
        }
        public int getPicWidth() {
        // TODO Auto-generated method stub
  nbsp;      return getCurrentPic().getWidth();
        }
        public int getPicHeight() {
        // TODO Auto-generated method stub
        return getCurrentPic().getHeight();
        }
        }

6.3.5. 游戏音效

游戏中不可或缺的另一个方面就是音效,音效容易让玩家将自己融入其中,随着游戏的节奏喜怒哀乐。游戏开发的高境界就是能带动玩家的情绪,如果游戏没有音效,会是一个什么样的情况呢?可能总是感觉缺少什么一样,玩家不会如此轻易地进入游戏的情节。好的游戏音效和音乐可以使玩家融入游戏世界,产生共鸣。音效的作用还不仅限于此。如果没有高超的游戏音效的映衬,再好的图像技巧也无法使游戏的表现摆脱平庸,对玩家也没有足够的吸引力。开发游戏时,人们常常忽视游戏的音效。开发者往往把主要精力花费在游戏的图像和动画等方面,而忽视了背景音乐和声音效果。当他们意识到这一点时,通常为时已晚,这种做法显然是不正确的。

游戏中的音效可分为如下几类:背景音乐、剧情音乐、音效(动作的音效、使用道具音效、辅助音效)等。背景音乐一般需要一直播放,而剧情音乐则只需要在剧情需要的时候播放,音效则是很短小的一段,比如挥刀的声音、怪物叫声等。

《小鱼快跑》中的音效存储在res文件夹的raw下,管理类分别在soundManager和musicManager实现。

6.4.小结

本章我们通过实现一个Android平台下的小游戏《小鱼快跑》学习了Android平台游戏开发的相关知识。本章所讲述的内容基本上包括了游戏开发中经常使用的技术,由于在前面的章节中我们介绍了有关图形绘制和操作的一些知识,我们把重点放在游戏框架、如何实现以及游戏开发流程上,关于该游戏的具体实现可以参考所附源代码。

发表评论
评论列表(网友评论仅供网友表达个人看法,并不表明本站同意其观点或证实其描述)