2048游戏回顾一:使用SurfaceView创建游戏启动动画

Posted 阳光玻璃杯

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2048游戏回顾一:使用SurfaceView创建游戏启动动画相关的知识,希望对你有一定的参考价值。

SurfaceView有个很大的好处,就是可以在子线程中绘制UI,其他的View只能在主线程中更新UI,这或多或少给编程增加了些不便。而SurfaceVIew在子线程中可以绘制UI的特性,再加上其可以直接从内存或者DMA等硬件接口取得图像数据,这使得它适合2d游戏的开发。

SurfaceView使用步骤

SurfaceView的使用比较简单,可以总结为如下几个步骤:

1.继承SurfaceView并实现 SurfaceHolder.Callback方法

譬如:

public class StartAniSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
  @Override
    public void surfaceCreated(SurfaceHolder holder) {
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {

    }
}

2.给SurfaceHolder对象注册回调方法

getHolder().addCallback(this);

SurfaceView中有个SurfaceHolder 的实例,这个实例是整个SurfaceView的核心,我们这个给这个SurfaceHolder实力添加回掉方法以后,就会导致surfaceCreated方法被回掉,用来通知你Surface已经准备好了,你可以绘图了。当你修改了SurfaceView的大小以后,surfaceChanged方法就会被会调,用来通知你Surface发生了改变,当你按下退出键,导致SurfaceView销毁的之前,surfaceDestroyed方法就会被调用,通知你Surface要销毁了,你赶紧手动释放需要释放的资源吧等等。
总之,addCallback这份方法一定要调用,不然回掉方法没有添加进去,就不可能有有毁掉方法调用的行为了。

3.在surfaceCreated方法中开始绘画

一般我们会在surfaceCreated方法中开启一个线程,让它来执行绘画的工作,当然不开启也没关系,直接绘画也可以。

准备好SurfaceView以后,SurfaceView的使用和其他的View就没有社么不同了,同门可以静态的在布局文件中使用,然后使用findViewById来获取到它的实例,也可以动态使用,直接new就可以了,相比也没什么好说的呢。

绘图

关于绘图,需要使用至少两个类:Canvas,和Paint,Paint可以理解成一个画笔,你在绘画前需要设置它的颜色,粗细等信息,Canvas可以理解成一个人,他手里拿着Paint,而且他有很多的技能,比如绘制矩形,椭圆等。我们说SurfaceView的核心是一个SurfaceHolder,所以,这个我们要绘图的话得有专业的画家,这个画家得向SurfaceHolder来要,它要是不给那就没办法了。因此,绘图的代码可以是这样:

        Canvas canvas = holder.lockCanvas();
        canvas.drawColor(Color.WHITE);
        paint.setColor(bgColor);
        canvas.drawRoundRect(0,0,getWidth(),getHeight(),cornerRadius,cornerRadius,paint);
        。。。
        holder.unlockCanvasAndPost(canvas);

可以看到我们首先向holder请求一个画家canvas,canvas会使用paint来绘画,画好以后化一定要交给画家的boss,也就是holder,使用holder.unlockCanvasAndPost(canvas);来实现,这样这幅画才能显示出来。
动画就是不断的这样话一幅又一幅的画,只不过这些画前后有联系,这样就能形成动画。

游戏的启动动画

下面以2048游戏的启动动画为例,具体介绍SurfaceView的使用。

在2048游戏的启动动画中,就是使用了SurfaceView来绘制动画的,这里再把效果图拿出来:
技术分享

这个动画模仿了焰火的效果,实现过程如下:

FlyNumber

一个飞出去的数字用FlyNumber类来表示,她的定义如下:

public class FlyNumber {
    public Point start;
    public Point control;
    public Point end;
    public Point cur;
    public float t;
    public int number;
    public int clolor;
    public int textSize;
    public FlyNumber(){
        start = new Point();
        control = new Point();
        end = new Point();
        cur = new Point();
        t=0f;
    }
    public void setStart(int x,int y){
        start.x=x;
        start.y=y;
    }
    public void setControl(int x,int y){
        control.x=x;
        control.y=y;
    }
    public void setEnd(int x,int y){
        end.x=x;
        end.y=y;
    }
}

类中定义了一个要飞出来的数字的值,颜色,大小,起始点,结束点,控制点以及时间等信息,说到起始点,结束点,控制点以及时间,大家应该已经想到了贝塞尔曲线了吧,是的每一个飞出去的数字使用贝塞尔曲线生成路线。

贝塞尔曲线

二次方公式
二次方贝兹曲线的路径由给定点P0、P1、P2的函数B(t)追踪:
技术分享
这里只用到二次贝塞尔曲线,所以只给出二次的公式,其他的就不深究了。根据这个公式,我们可以把它转换为java代码:

    public void getBesselFlayNumber(FlyNumber number,float gatT){
        number.t += gatT;
        double x = (1-number.t)*(1-number.t)*number.start.x +2*(1-number.t)*number.t*number.control.x + number.t*number.t*number.end.x;
        double y = (1-number.t)*(1-number.t)*number.start.y +2*(1-number.t)*number.t*number.control.y + number.t*number.t*number.end.y;
        number.cur.x = (int)x;
        number.cur.y = (int)y;
    }

我们会通过时间t,以及起始点,控制点,和结束点,生成当前时刻FlyNumber的值,也就是FlyNumber的cur的值。然后再cur.x,cur.y的地方绘制一个数字,绘制方法为:

    public void drawOneFlyNumber(Canvas canvas, Paint paint, FlyNumber number){
        paint.setTextSize(number.textSize);
        paint.setColor(number.clolor);
        canvas.drawText(String.valueOf(number.number),number.cur.x,number.cur.y,paint);
    }

调用可以想象我们需要不断的生成数字,然后绘制出所有生成的数字,当这个数字到达终点后,就把它销毁掉,怎么实现呢?用一个链表就可以实现,思路如下:
不断的构造FlyNumber对象,构造好它的起点,终点,控制点等信息以后,把它放到一个链表中,这样生成端什么都不考虑,只管生成。绘制端不断的从遍历链表,把通过贝塞尔曲线函数生成坐标,然后绘制它,当一个FlyNumber到达终点后,就把它从链表中移除。
根据这个思路,代码如下:

         public void run() {
                ArrayList<FlyNumber> arrayList=new ArrayList<>();
                drawFlayNumbersBessel(holder,paint,arrayList,aniCount);
               ...

在绘制线程的run方法中构建一个链表:arrayList,然后把它传给drawFlayNumbersBessel方法继续处理:


    final int MAX = 100;
    public void drawFlayNumbersBessel(SurfaceHolder holder,Paint paint,ArrayList<FlyNumber> arrayList,final int count){
        int countl = 0;
        final float gapT = 1.0f/MAX;
        for(int i=0;i<100;i++){
            arrayList.add(generateRandomNumber());
        }
        Canvas canvas;
        FlyNumber number;
        while (countl++<(count)){
            canvas = holder.lockCanvas();
            if(countl<count-MAX){
                arrayList.add(generateRandomNumber());
                arrayList.add(generateRandomNumber());
            }
            if(canvas != null){
                canvas.drawColor(Color.WHITE);
                paint.setColor(bgColor);
                canvas.drawRoundRect(0,0,getWidth(),getHeight(),cornerRadius,cornerRadius,paint);
                if(arrayList.size()>0){
                    Iterator<FlyNumber> iterator = arrayList.iterator();
                    while (iterator.hasNext()){
                        number = iterator.next();
                        if(number.t>=1.0f){
                            iterator.remove();
                        }
                        getBesselFlayNumber(number,gapT);
                        drawOneFlyNumber(canvas,paint,number);
                        int dif = count-countl;
                        if(dif<50 && dif>0){
                            drawWelComeState(canvas,paint,255-(count-countl)*3,300-(count-countl)*6);
                        }
                    }
                }
                holder.unlockCanvasAndPost(canvas);
            }
        }
    }

这份方法如下事情:
1.初始化100个数字

        for(int i=0;i<100;i++){
            arrayList.add(generateRandomNumber());
        }

2.每次循环,如果循环次数countl

            if(countl<count-MAX){
                arrayList.add(generateRandomNumber());
                arrayList.add(generateRandomNumber());
            }

为什么是countl

getBesselFlayNumber(number,gapT);

4.绘制FlyNumber

drawOneFlyNumber(canvas,paint,number);

5.最后50次绘制不断放大的2048字样

drawWelComeState(canvas,paint,255-(count-countl)*3,300-(count-countl)*6);

drawWelComeState函数如下:

    private void drawWelComeState(Canvas canvas,Paint paint,int alpha,int textSieze){
        paint.setAlpha(alpha);
        paint.setTextSize(textSieze);
        paint.setColor(Color.WHITE);
        String string = "2048";
        float width = paint.measureText(string);
        canvas.drawText(string,getWidth()/2-width/2,getHeight()/2+textSieze/3,paint);
    }

大家在计算数字位置的时候,使用paint.measureText方法能准确的计算出字符串的宽度,这在绘制字符串的过程中十分常用。

这样2048游戏的开机动画就结束了。最后,把整个StartAniSurfaceView类贴出来,方便对比:

package com.jinwei.tvgame2048.ui;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import com.jinwei.tvgame2048.R;
import com.jinwei.tvgame2048.model.FlyNumber;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.Random;

/**
 * Created by Jinwei on 2016/10/27.
 */
public class StartAniSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
    private final int textSize = 60;
    private final int bgColor = Color.BLACK;
    private final int aniCount = 300;
    private final int forbidArea = 200;
    private final int gameNameSize = 300;
    private final int cornerRadius = 20;
    private Handler mHandler;
    public interface AniOverListener{
        public void onAniOver();
    }
    public void setHandler(Handler handler){
        mHandler = handler;
    }
    AniOverListener mListener;
    public StartAniSurfaceView(Context context) {
        super(context);
    }

    public StartAniSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public void init(){
        getHolder().addCallback(this);
    }
    public void setAniOverListener(AniOverListener listener){
        mListener = listener;
    }
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        doDraw(holder,paint);
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {

    }
    public void getBesselFlayNumber(FlyNumber number,float gatT){
        number.t += gatT;
        double x = (1-number.t)*(1-number.t)*number.start.x +2*(1-number.t)*number.t*number.control.x + number.t*number.t*number.end.x;
        double y = (1-number.t)*(1-number.t)*number.start.y +2*(1-number.t)*number.t*number.control.y + number.t*number.t*number.end.y;
        number.cur.x = (int)x;
        number.cur.y = (int)y;
    }

    public void drawOneFlyNumber(Canvas canvas, Paint paint, FlyNumber number){
        paint.setTextSize(number.textSize);
        paint.setColor(number.clolor);
        canvas.drawText(String.valueOf(number.number),number.cur.x,number.cur.y,paint);
    }
    public void doDraw(final SurfaceHolder holder,final Paint paint){
        new Thread(new Runnable() {
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.game_name);
            @Override
            public void run() {
                ArrayList<FlyNumber> arrayList=new ArrayList<>();
                drawFlayNumbersBessel(holder,paint,arrayList,aniCount);
                drawGameName(holder,paint);
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        if(mListener!=null){
                            mListener.onAniOver();
                        }
                    }
                },2000);

            }
        }).start();
    }
    int numbers[] = {2,0,4,8};
    public FlyNumber generateRandomNumber(){
        FlyNumber number = new FlyNumber();
        Random random = new Random();
        number.setStart(getWidth()/2,getHeight());
        number.setControl(getWidth()/2,0);
        int endX = random.nextInt(getWidth());
        int endY = random.nextInt(getHeight());
        if(endY>getHeight()/2){
            if (endX>forbidArea&&endX<getWidth()/2){
                endX -= forbidArea;
            }else if(endX>getWidth()/2 && endX<getWidth()-forbidArea){
                endX+=forbidArea;
            }
        }
        number.setEnd(endX,endY);
        number.t = 0;
        number.number = numbers[random.nextInt(4)];
        number.clolor = Color.rgb(random.nextInt(256),random.nextInt(256),random.nextInt(256));
        number.textSize = random.nextInt(textSize);
        return number;
    }

    final int MAX = 100;
    public void drawFlayNumbersBessel(SurfaceHolder holder,Paint paint,ArrayList<FlyNumber> arrayList,final int count){
        int countl = 0;
        final float gapT = 1.0f/MAX;
        for(int i=0;i<100;i++){
            arrayList.add(generateRandomNumber());
        }
        Canvas canvas;
        FlyNumber number;
        while (countl++<(count)){
            canvas = holder.lockCanvas();
            if(countl<count-MAX){
                arrayList.add(generateRandomNumber());
                arrayList.add(generateRandomNumber());
            }
            if(canvas != null){
                canvas.drawColor(Color.WHITE);
                paint.setColor(bgColor);
                canvas.drawRoundRect(0,0,getWidth(),getHeight(),cornerRadius,cornerRadius,paint);
                if(arrayList.size()>0){
                    Iterator<FlyNumber> iterator = arrayList.iterator();
                    while (iterator.hasNext()){
                        number = iterator.next();
                        if(number.t>=1.0f){
                            iterator.remove();
                        }
                        getBesselFlayNumber(number,gapT);
                        drawOneFlyNumber(canvas,paint,number);
                        int dif = count-countl;
                        if(dif<50 && dif>0){
                            drawWelComeState(canvas,paint,255-(count-countl)*3,300-(count-countl)*6);
                        }
                    }
                }
                holder.unlockCanvasAndPost(canvas);
            }
        }
    }
    private void drawWelComeState(Canvas canvas,Paint paint,int alpha,int textSieze){
        paint.setAlpha(alpha);
        paint.setTextSize(textSieze);
        paint.setColor(Color.WHITE);
        String string = "2048";
        float width = paint.measureText(string);
        canvas.drawText(string,getWidth()/2-width/2,getHeight()/2+textSieze/3,paint);
    }
    private void drawGameName(SurfaceHolder holder,Paint paint){
        Canvas canvas = holder.lockCanvas();
        canvas.drawColor(Color.WHITE);
        paint.setColor(bgColor);
        canvas.drawRoundRect(0,0,getWidth(),getHeight(),cornerRadius,cornerRadius,paint);
        drawWelComeState(canvas,paint,255,gameNameSize);
        holder.unlockCanvasAndPost(canvas);
    }
}









以上是关于2048游戏回顾一:使用SurfaceView创建游戏启动动画的主要内容,如果未能解决你的问题,请参考以下文章

《黑马程序猿》 cocos2d游戏引擎复习笔记一

Java 实现2048游戏之详细教程

2048小游戏竟然还有3D版?使用MATLAB制作一款3D版2048小游戏

2048小游戏竟然还有3D版?使用MATLAB制作一款3D版2048小游戏

2048作为一款简单的益智小游戏,都有哪些技巧可以快速合成2048?

android:怎样用一天时间,写出“飞机大战”这种游戏!(无框架-SurfaceView绘制)