Android游戏——贪吃蛇开发实录(改进后的源码和详解)

        本人大学刚毕业,进入公司后做的第一个Android入门小游戏——贪吃蛇。从APP的构思设计到完成,差不多经历了一个星期的时间。现在回想起来感觉挺有收获,所以决定把源码和理解分享出来,以此和广大的程序员朋友们交流交流经验。

        该游戏实现的思路和源码参考了Google自带的Snake的例子,其中修改了一些个人认为还不够完善的地方,加入了一些新的功能,比如屏幕上的方向操作盘,暂停按钮,开始按钮,退出按钮。另外,为了稍微增加些用户体验,除了游戏的主界面,本人自己新增了5个界面,分别是登陆界面,菜单界面,背景音乐设置界面,难度设置界面,还有个关于游戏的介绍界面。个人觉得在新手阶段,参考现成的思路和实现方式是难以避免的。重要的是我们需要有自己的理解,读懂代码之后,需要思考代码背后的实现逻辑,形成自己的思维。这样在下次开发工作时,就不用参考别人自己也能涌现出解决的思路。

        我觉得经过自己的构思和实践,做出一个可操作有界面的小作品还是挺有成就感的,在探索和思考的过程中时间过的很快,这段时间基本都是自觉的加班到晚上9点以后。好了,下面切入正题,我考虑了下讲述的顺序,决定就以进入软件后的界面顺序来把。

        由于篇幅的关系,布局的XML文件就不发了,而且我把导包的语句也省略了,反正像AS,eclipse这些工具都是可以智能导包的。完整的源码和配置文件下载地址http://download.csdn.net/detail/kuaiguixs/9588233。那么,首先是登陆界面,找了些网上的资源当背景。布局还是比较简单的。下图中,左图为效果图,右图为逻辑实现的流程图。

Android游戏——贪吃蛇开发实录(改进后的源码和详解)_第1张图片Android游戏——贪吃蛇开发实录(改进后的源码和详解)_第2张图片

// MainActivity.java
package con.example.wang.game;
public class MainActivity extends Activity implements OnClickListener{
    Button button;
    EditText edit1,edit2;
    CheckBox checkbox;
    ProgressBar bar;
    SharedPreferences pref;
    SharedPreferences.Editor editor;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button=(Button) findViewById(R.id.login_button);
        edit1=(EditText) findViewById(R.id.input1);
        edit2=(EditText) findViewById(R.id.input2);
        checkbox=(CheckBox) findViewById(R.id.remember_button);
        bar=(ProgressBar) findViewById(R.id.progress);
        pref= PreferenceManager.getDefaultSharedPreferences(this);
        boolean isRemember=pref.getBoolean("rem",false);     //获取代表是否保存密码的变量值,这里初值设为false

        if(isRemember) {
            //如果记住密码,则将账号和密码自动填充到文本框中
            String account=pref.getString("account","");
            String password=pref.getString("password","");
            edit1.setText(account);
            edit2.setText(password);
            checkbox.setChecked(true);
        }
        button.setOnClickListener(this);
    }
    @Override
    public void onClick(View v){
        new Thread(new Runnable(){      //开启线程运行进度条,减少主线程的压力,这里不用子线程也影响不大
            @Override
            public void run() {
                for (int i = 0; i < 25; i++) {
                    int progress = bar.getProgress();
                    progress = progress + 10;
                    bar.setProgress(progress);
                }
            }
        }).start();

        String account=edit1.getText().toString();
        String password=edit2.getText().toString();
        if(account.equals("admin") && password.equals("123456")) {
            editor = pref.edit();    //这个方法用于向SharedPreferences文件中写数据
            if(checkbox.isChecked()) {
                editor.putBoolean("rem",true);
                editor.putString("account",account);
                editor.putString("password",password);
            }
            else {
                editor.clear();
            }
            editor.commit();    //这个方法必须要有,不然数据不会被保存。生效后,就可以从该文件中读取数据。
            Intent intent=new Intent(MainActivity.this,SecondActivity.class);
            startActivity(intent);
        }
        else{    //如果用户名或密码不正确,这里会弹出一个提示框
            Toast.makeText(MainActivity.this,"账号或用户名错误",Toast.LENGTH_SHORT).show();
        }
    }
}

        这个逻辑还算比较简单,实现了记住密码的功能,这里的数据存储使用的是SharedPreferences。点击登陆后,会进入一个菜单界面,这里设置几个四个按钮,分别做好监听就可以了,然后用Intent在活动间跳转就好了。效果图也分享一下。

Android游戏——贪吃蛇开发实录(改进后的源码和详解)_第3张图片


// SecondActivity.java
package com.example.wang.game;
public class SecondActivity extends Activity implements OnClickListener{

    ImageButton button1,button2,button3,button4;
    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        button1=(ImageButton) findViewById(R.id.button_start);
        button2=(ImageButton) findViewById(R.id.button_difficulty);
        button3=(ImageButton) findViewById(R.id.button_music);
        button4=(ImageButton) findViewById(R.id.button_about);
        button4.setOnClickListener(this);
        button3.setOnClickListener(this);
        button2.setOnClickListener(this);
        button1.setOnClickListener(this);
    }
    @Override
    public void onClick(View v){
        switch(v.getId()) {        //看下Intent的用法,还是挺方便的,这里用的都是显式的方法
            case R.id.button_about:
                Intent intent1 = new Intent(SecondActivity.this, AboutActivity.class);
                startActivity(intent1);
                break;
            case R.id.button_music:
                Intent intent2 = new Intent(SecondActivity.this, MusicActivity.class);
                startActivity(intent2);
                break;
            case R.id.button_difficulty:
                Intent intent3 = new Intent(SecondActivity.this, DifficultyActivity.class);
                startActivity(intent3);
                break;
            case R.id.button_start:
                Intent intent4 = new Intent(SecondActivity.this, GameActivity.class);
                startActivity(intent4);
                break;
            default:
                break;
        }
    }
}



        下面先讲难度设置界面把,这个和背景音乐开关其实差不多,所以以此为例,背景音乐开关界面就不啰嗦了。这里也是用的SharedPreferences存储数据。这里布局文件里把三个RadioButton放入RadioGroup,实现单选的效果。给三个按钮设置监听,触发事件后分别返回对应的三个变量,这三个变量控制的是贪吃蛇运行的速度。参考下流程图更好理解。

Android游戏——贪吃蛇开发实录(改进后的源码和详解)_第4张图片Android游戏——贪吃蛇开发实录(改进后的源码和详解)_第5张图片

//  DifficultyActivity.java
package com.example.wang.game;
public class DifficultyActivity extends Activity implements OnClickListener{
    private SharedPreferences saved;
    private SharedPreferences.Editor editor;

    RadioButton button_jiandan,button_yiban,button_kunnan;
    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_difficulty);
        saved = PreferenceManager.getDefaultSharedPreferences(this);
        int level = saved.getInt("nandu",500);

        button_jiandan = (RadioButton) findViewById(R.id.button_difficulty1);
        button_yiban = (RadioButton) findViewById(R.id.button_difficulty2);
        button_kunnan = (RadioButton) findViewById(R.id.button_difficulty3);
        button_jiandan.setOnClickListener(this);
        button_yiban.setOnClickListener(this);
        button_kunnan.setOnClickListener(this);
    }
    @Override
    public void onClick(View v){
        editor=saved.edit();
        switch(v.getId()){
            case R.id.button_difficulty1:
                if(button_jiandan.isChecked()){
                    editor.putInt("nandu",500);
                }
                break;
            case R.id.button_difficulty2:
                if(button_yiban.isChecked()){
                    editor.putInt("nandu",200);
                }
                break;
            case R.id.button_difficulty3:
                if(button_kunnan.isChecked()){
                    editor.putInt("nandu",100);
                }
                break;
        }
        editor.commit();
    }
}

        其它的两个辅助界面比较简单,背景音乐开关界面也是通过SharedPreferences文件存储一个boolean的值,true的话就播放音乐,false就不播放。关于游戏的介绍界面就加一些文字。上述这些都是辅助,下面是游戏的主体部分。

        游戏界面的设计思路就是将手机屏幕分为多行多列的像素块,以像素块为最小单位,确定各点的坐标。这里每个像素块的大小设置为32像素。我的手机模拟器的屏幕分辨率为768*1280,由公式可算出,我的游戏界面x轴上坐标最大为24,y轴上坐标最大为35。坐标完成后,这里会使用三种颜色不同的图片来填充像素块,这里就叫砖块把。

        根据java面向对象的逻辑,需要给各块内容分类,蛇,苹果,边界的墙都是必不可少的元素。视图的初始化也是围绕着这三个元素展开的。其实这里蛇,苹果和边界墙就是由不同颜色的砖块表示出来的。该部分内容包含三个java文件,首先是砖块的初始化。

//  TileView.java
package com.example.wang.game;
public class TileView extends View {
    public static int mTileSize =32;
    public static int mXTileCount;  //地图上所能容纳的格数
    public static int mYTileCount;
    public static int mXOffset;     //偏移量
    public static int mYOffset;
    Bitmap[] mTileArray;            //放置图片的数组
    int[][] mTileGrid;              //存放各坐标对应的图片

    public TileView(Context context, AttributeSet attrs,int defStyle){
        super(context,attrs,defStyle);
    }
    public TileView(Context context, AttributeSet attrs){
        super(context,attrs);
    }
    public TileView(Context context){
        super(context);
    }
    //加载三幅小图片
    public void loadTile(int key, Drawable tile) {
        Bitmap bitmap = Bitmap.createBitmap(mTileSize, mTileSize, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        tile.setBounds(0, 0, mTileSize, mTileSize);
        tile.draw(canvas);
        mTileArray[key] = bitmap;
    }
    //给地图数组赋值
    public void setTile(int tileindex, int x, int y) {
        mTileGrid[x][y] = tileindex;
    }
    public void resetTiles(int tilecount) {
        mTileArray = new Bitmap[tilecount];
    }
    //我的游戏界面不是占满整个屏幕,所以地图遍历的时候,y不是从0开始
    public void clearTiles() {
        for (int x = 0; x < mXTileCount; x++) {
            for (int y = 2; y < mYTileCount-8; y++) {
                setTile(0, x, y);
            }
        }
    }
    //计算当前屏幕在X,Y轴上分别所能容纳的最大砖块数量
    //这里输出“mXTileCount"和”mYTileCount"的值后面会用到
    @Override
    public void onSizeChanged(int w, int h, int oldw, int oldh){
        //地图数组初始化
        mXTileCount = (int) Math.floor(w / mTileSize);
        mYTileCount = (int) Math.floor(h / mTileSize);
//        System.out.println("-------"+mXTileCount+"----------");
//        System.out.println("-------"+mYTileCount+"----------");
        //可能屏幕的长宽不能整除,所以够分成一格的分成一格, 剩下不够一格的分成两份,左边一份,右边一份
        mXOffset = ((w - (mTileSize * mXTileCount)) / 2);
        mYOffset = ((h - (mTileSize * mYTileCount)) / 2);
//        System.out.println("-------"+mXOffset+"----------");
//        System.out.println("-------"+mYOffset+"----------");
        mTileGrid = new int[mXTileCount][mYTileCount];
        clearTiles();
    }
    @Override
    public void onDraw(Canvas canvas){
        super.onDraw(canvas);
    }
}

       

        其实上述这段程序就是实现了几个方法,loadTile()用于加载图片,setTile()和resetTile()是把图片与坐标联系起来,而onSizedChanged()是把手机屏幕像素块化。这些方法都将为以下这个类服务。为了便于利用这些方法,以下这个类继承自TileView。

        由于我加了一些按钮,所以游戏界面生成时没有占满整个屏幕,所以在设置坐标和遍历地图时与源码的数据相差较多。

//  SnakeView.java
package com.example.wang.game;
public class SnakeView extends TileView{

    static int mMoveDelay = 500;
    private long mLastMove;

    private static final int RED_STAR = 1;
    private static final int YELLOW_STAR = 2;
    private static final int GREEN_STAR = 3;

    private static final int UP = 1;
    private static final int DOWN = 2;
    private static final int RIGHT = 3;
    private static final int LEFT = 4;
    static int mDirection = RIGHT;
    static int mNextDirection = RIGHT;
    // 这里把游戏界面分为5种状态,便于逻辑实现
    public static final int PAUSE = 0;
    public static final int READY = 1;
    public static final int RUNNING = 2;
    public static final int LOSE = 3;
    public static final int QUIT = 4;
    public int mMode = READY;
    public int newMode;

    private TextView mStatusText;    // 用于每个状态下的文字提醒
    public long mScore = 0;

    private ArrayList mSnakeTrail = new ArrayList(); // 存储蛇的所有坐标的数组
    private ArrayList mAppleList = new ArrayList();  // 存储苹果的所有坐标的数组

    private static final Random RNG = new Random();    //用于生成苹果坐标的随机数
//    private static final String TAG = "SnakeView";

    //开启线程,不断调用更新和重绘。这里利用了Handler类来实现异步消息处理机制
    MyHandler handler=new MyHandler();
    class MyHandler extends Handler{
        @Override
        public void handleMessage(Message msg) {
            SnakeView.this.update();         //不断调用update()方法
            SnakeView.this.invalidate();    //请求重绘,不断调用ondraw()方法
        }
        //调用sleep后,在一段时间后再sendmessage进行UI更新
        public void sleep(int delayMillis) {
            this.removeMessages(0);         //清空消息队列
            sendMessageDelayed(obtainMessage(0), delayMillis);
        }
    }
    //这是三个构造方法,别忘了加上下面这个初始化方法
    public SnakeView(Context context, AttributeSet attrs, int defStyle){
        super(context,attrs,defStyle);
        initNewGame();
    }
    public SnakeView(Context context, AttributeSet attrs){
        super(context,attrs);
        setFocusable(true);
        initNewGame();
    }
    public SnakeView(Context context){
        super(context);
    }
    //添加苹果的方法,最后将生成的苹果坐标存储在上面定义的数组中
    private void addRandomApple() {
        Coordinate newCoord = null;
        boolean found = false;
        while (!found) {
            // 这里设定了苹果坐标能随机生成的范围,并生成随机坐标。这里Google源码中是直接使用变量
            // mXTileCount和mYTileCount,我编译时会报错,因为随机数不能生成负数,而直接使用这两个变量程序不能
            // 识别这个变量减去一个数后是否会是负数,所以我把TileView里输出的确切值放了进去
            int newX = 1 + RNG.nextInt(24-2);
            int newY = 3 + RNG.nextInt(35-12);
            newCoord = new Coordinate(newX, newY);

            boolean collision = false;
            int snakelength = mSnakeTrail.size();
            //遍历snake, 看新添加的apple是否在snake体内, 如果是,重新生成坐标
            for (int index = 0; index < snakelength; index++) {
                if (mSnakeTrail.get(index).equals(newCoord)) {
                    collision = true;
                }
            }
            found = !collision;
        }
//        if (newCoord == null) {
//            Log.e(TAG, "Somehow ended up with a null newCoord!");
//        }
        mAppleList.add(newCoord);
    }

    //绘制边界的墙
    private void updateWalls() {
        for (int x = 0; x < mXTileCount; x++) {
            setTile(GREEN_STAR, x, 2);
            setTile(GREEN_STAR, x, mYTileCount - 8);
        }
        for (int y = 2; y < mYTileCount - 8; y++) {
            setTile(GREEN_STAR, 0, y);
            setTile(GREEN_STAR, mXTileCount - 1, y);
        }
    }
    //更新蛇的运动轨迹
    private void updateSnake(){
        boolean growSnake = false;
        Coordinate head = mSnakeTrail.get(0);
        Coordinate newHead = new Coordinate(1, 1);

        mDirection = mNextDirection;
        switch (mDirection) {
            case RIGHT: {
                newHead = new Coordinate(head.x + 1, head.y);
                break;
            }
            case LEFT: {
                newHead = new Coordinate(head.x - 1, head.y);
                break;
            }
            case UP: {
                newHead = new Coordinate(head.x, head.y - 1);
                break;
            }
            case DOWN: {
                newHead = new Coordinate(head.x, head.y + 1);
                break;
            }
        }
        //检测是否撞墙
        if ((newHead.x < 1) || (newHead.y < 3) || (newHead.x > mXTileCount - 2)
                || (newHead.y > mYTileCount - 9)) {
            setMode(LOSE);
            return;
        }
        //检测蛇头是否撞到自己
        int snakelength = mSnakeTrail.size();
        for (int snakeindex = 0; snakeindex < snakelength; snakeindex++) {
            Coordinate c = mSnakeTrail.get(snakeindex);
            if (c.equals(newHead)) {
                setMode(LOSE);
                return;
            }
        }
        //检测蛇是否吃到苹果
        int applecount = mAppleList.size();
        for (int appleindex = 0; appleindex < applecount; appleindex++) {
            Coordinate c = mAppleList.get(appleindex);
            if (c.equals(newHead)) {
                mAppleList.remove(c);
                addRandomApple();
                mScore++;
                mMoveDelay *= 0.95;    //蛇每迟到一个苹果,延时就会减少,蛇的速度就会加快
                growSnake = true;
            }
        }
        mSnakeTrail.add(0,newHead);
        if(!growSnake) {
            mSnakeTrail.remove(mSnakeTrail.size() - 1);
        }
        //蛇头和蛇身分别设置不同的图片
        int index=0;
        for(Coordinate c:mSnakeTrail) {
            if(index == 0) {
                setTile(RED_STAR, c.x, c.y);
            } else {
                setTile(YELLOW_STAR,c.x,c.y);
            }
            index++;
        }
    }
    给苹果加载对应的图片
    private void updateApples() {
        for (Coordinate c : mAppleList) {
            setTile(YELLOW_STAR, c.x, c.y);
        }
    }
    // 该方法很重要,用于更新蛇,苹果和墙的坐标
    // 这里设置了更新的时间间隔,我发现不加这个延时的话蛇运动时容易出现一下跳很多格的情况
     public void update(){
        if(mMode == RUNNING) {
            long now = System.currentTimeMillis();
            if (now - mLastMove > mMoveDelay) {
                clearTiles();
                updateWalls();
                updateSnake();
                updateApples();
                mLastMove = now;
            }
            handler.sleep(mMoveDelay);
        }
    }

    //图像初始化,引入图片资源
    private void initSnakeView() {
        setFocusable(true);     //添加焦点
        Resources r = this.getContext().getResources();
        //添加几种不同的tile
        resetTiles(4);
        //从文件中加载图片
        loadTile(RED_STAR, r.getDrawable(R.drawable.redstar));
        loadTile(YELLOW_STAR, r.getDrawable(R.drawable.yellowstar));
        loadTile(GREEN_STAR, r.getDrawable(R.drawable.greenstar));
        update();
    }
    // 数据初始化方法,定义蛇的起始坐标,运动方向和得分变量。给数组添加坐标的时候注意顺序,因为有蛇头和蛇尾的区别
    public void initNewGame() {
        mSnakeTrail.clear();
        mAppleList.clear();
        //snake初始状态时的个数和位置,方向
        mSnakeTrail.add(new Coordinate(8, 7));
        mSnakeTrail.add(new Coordinate(7, 7));
        mSnakeTrail.add(new Coordinate(6, 7));
        mSnakeTrail.add(new Coordinate(5, 7));
        mSnakeTrail.add(new Coordinate(4, 7));
        mSnakeTrail.add(new Coordinate(3, 7));
        mDirection = RIGHT;
        mNextDirection = RIGHT;  // 这个变量必须初始化,不然每次游戏结束重新开始后,蛇初始的方向将不是向右,而是你游戏结束时蛇的方向,
                                                      // 如果死的时候,蛇的方向向左,那么再次点击开始时会无法绘出蛇的图像
        addRandomApple();
        mScore=0;
    }
    // 根据各个数组中的数据,遍历地图设置各点的图片
    public void onDraw(Canvas canvas){
        super.onDraw(canvas);
        Paint paint=new Paint();
        initSnakeView();
        //遍历地图绘制界面
        for (int x = 0; x < mXTileCount; x++) {
            for (int y = 0; y < mYTileCount; y++) {
                if (mTileGrid[x][y] > 0) {    // 被加了图片的点mTileGird是大于0的
                    canvas.drawBitmap(mTileArray[mTileGrid[x][y]], mXOffset + x * mTileSize, mYOffset + y * mTileSize, paint);
                }
            }
        }
    }

    //把蛇和苹果各点对应的坐标利用一个一维数组储存起来
    private int[] coordArrayListToArray(ArrayList cvec) {
        int count = cvec.size();
        int[] rawArray = new int[count * 2];
        for (int index = 0; index < count; index++) {
            Coordinate c = cvec.get(index);
            rawArray[2 * index] = c.x;
            rawArray[2 * index + 1] = c.y;
        }
        return rawArray;
    }

    //将当前所有的游戏数据全部保存
    public Bundle saveState() {
        Bundle map = new Bundle();
        map.putIntArray("mAppleList", coordArrayListToArray(mAppleList));
        map.putInt("mDirection", Integer.valueOf(mDirection));
        map.putInt("mNextDirection", Integer.valueOf(mNextDirection));
        map.putInt("mMoveDelay", Integer.valueOf(mMoveDelay));
        map.putLong("mScore", Long.valueOf(mScore));
        map.putIntArray("mSnakeTrail", coordArrayListToArray(mSnakeTrail));
        return map;
    }
    //是coordArrayListToArray()的逆过程,用来读取数组中的坐标数据
    private ArrayList coordArrayToArrayList(int[] rawArray) {
        ArrayList coordArrayList = new ArrayList();
        int coordCount = rawArray.length;
        for (int index = 0; index < coordCount; index += 2) {
            Coordinate c = new Coordinate(rawArray[index], rawArray[index + 1]);
            coordArrayList.add(c);
        }
        return coordArrayList;
    }
    //saveState()的逆过程,用于恢复游戏数据
    public void restoreState(Bundle icicle) {
        setMode(PAUSE);
        mAppleList = coordArrayToArrayList(icicle.getIntArray("mAppleList"));
        mDirection = icicle.getInt("mDirection");
        mNextDirection = icicle.getInt("mNextDirection");
        mMoveDelay = icicle.getInt("mMoveDelay");
        mScore = icicle.getLong("mScore");
        mSnakeTrail = coordArrayToArrayList(icicle.getIntArray("mSnakeTrail"));
    }
    // 设置键盘监听,在模拟器中可以使用电脑键盘控制蛇的方向
    public boolean onKeyDown(int keyCode, KeyEvent event){
        if(keyCode == KeyEvent.KEYCODE_DPAD_UP){
            if(mDirection != DOWN) {
                mNextDirection = UP;
            }
            return (true);
        }
        if(keyCode == KeyEvent.KEYCODE_DPAD_DOWN){
            if(mDirection != UP) {
                mNextDirection = DOWN;
            }
            return (true);
        }
        if(keyCode == KeyEvent.KEYCODE_DPAD_RIGHT){
            if(mDirection != LEFT) {
                mNextDirection = RIGHT;
            }
            return (true);
        }
        if(keyCode == KeyEvent.KEYCODE_DPAD_LEFT){
            if(mDirection != RIGHT) {
                mNextDirection = LEFT;
            }
            return (true);
        }
        return super.onKeyDown(keyCode,event);
    }

    public void setTextView(TextView newView) {
        mStatusText = newView;
    }
    // 设置不同状态下提示文字的显示内容和可见状态
    public void setMode(int newMode) {
        this.newMode=newMode;
        int oldMode = mMode;
        mMode = newMode;
        if (newMode == RUNNING & oldMode != RUNNING) {
            mStatusText.setVisibility(View.INVISIBLE);
            update();
            return;
        }
        // 这里定义了一个空字符串,用于放入各个状态下的提醒文字
        Resources res = getContext().getResources();
        CharSequence str = "";
        if (newMode == PAUSE) {
            str = res.getText(R.string.mode_pause);
        }
        if (newMode == READY) {
            str = res.getText(R.string.mode_ready);
        }
        if (newMode == LOSE) {
            str = res.getString(R.string.mode_lose_prefix) + mScore
                    + res.getString(R.string.mode_lose_suffix);
        }
        if (newMode == QUIT){
            str = res.getText(R.string.mode_quit);
        }
        mStatusText.setText(str);
        mStatusText.setVisibility(View.VISIBLE);
    }

    //记录坐标位置
    private class Coordinate {
        public int x;
        public int y;
        public Coordinate(int newX, int newY) {
            x = newX;
            y = newY;
        }
        //触碰检测,看蛇是否吃到苹果
        public boolean equals(Coordinate other) {
            if (x == other.x && y == other.y) {
                return true;
            }
            return false;
        }
        // 这个方法没研究过起什么作用,我注释掉对程序的运行没有影响
        @Override
        public String toString() {
            return "Coordinate: [" + x + "," + y + "]";
        }
    }
}


        以上是自定义View的实现,实现了绘制游戏界面的主逻辑。将SnakeView作为一个UI控件插入到该界面的布局中。下面开启活动,显示界面。

//  GameActivity.java
package com.example.wang.game;
public class GameActivity extends Activity implements OnClickListener{
    private SharedPreferences saved;
    private static String ICICLE_KEY = "snake-view";    //  个人认为这个变量就是一个中间值,在该类的最后一个方法中传入该变量,完成操作。
    private SnakeView mSnakeView;
    private ImageButton change_stop,change_start,change_quit;
    private ImageButton mLeft;
    private ImageButton mRight;
    private ImageButton mUp;
    private ImageButton mDown;
    private static final int UP = 1;
    private static final int DOWN = 2;
    private static final int RIGHT = 3;
    private static final int LEFT = 4;

    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_game);
        mSnakeView = (SnakeView) findViewById(R.id.snake);  //给自定义View实例化,把这个布局当一个UI控件一样插入进来
        mSnakeView.setTextView((TextView) findViewById(R.id.text_show));

        change_stop = (ImageButton) findViewById(R.id.game_stop);
        change_start = (ImageButton) findViewById(R.id.game_start);
        change_quit = (ImageButton) findViewById(R.id.game_quit);

        mLeft = (ImageButton) findViewById(R.id.left);
        mRight = (ImageButton) findViewById(R.id.right);
        mUp = (ImageButton) findViewById(R.id.up);
        mDown = (ImageButton) findViewById(R.id.down);

        change_start = (ImageButton) findViewById(R.id.game_start);
        change_stop = (ImageButton) findViewById(R.id.game_stop);
        change_quit = (ImageButton) findViewById(R.id.game_quit);

        saved = PreferenceManager.getDefaultSharedPreferences(this);
        boolean playMusic = saved.getBoolean("ifon" ,true);     // 获取背景音乐开关的状态变量,在设置开关界面存储,在这里读取
        if(playMusic) {    // 如果设置背景音乐打开,则开启服务,播放音乐
            Intent intent_service = new Intent(GameActivity.this, MusicService.class);
            startService(intent_service);
        }
        SnakeView.mMoveDelay=saved.getInt("nandu",500);     // 获取当前设置的代表游戏难度的变量,在难度设置界面保存,在这里读取

        // 判断是否有保存数据,如果数据为空就准备重新开始游戏
        if (savedInstanceState == null) {
            mSnakeView.setMode(SnakeView.READY);
        } else {
            // 暂停后的恢复
            Bundle map = savedInstanceState.getBundle(ICICLE_KEY);
            if (map != null) {
                mSnakeView.restoreState(map);
            } else {
                mSnakeView.setMode(SnakeView.PAUSE);
            }
        }
        mDown.setOnClickListener(this);
        mUp.setOnClickListener(this);
        mRight.setOnClickListener(this);
        mLeft.setOnClickListener(this);
        change_start.setOnClickListener(this);
        change_stop.setOnClickListener(this);
        change_quit.setOnClickListener(this);
    }
    @Override
    public void onDestroy(){
        super.onDestroy();
        saved = PreferenceManager.getDefaultSharedPreferences(this);
        boolean playMusic = saved.getBoolean("ifon" ,true);
        if(playMusic) {
            Intent intent_service = new Intent(GameActivity.this, MusicService.class);
            stopService(intent_service);
        }

    }
    // 给开始,暂停,退出,上下左右按钮设置监听。根据当前的状态来决定界面的更新操作
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.game_start:
                 // 重新开始游戏,这里延时变量必须初始化,不然每次游戏重新开始之后,蛇的运动速度不会初始化
                if ( mSnakeView.mMode == SnakeView.READY || mSnakeView.mMode == SnakeView.LOSE) {
                    SnakeView.mMoveDelay=saved.getInt("nandu",500);
                    mSnakeView.initNewGame();
                    mSnakeView.setMode(SnakeView.RUNNING);
                    mSnakeView.update();
                }
                // 暂停后开始游戏,继续暂停前的界面
                if ( mSnakeView.mMode == SnakeView.PAUSE) {
                    mSnakeView.setMode(SnakeView.RUNNING);
                    mSnakeView.update();
                }
                break;
            case R.id.game_stop:    //  暂停
                if(mSnakeView.mMode == SnakeView.RUNNING) {
                    mSnakeView.setMode(SnakeView.PAUSE);
                }
                break;
            case R.id.game_quit:   //  退出,返回菜单界面
                mSnakeView.setMode(SnakeView.QUIT);
                finish();
                break;
            // 使界面上的方向按钮起作用
            case R.id.left:
                if (SnakeView.mDirection != RIGHT) {
                    SnakeView.mNextDirection = LEFT;
                }
                break;
            case R.id.right:
                if (SnakeView.mDirection != LEFT) {
                    SnakeView.mNextDirection = RIGHT;
                }
                break;
            case R.id.up:
                if (SnakeView.mDirection != DOWN) {
                    SnakeView.mNextDirection = UP;
                }
                break;
            case R.id.down:
                if (SnakeView.mDirection != UP) {
                    SnakeView.mNextDirection = DOWN;
                }
                break;
            default:
                break;
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        mSnakeView.setMode(SnakeView.PAUSE);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        //保存游戏状态
        outState.putBundle(ICICLE_KEY, mSnakeView.saveState());
    }
}


        下面是游戏效果图,运行界面和暂停界面。我把逻辑流程图也贴出来,有什么问题大家可以留言,多多交流!

Android游戏——贪吃蛇开发实录(改进后的源码和详解)_第6张图片    Android游戏——贪吃蛇开发实录(改进后的源码和详解)_第7张图片   

Android游戏——贪吃蛇开发实录(改进后的源码和详解)_第8张图片



很多大兄弟都问我要源码。 以下自取哈,觉得有用记得关注下我的博客和GitHub
CSDN下载地址: http://download.csdn.net/detail/kuaiguixs/9588233
GitHub: https://github.com/AndroidWJC/SnakeGame
这个小游戏是一年多以前写的了,近期打算整理下。再加入应用退出后的暂停保存和排行榜功能,敬请期待。



你可能感兴趣的:(android开发笔记)