这学期的一门课要求分组完成一个课题,我们小组选题是基于安卓的贪吃蛇游戏,在网上确实源码满天飞,按照一贯的分工模式,可怜的我作为组长只有一个人支撑起整个开发过程。
这个游戏现在功能已经基本实现,不过页面急待美化,代码更需优化。里面主要实现的功能是:不同速度下的游戏体验,两人联机效果的游戏体验,最高分的展示。
基本原理是利用自定义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
5.SnakeView中重写onDraw()
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawBg(canvas,mBgPaint);
drawFood(canvas,mFoodPaint);
drawSnake(canvas,mSnakePaint);
}
6.在Activity中载入SnakeView
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
snakeView=new SnakeView(this,mhandler);
setContentView(snakeView);
}
7.在Activity开启线程不断刷新SnakeView,并接收和处理游戏结果
private void startThread(){
new Thread(new Runnable() {
@Override
public void run() {
while (!isThreadStop) {
Message msg = mhandler.obtainMessage();
msg.arg1 = MainActivity.MSG_ONE;
mhandler.sendMessage(msg);
try {
Thread.sleep(REFRESHINTERVAL); //休眠一段时间后再发送消息刷新界面
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
> 说明:这里设置一个结束线程中循环的标志位isThreadStop(默认为false),利用handler发送消息:MSG_ONE(arg1)会继续
> snakeView.invalidate()来刷新view,直到handler接收到MSG_TWO(arg1)才会使isThreadStop为true,而后结束循环释放线程。
private Handler mhandler=new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.arg1==MainActivity.MSG_ONE){
if (snakeView!=null){
snakeView.invalidate();
}
}else if(msg.arg1==MainActivity.MSG_TWO){//游戏结束
// isThreadStop=true;
String count=msg.obj.toString();//获取传过来的分数
NetreqUtils.getData(MainActivity.BASE_URL+" snakescore.php?opstr="insertScore&score="+count+"&pat="+myPatt+"&name="+player_name,null,"");//发起http请求,提交分数至网络" new="" alertdialog.builder(basegame.this).settitle("game="" over!").setmessage("得分:"+count+"分"+"\n请再接再厉!").setnegativebutton("返回",="" dialoginterface.onclicklistener()="" @override="" public="" onclick(dialoginterface="" dialog,="" int="" which)="" finish();="" 退出="" }).setpositivebutton("再玩一次",="" isthreadstop="false;" snakeview.initall();="" startthread();="" 重新开始游戏="" }).show();="" };="" <="" code="">
双人联机玩法
玩法上没多少创意:
1.一个玩家选蛇,另一个玩家选食物建立连接
2.玩家控制蛇(手势滑动)去吃食物,玩家控制食物(双击移动)躲避蛇或者诱导自撞。
3.蛇吃到食物或者食物撞到边界则蛇win,蛇自撞或撞到边界则食物win。
由于这个选题也有其他几个小组在做,所以为了体现与众不同的我们,于是加了这个模块,这个在网上可没有多少资料参考,作为组长亚历山大,苦苦思索后决定用socket来实现数据交互,虽说这个功能已经实现,可体验是真的差说到底还是自己太菜,不多说了,主要总结下实现思路,后期会继续优化。
实现原理
其实基本原理就是,让蛇作为服务端(ServerSocket),而食物作为客户端(ClientSocket),移动过程中双方通过互相发送坐标来实现画面同步,交互方式如图:
![贪吃蛇的简单实现_第1张图片](http://img.e-com-net.com/image/info8/c5df2de805cf471aa82f5d9ef6727550.jpg)
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的封装,所以我应该多去了解相关的通讯原理来优化连接和数据传输方式。还有就是我的代码太过冗余(其实很多可以重用),现在深有体会(自己看的都头疼),从今以后的每句代码,编码规范将作为个人习惯来看待!
你可能感兴趣的:(学生)