贪吃蛇的简单实现

背景

这学期的一门课要求分组完成一个课题,我们小组选题是基于安卓的贪吃蛇游戏,在网上确实源码满天飞,按照一贯的分工模式,可怜的我作为组长只有一个人支撑起整个开发过程。

简介

这个游戏现在功能已经基本实现,不过页面急待美化,代码更需优化。里面主要实现的功能是:不同速度下的游戏体验,两人联机效果的游戏体验,最高分的展示。

基本功能实现

基本原理是利用自定义view的onDraw()不断更新画布中的绘制对象来实现游戏画面,重写触摸事件来进行控制,根据坐标(point(x,y))进行一些判断。

实现过程

1.创建一个SnakeView继承View

public class SnakeView extends View {
	public SnakeView(Context context, Handler handler) {
    		super(context);
    		mhandler=handler;
	}
}
Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    mHeight=h;
    mWidth=w;
}

2.初始画蛇体、食物、游戏背景
(1)创建蛇体

private int BOXWIDTH =0; //食物的边长,蛇体的宽度
private ArrayList mSnakeList;  //蛇体可以看做是很多食物组成的
private Paint mSnakePaint;  //用于画蛇的画笔
private int mSnakeDirection = 0; //蛇体运动的方向
private final int UP = 1,DOWN = 2,LEFT = 3,RIGHT =4;
private void  initSnake(){
    mSnakeList = new ArrayList();
    mSnakePaint = new Paint();
    mSnakePaint.setColor(Color.RED);
    mSnakePaint.setStyle(Paint.Style.FILL_AND_STROKE);//设置画蛇画笔的属性
    mSnakeList.add(new Point(600,600));
    mSnakeList.add(new Point(600,600+BOXWIDTH)); //初始化一条丑陋的蛇
    mSnakeDirection = RIGHT;
}

(2)创建食物

private int BOXWIDTH =0; //食物的边长,蛇体的宽度
private Random mRandom; //用于产生随机数
private Point foodPosition; //食物的位置
private Paint mFoodPaint;//食物画笔
private boolean foodIsEaten; //食物是否已经被吃掉
private void initFood(){
   // foodIsEaten=false;
    foodPosition=new Point();
    mFoodPaint=new Paint();
    mFoodPaint.setColor(Color.CYAN);
    mFoodPaint.setStyle(Paint.Style.FILL);
}

(3)初始化背景

private int mWidth;  //view的宽
private int mHeight; //View的高
private static int sYOffset = 0,sXOffset = 0  ; // X坐标和Y坐标的偏移量,可以修改来缩小游戏范围
private Paint mBgPaint;//游戏背景画笔
private void initBg(){
    mBgPaint = new Paint();
    Paint paint = new Paint();
    paint.setColor(Color.WHITE);
    mWidth=getWidth();
    mHeight=getHeight();
}

//画背景 这里通过sXOffset, sYOffset可以实现对蛇活动区域的限制
private void drawBg(Canvas canvas, Paint paint) {
    canvas.drawColor(Color.WHITE);
    Rect rect = new Rect(sXOffset, sYOffset, mWidth - sXOffset, mHeight - sYOffset);
    canvas.drawRect(rect, paint);
}

3.绘制蛇(移动)、(next)食物的方法

(1)蛇的绘制
private void drawSnake(Canvas canvas, Paint paint) {
    for(int i = 0 ; i < mSnakeList.size() ; i++ ) {
        Point point = mSnakeList.get(i);
        Rect rect = new Rect(point.x , point.y , point.x + BOXWIDTH , point.y + BOXWIDTH);
        canvas.drawRect(rect, paint);
    }
    //蛇移动,更新list为下一次刷新做准备
    snakeMove(mSnakeList, mSnakeDirection);
    if(isFoodEaten()) {  //如果吃了食物,长度加1
        foodIsEaten = true;
    } else {    //如果没有吃食物,由于前进时加了一个 这里删除尾巴,出现移动的效果
        mSnakeList.remove(mSnakeList.size() - 1);
    }
}
public void snakeMove(ArrayList list , int direction) {
    //Log.e(TAG," snakeMove ArrayList = " + list.toString());
    Point orighead = list.get(0);
    Point newhead = new Point();
    //蛇前进,实现原理就是头加尾减,若吃到食物,头加尾不减
    switch(direction) {
        case UP:
            newhead.x = orighead.x;
            newhead.y = orighead.y  - BOXWIDTH ;
            break;
        case DOWN:
            newhead.x = orighead.x;
            newhead.y = orighead.y  + BOXWIDTH ;
            break;
        case LEFT:
            newhead.x = orighead.x  - BOXWIDTH;
            newhead.y = orighead.y;
            break;
        case RIGHT:
            newhead.x = orighead.x + BOXWIDTH ;
            newhead.y = orighead.y;
            break;
        default:
            break;
    }
    list.add(0, newhead);
    overGame(newhead);//判断游戏是否结束
}
(2)食物的生成
private void drawFood(Canvas canvas, Paint paint) {
    if(foodIsEaten) {  //只在前一个食物被吃掉的情况下才产生食物
        foodPosition.x = mRandom.nextInt(mWidth - 2*sXOffset - BOXWIDTH) + sXOffset ;
        foodPosition.y = mRandom.nextInt(mWidth - 2*sYOffset - BOXWIDTH) + sYOffset ;
        foodIsEaten = false;
    }
    Rect food = new Rect(foodPosition.x , foodPosition.y , foodPosition.x + BOXWIDTH , foodPosition.y + BOXWIDTH);
    canvas.drawRect(food, paint);
}

4.游戏过程规则(吃食物,结束,得分)

//边界判断
private boolean isOutBound(Point point) {
    if(point.x < sXOffset || point.x > mWidth - sXOffset) return true;
    if(point.y < sYOffset || point.y > mHeight - sYOffset) return true;
    return false;
}

//自撞判断
private boolean isBodyCol(ArrayList sankeList){
    Point mhead=sankeList.get(0);
    for (int i=1;i

双人联机玩法

玩法上没多少创意:
1.一个玩家选蛇,另一个玩家选食物建立连接
2.玩家控制蛇(手势滑动)去吃食物,玩家控制食物(双击移动)躲避蛇或者诱导自撞。
3.蛇吃到食物或者食物撞到边界则蛇win,蛇自撞或撞到边界则食物win。

由于这个选题也有其他几个小组在做,所以为了体现与众不同的我们,于是加了这个模块,这个在网上可没有多少资料参考,作为组长亚历山大,苦苦思索后决定用socket来实现数据交互,虽说这个功能已经实现,可体验是真的差说到底还是自己太菜,不多说了,主要总结下实现思路,后期会继续优化。

实现原理

其实基本原理就是,让蛇作为服务端(ServerSocket),而食物作为客户端(ClientSocket),移动过程中双方通过互相发送坐标来实现画面同步,交互方式如图:
贪吃蛇的简单实现_第1张图片

FoodClient的实现

1.创建Socket对象

 Socket  socket = new Socket(wifiUtils.getRouterIP(),9999);
 
   public String getRouterIP(){
        DhcpInfo info=wifiManager.getDhcpInfo();
        String ip = intToRouterIp(info.gateway);
        return ip;
    }//获取本地路由
    private String intToRouterIp(int i) {
        return (i & 0xFF) + "." +
                ((i >> 8) & 0xFF) + "." +
                ((i >> 16) & 0xFF) + "." +
                1;
    }//转换成ip地址(DhcpInfo中的ipAddress是一个int型的变量)

2.创建和开启SocketConnect的线程

1.SocketConnect类继承Thread,在run方法中可以获取socket的输入输出流
public class SocketConnect extends Thread {
    private Handler handler;
    private Socket socket;
    private InputStream is;
    private OutputStream os;
    private boolean isStop=false;
    public SocketConnect(Handler handler,Socket socket){
        setName("SEVERSOCKET_CONNECT_THREAD");
        this.handler=handler;
        this.socket=socket;
    }
    @Override
    public void run() {
        super.run();
        if (socket!=null){
            handler.sendEmptyMessage(MainActivity.MSG_ONE);
            try {
                is=socket.getInputStream();
                os=socket.getOutputStream();
                //获取socket的数据流
                int bytes;
                byte[] buffer = new byte[1024];
                while (!isStop) {
                    //读取socket中的数据
                    bytes = is.read(buffer);
                    if (bytes > 0) {
                        final byte[] data = new byte[bytes];
                        System.arraycopy(buffer, 0, data, 0, bytes);
                        Log.e("cindata",new String(data));
                        Message message = Message.obtain();
                        message.what=MainActivity.MSG_TWO;
                        bundleMeg(new String(data),message);
                        handler.sendMessage(message);
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
           //close();
        }
    }
2.在SocketConnect中定义一个发送数据的方法
  public void sendData(final String msg) {
  //主要利用上述获取到的输出流
        if (os != null) {
            //往Socket里面写数据,需要新开一个线程
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            os.write(msg.getBytes());
                            os.flush();
                            Log.e("socketThread", "发送消息:" + msg);
                            Message message = Message.obtain();
                            message.what = MainActivity.MSG_ONE;//发送成功
                            bundleMeg(msg,message);
                            handler.sendMessage(message);
                        } catch (IOException e) {
                            e.printStackTrace();
                            Message message = Message.obtain();
                            message.what = MainActivity.MSG_ZERO;//发送出错
                            bundleMeg(msg,message);
                            handler.sendMessage(message);
                        }
                    }
                }).start();
        }
    }
    
这里利用bundle捆绑数据并交给message传递
 private void bundleMeg(String data,Message message){
        Bundle bundle = new Bundle();
        bundle.putString("Data", data);
        message.setData(bundle);
    }
    

   当然也不能忘了关闭socket
    public void close() {
        isStop=true;//线程停止标志位
        try {
            if (socket != null)
                socket.close();
            if (is != null)
                is.close();
            if (os != null)
                os.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

3.FoodClient中处理socket消息的handler

1.处理socket连接
             switch (msg.what) {
                    case MainActivity.MSG_ZERO:
                        Toast.makeText(FoodClient.this, "client:连接失败", Toast.LENGTH_SHORT).show();
                        //finish();
                        break;
                    case MainActivity.MSG_ONE:
                       // Toast.makeText(FoodClient.this, "client:连接成功", Toast.LENGTH_SHORT).show();
                        isThreadStop=false;
                        break;
       	   }
 2.处理socket接收的数据
 String MSGjson=msg.getData().getString("Data");
	switch (msg.what) {
	  case MainActivity.MSG_TWO:
	   if (MSGjson.equals("sover")) {
	   //游戏结束,win
           }else{
           //获取snh_x,snh_y
           }
       }

4.FoodClient中处理ClientView的handler

1.ClientView中发送食物的坐标(fd_x,fd_y)

  Message message=Message.obtain();
        if (isOutBound(newhead)||isFoodEaten()){
            message.arg1=MainActivity.MSG_TWO;
            message.obj="you lost game";
        }else {
            message.what=direction;
            message.arg1=MainActivity.MSG_ZERO;
            Bundle bundle=new Bundle();
            bundle.putInt("fd_x", newhead.x);
            bundle.putInt("fd_y", newhead.y);
            message.setData(bundle);
        }
        mhandler.sendMessage(message);
        
2.FoodClient处理ClientView发送过来的消息

	if (msg.arg1==MainActivity.MSG_ONE){
                if (clientSnakeView!=null){
                	clientSnakeView.setSx(sx);
                	clientSnakeView.setSy(sy);//设置蛇的坐标
                	
                    clientSnakeView.invalidate();//更新view
                }
            }else if(msg.arg1==MainActivity.MSG_TWO){
                if (socketConnect!=null)
                socketConnect.sendData("fover");//游戏结束,lose
                } else {
                JSONObject object=new JSONObject();
                int fd_x=msg.getData().getInt("fd_x");
                int fd_y=msg.getData().getInt("fd_y");
                try {
                    object.put("fdx",fd_x);
                    object.put("fdy",fd_y);
                } catch (JSONException e) {
                    e.printStackTrace();
                }//将数据包装成json字符串传输
                Log.e("json", object.toString());
                if (socketConnect!=null)
                    socketConnect.sendData(object.toString());//socket发送数据(fd_x,fd_y)
            }

SnakeServer的实现

1.同样创建ListenSocket继承Thread,然后在构造方法中创建ServerSocket侦听之前9999端口的Socket,run方法中则获得侦听到的socket

public class ListenSocket extends Thread {
    private Handler handler;
    private Socket socket;
    private ServerSocket serverSocket=null;
    private SocketConnect connect;
    public boolean isStop=false;
    private InputStream is;
    private OutputStream os;
    public ListenSocket(Handler handler){
        this.handler=handler;
        try {
            serverSocket=new ServerSocket(9999);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 @Override
    public void run() {
        super.run();
        while (socket==null){
            try {
                if (serverSocket!=null)
                socket=serverSocket.accept();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
     }

2.侦听到socket之后则获取该socket的输入流数据以及它的输出流对象

	if (socket!=null){
            handler.sendEmptyMessage(MainActivity.MSG_ONE);//获取成功
            try {
                is=socket.getInputStream();
                os=socket.getOutputStream();
                //获取socket的数据流
                int bytes;
                byte[] buffer = new byte[1024];
                while (!isStop) {
                    //读取socket中的数据
                    bytes = is.read(buffer);
                    if (bytes > 0) {
                        final byte[] data = new byte[bytes];
                        System.arraycopy(buffer, 0, data, 0, bytes);
                        Log.e("sindata",new String(data));
                        Message message = Message.obtain();
                        message.what=MainActivity.MSG_TWO;
                        bundleMeg(new String(data),message);
                        handler.sendMessage(message);
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }


当然有了输出流对象就可以发送数据了:
 public void sendData(final String msg) {
        if (os != null) {
            //往Socket里面写数据,需要新开一个线程
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        os.write(msg.getBytes());
                        os.flush();
                        Log.e("listenThread", "发送消息:" + msg);
                        Message message = Message.obtain();
                        message.what = MainActivity.MSG_ONE;//发送成功
                        bundleMeg(msg,message);
                        handler.sendMessage(message);
                    } catch (IOException e) {
                        e.printStackTrace();
                        Message message = Message.obtain();
                        message.what = MainActivity.MSG_ZERO;//发送出错
                        bundleMeg(msg,message);
                        handler.sendMessage(message);
                    }
                }
            }).start();
        }
    }
    
    和前面一样,message数据用bundle包裹,
    private void bundleMeg(String data,Message message){
        Bundle bundle = new Bundle();
        bundle.putString("Data", data);
        message.setData(bundle);
    }
    以及关闭的方法:
  public void close() {
        isStop=true;
        try {
            if (socket != null)
                socket.close();
            if (is != null)
                is.close();
            if (os != null)
                os.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

3.SnakeServer中处理socket消息的handler

String MSG=msg.getData().getString("Data");
	if (MSG!=null) {
		switch (msg.what) {
                    case MainActivity.MSG_ZERO:
                       // Toast.makeText(SnakeServer.this, "server:唤醒失败", Toast.LENGTH_SHORT).show();
                        //finish();
                        break;
                    case MainActivity.MSG_ONE:
                       // Toast.makeText(SnakeServer.this, "server:连接成功", Toast.LENGTH_SHORT).show();
                        //MyDialog.dismiss();
                        isThreadStop=false;
                        break;
                    case MainActivity.MSG_TWO:
                    if (MSG.equals("fover")) {
                            isThreadStop = true;
                            //游戏结束了   win
                            }else{
                            //获取fd_x,fd_y
                            }
		}

4.处理ServerVew的handler

1.ServerView中发送蛇头的坐标(snh_x,snh_y)
Message message=Message.obtain();
        //message.what=direction;
        message.arg1=MainActivity.MSG_ZERO;
        Bundle bundle=new Bundle();
        bundle.putInt("snh_x", newhead.x);
        bundle.putInt("snh_y", newhead.y);
        message.setData(bundle);
        mhandler.sendMessage(message);
        
        if(isOutBound(point)||isBodyCol(mSnakeList)){//自撞和撞壁后结束游戏
            Message message=Message.obtain();
            message.arg1=MainActivity.MSG_TWO;
            message.obj="you lost game";
            mhandler.sendMessage(message);
        }
        
 2.SnakeServer中处理
 if (msg.arg1==MainActivity.MSG_ONE){
                if (serverSnakeView!=null){
                serverSnakeView.setFx(fx);
                serverSnakeView.setFy(fy);//设置食物的位置
                    serverSnakeView.invalidate();
                }
            }else if(msg.arg1==MainActivity.MSG_TWO){
                if (listenSocket!=null)
                 listenSocket.sendData("sover");
                isThreadStop=true;
                listenSocket.close();//关闭连接
                }else {
                JSONObject object=new JSONObject();
                int snhead_x=msg.getData().getInt("sknh_x");
                int snhead_y=msg.getData().getInt("sknh_y");
               // int dir=msg.what;
                try {
                    object.put("snx",snhead_x);
                    object.put("sny",snhead_y);
                    //object.put("direc",dir);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                Log.e("sjson", object.toString());
                if (listenSocket!=null)
                    listenSocket.sendData(object.toString());
            }//同样的先转换成json后发送

总结反思

在传统游戏功能的实现上主要采用view的绘制(对画布中绘制的对象进行操作),绘制的频率(游戏中对象的移动速度)由线程中循环的每次的休眠时间来决;而联机的实现则是采用socket互传数据(主要是坐标),控制蛇的一方作为Socket的Server端,控制食物的一方为客户端(需要连接另一方的wifi热点创建socket连接)。虽说功能是可以实现,但发现用户体验这块是真的令人头秃,就比如说双方建立连接后会出现数据丢失,断开连接后再次连接会出现,首先在开始游戏之前必须确保socket连接没有问题,然后就是断开连接应该保存当前状态数据,待恢复连接时载入。Socket是对TCP/IP的封装,所以我应该多去了解相关的通讯原理来优化连接和数据传输方式。还有就是我的代码太过冗余(其实很多可以重用),现在深有体会(自己看的都头疼),从今以后的每句代码,编码规范将作为个人习惯来看待!

你可能感兴趣的:(学生)