關(guān)于我
我是IsCoding,11年開始做 Android 開發(fā) 已經(jīng)做了7年
在創(chuàng)業(yè)公司負(fù)責(zé)過技術(shù),拿到過融資。
想做一些事。這件事我想了很久研究了很久,現(xiàn)在時(shí)機(jī)成熟了。
QQ群號(hào) 121915371
QQ 號(hào) 1400100300 (個(gè)人QQ 建議加群咨詢)
引言
我相信大部分人都應(yīng)該玩過貪吃蛇。具體規(guī)則大家都懂,這里不一一介紹規(guī)則。
在網(wǎng)上有很多貪吃蛇的代碼,為什么你還要看我的代碼。看了我的方式有什么好處。
這里我跟你保證,只要你有點(diǎn)Java基礎(chǔ),看半個(gè)小時(shí),自己就能獨(dú)立寫出來一個(gè)完整的貪吃蛇游戲。而且能真的理解為什么這么寫程序。
如果有不懂的地方可以加上面的群咨詢
分析
下面來講講如何寫一個(gè)程序。
分析界面,界面元素有三個(gè),一個(gè)是背景格子,一個(gè)是蛇,一個(gè)是隨機(jī)產(chǎn)生的點(diǎn)。
那么我們就可以定義三個(gè)對(duì)象
GridBean 格子對(duì)象 就是游戲的背景表格。
PointBean 點(diǎn)對(duì)象,就是隨機(jī)產(chǎn)生的點(diǎn)
SnakeBean 蛇的對(duì)象
整個(gè)游戲用戶看到的就是這幾個(gè)東西,我們要做的就是把這些東西畫到頁(yè)面上。
操作,游戲過程主要的操作就是上下左右。
我們可以用按鈕實(shí)現(xiàn),在頁(yè)面上添加四個(gè)按鈕,供用戶點(diǎn)擊
也可以用手勢(shì)實(shí)現(xiàn),用戶在頁(yè)面上滑動(dòng)來改變方向。
這幾個(gè)功能實(shí)現(xiàn)了,一個(gè)小游戲就基本完成了,我們還可以添加暫停,開始,添加速度等等操作。
實(shí)現(xiàn)
接下來我們一步一步分析,把游戲?qū)崿F(xiàn)起來
第一步
我們要定義三個(gè)類,就是上面我們說的三個(gè)類
先寫第一個(gè)PointBean用來表示頁(yè)面上的一個(gè)格子的位置
我們用想x,y來確定格子的位置,值是從0開始計(jì)算的。
比如0,0代表的是左上角第一個(gè)格子,2,1代表第三行,第二個(gè)格子,為什么從0開始,是因?yàn)閿?shù)組下標(biāo)從0開始,具體這里不解釋了。
代碼非常簡(jiǎn)單,這里直接給出來了
public class PointBean {
private int x;
private int y;
public PointBean(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
}
第二個(gè)類SnakeBean 這個(gè)類就很有意思了,其實(shí)蛇就是由一個(gè)個(gè)連續(xù)的小格子組成的,SnakeBean 里面就只有一個(gè)PointBean的List 代碼如下
public class SnakeBean {
private List<PointBean> snake = new LinkedList<PointBean>();
public List<PointBean> getSnake() {
return snake;
}
public void setSnake(List<PointBean> snake) {
this.snake = snake;
}
}
其實(shí)這里我們只要在View里面定這個(gè)List 也可以去做,但是為了封裝需求,我們還是定義了一個(gè)Bean
接下來我們定義的是格子類
格子類主要包括幾個(gè)東西,間距這里方便起見所有間距設(shè)置一樣,也可以為每個(gè)方向設(shè)置一個(gè)間距,第二個(gè)就是格子的總長(zhǎng)度,還有每個(gè)格子的長(zhǎng)度,還有格子的數(shù)量
這里以1080*1920 手機(jī)為例子,如果需要?jiǎng)e的手機(jī)直接把這個(gè)值賦值為獲取手機(jī)值就行
public class GridBean {
private int height = 1920; //手機(jī)高
private int width = 1080; //手機(jī)寬
private int offset = 90 ;//偏移量,就是間距 上 左 右 間距一樣
private int gridSize = 30;//每行格子的數(shù)量
private int lineLength;//線的長(zhǎng)度
private int gridWidth ;//格子寬
public GridBean() {
lineLength = width - offset * 2;
gridWidth = lineLength / gridSize;// 格子數(shù)量
}
public int getOffset() {
return offset;
}
public void setOffset(int offset) {
this.offset = offset;
}
public int getGridSize() {
return gridSize;
}
public void setGridSize(int gridSize) {
this.gridSize = gridSize;
}
public int getLineLength() {
return lineLength;
}
public void setLineLength(int lineLength) {
this.lineLength = lineLength;
}
public int getGridWidth() {
return gridWidth;
}
public void setGridWidth(int gridWidth) {
this.gridWidth = gridWidth;
}
}
第二步,把格子和蛇畫到頁(yè)面上
首先你應(yīng)該了解自定義View,可以在自定義View中繪制直線,長(zhǎng)方形。
如果不了解請(qǐng)看這篇文章
http://www.lxweimin.com/p/2c3eb5924389
自定義View這里不需要多復(fù)雜的技術(shù),只用最簡(jiǎn)單部分就夠了
接下面我們一步步把這個(gè)自定義布局完成
首先初始化這個(gè)View編寫一個(gè)init方法
創(chuàng)建格子對(duì)象
初始化蛇對(duì)象
private void init() {
gridBean = new GridBean();//創(chuàng)建格子對(duì)象,畫格子時(shí)候使用
snakeBean = new SnakeBean();//創(chuàng)建一個(gè)蛇對(duì)象。這時(shí)候蛇對(duì)象是空的,我們需要初始化一個(gè)值
PointBean pointBean = new PointBean(gridBean.getGridSize()/2,gridBean.getGridSize()/2);
snakeBean.getSnake().add(pointBean);//定義一個(gè)中心點(diǎn) ,添加到蛇身上
}
然后我們根據(jù)格子對(duì)象在頁(yè)面上畫直線就好了
//畫豎線
for (int i = 0; i <= gridBean.getGridSize(); i++) {
int startX = gridBean.getOffset() + gridBean.getGridWidth() * i;
int stopX = startX;
int startY = gridBean.getOffset();//+gridBean.getGridWidth() * i
int stopY = startY + gridBean.getLineLength();//
canvas.drawLine(startX, startY, stopX, stopY, paint);
}
這里給出畫豎線的方法,畫橫線跟豎線基本一樣
然后我們畫蛇,其實(shí)就是根據(jù)點(diǎn)畫一個(gè)方塊
List<PointBean> snake = snakeBean.getSnake();
for (PointBean point : snake) {
int startX = gridBean.getOffset() + gridBean.getGridWidth() * point.getX();
int stopX = startX + gridBean.getGridWidth();
int startY = gridBean.getOffset() + gridBean.getGridWidth() * point.getY();
int stopY = startY + +gridBean.getGridWidth();
canvas.drawRect(startX, startY, stopX, stopY, paint);
}
運(yùn)行程序,貪吃蛇這個(gè)項(xiàng)目就做出來了,蛇會(huì)背景都繪制完成了。
這里給出完整代碼,直接運(yùn)行就能看到效果了。
public class GameView extends View {
private Paint paint = new Paint();
private GridBean gridBean;
private SnakeBean snakeBean;
public GameView(Context context) {
super(context);
init();
}
public GameView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
gridBean = new GridBean();//創(chuàng)建格子對(duì)象,畫格子時(shí)候使用
snakeBean = new SnakeBean();//創(chuàng)建一個(gè)蛇對(duì)象。這時(shí)候蛇對(duì)象是空的,我們需要初始化一個(gè)值
PointBean pointBean = new PointBean(gridBean.getGridSize()/2,gridBean.getGridSize()/2);
snakeBean.getSnake().add(pointBean);//定義一個(gè)中心點(diǎn) ,添加到蛇身上
}
@Override
protected void onDraw(Canvas canvas) {
if (gridBean != null) {
paint.setColor(Color.RED);
drawGrid(canvas);
}
if (snakeBean != null) {
paint.setColor(Color.GREEN);
drawSnake(canvas);
}
}
private void drawSnake(Canvas canvas) {
List<PointBean> snake = snakeBean.getSnake();
for (PointBean point : snake) {
int startX = gridBean.getOffset() + gridBean.getGridWidth() * point.getX();
int stopX = startX + gridBean.getGridWidth();
int startY = gridBean.getOffset() + gridBean.getGridWidth() * point.getY();
int stopY = startY + +gridBean.getGridWidth();
canvas.drawRect(startX, startY, stopX, stopY, paint);
}
}
private void drawGrid(Canvas canvas) {
//畫豎線
for (int i = 0; i <= gridBean.getGridSize(); i++) {
int startX = gridBean.getOffset() + gridBean.getGridWidth() * i;
int stopX = startX;
int startY = gridBean.getOffset();//+gridBean.getGridWidth() * i
int stopY = startY + gridBean.getLineLength();//
canvas.drawLine(startX, startY, stopX, stopY, paint);
}
//畫橫線
for (int i = 0; i <= gridBean.getGridSize(); i++) {
int startX = gridBean.getOffset();//+gridBean.getGridWidth() * i
int stopX = startX + gridBean.getLineLength();
int startY = gridBean.getOffset() + gridBean.getGridWidth() * i;
int stopY = startY;
canvas.drawLine(startX, startY, stopX, stopY, paint);
}
}
}
好了今天課程講完了。
開個(gè)玩笑,寫了這么半天我們只實(shí)現(xiàn)了一個(gè)功能就是,繪制頁(yè)面。
接下來我們要實(shí)現(xiàn)的功能就是如何讓蛇動(dòng)起來,這樣才是一個(gè)游戲嘛
那么如何讓蛇動(dòng)起來呢,其實(shí)很簡(jiǎn)單,就是不停的繪制頁(yè)面,繪制蛇在不同位置的頁(yè)面,這樣就可以繪制出蛇動(dòng)的效果了。
如何不停的繪制頁(yè)面呢,這里只要啟動(dòng)一個(gè)線程,不停的發(fā)送消息就可以了,告訴頁(yè)面要重新繪制。
這里分析下控制流程
貪吃蛇游戲默認(rèn)有個(gè)移動(dòng)方向,比如上,用戶可以操作上下左右改變方向。
所以這里定義一個(gè)操作類,用于記錄用戶的操作。
上下左右是默認(rèn)值,可以用枚舉實(shí)現(xiàn),更適合
public enum Control {
UP,DOWN,LEFT,RIGHT
}
這里簡(jiǎn)單分析下用戶操作時(shí)候發(fā)生的變化。
蛇默認(rèn)會(huì)在收到消息之后往上走一個(gè),如果用戶做了一個(gè)轉(zhuǎn)左的操作,那么蛇的下一步就是往左走一個(gè),實(shí)際上蛇每次都是按一個(gè)固定方向行走的,那么我們寫代碼的時(shí)候只要實(shí)現(xiàn)一個(gè)方法是根據(jù)下一步方向來繪制蛇就行,而用戶的操作僅僅只是告訴View下一步的方向是什么。
這樣做的一個(gè)好處就是,把操作分割開了,便于實(shí)現(xiàn),用戶的操作只是告訴了View去改變方向,而蛇每次的真正移動(dòng)只是根據(jù)線程消息移動(dòng),這兩個(gè)地方就不會(huì)相互影響了。
好了現(xiàn)在我們來實(shí)現(xiàn)這個(gè)步驟
我們定義一個(gè)Control對(duì)象記錄用戶下一步要走的方向。
我們刷新頁(yè)面是根據(jù)下一步走的方向去改變蛇的位置的。
private Control control = Control.UP;
然后我們看看刷新頁(yè)面時(shí)候我們要做什么。
刷新頁(yè)面說明蛇是要往前走一個(gè),根據(jù)下一步方向走。
我們?nèi)绾螌?shí)現(xiàn)蛇走下一步呢
其實(shí)這個(gè)規(guī)則很簡(jiǎn)單就是在蛇的control方向添加一個(gè)點(diǎn),然后刪除最后一個(gè)點(diǎn),這樣蛇就根據(jù)方向變化了
然后刷新頁(yè)面就ok了
public void refreshView(boolean isAdd){
List<PointBean> pointList = snakeBean.getSnake();
PointBean point = pointList.get(0);
PointBean pointNew = null;
if (control == Control.LEFT) {
pointNew = new PointBean(point.getX() - 1, point.getY());
} else if (control == Control.RIGHT) {
pointNew = new PointBean(point.getX() + 1, point.getY());
} else if (control == Control.UP) {
pointNew = new PointBean(point.getX(), point.getY() - 1);
} else if (control == Control.DOWN) {
pointNew = new PointBean(point.getX(), point.getY() + 1);
}
if (pointNew != null) {
pointList.add(0, pointNew);
if(!isAdd){
pointList.remove(pointList.get(pointList.size() - 1));
}
}
invalidate();
//此處只是刷新頁(yè)面
//刷新頁(yè)面會(huì)重新繪制
}
這個(gè)就是刷新頁(yè)面的方法了
里面的規(guī)則就是根據(jù)位置確定下一個(gè)頂點(diǎn)的位置,然后把這個(gè)新的頂點(diǎn)添加到蛇的頭部,刪除蛇的尾部就可以了。邏輯是不是很清晰?
這樣蛇就從上一個(gè)位置移動(dòng)到下一個(gè)位置了。
現(xiàn)在就開啟線程不停的調(diào)用刷新方法蛇就可以動(dòng)起來了。
下面給出開啟線程的代碼
public class MainActivity extends AppCompatActivity {
public static final int WHAT_REFRESH = 200;
private GameView gameView;
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if(WHAT_REFRESH == msg.what){
gameView.refreshView(false);
sendControlMessage();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
gameView = new GameView(this);
setContentView(gameView);
sendControlMessage();
}
private void sendControlMessage(){
handler.postDelayed(new Runnable() {
@Override
public void run() {
handler.sendEmptyMessage(WHAT_REFRESH);
}
},300);
}
}
這段代碼只是不停的調(diào)用線程每300 毫秒告訴一次View要刷新頁(yè)面
此時(shí)View的完整代碼是這個(gè)
public class GameView extends View {
private Paint paint = new Paint();
private GridBean gridBean;
private SnakeBean snakeBean;
private Control control = Control.UP;
public GameView(Context context) {
super(context);
init();
}
public GameView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
gridBean = new GridBean();//創(chuàng)建格子對(duì)象,畫格子時(shí)候使用
snakeBean = new SnakeBean();//創(chuàng)建一個(gè)蛇對(duì)象。這時(shí)候蛇對(duì)象是空的,我們需要初始化一個(gè)值
PointBean pointBean = new PointBean(gridBean.getGridSize()/2,gridBean.getGridSize()/2);
snakeBean.getSnake().add(pointBean);//定義一個(gè)中心點(diǎn) ,添加到蛇身上
}
@Override
protected void onDraw(Canvas canvas) {
if (gridBean != null) {
paint.setColor(Color.RED);
drawGrid(canvas);
}
if (snakeBean != null) {
paint.setColor(Color.GREEN);
drawSnake(canvas);
}
}
private void drawSnake(Canvas canvas) {
List<PointBean> snake = snakeBean.getSnake();
for (PointBean point : snake) {
int startX = gridBean.getOffset() + gridBean.getGridWidth() * point.getX();
int stopX = startX + gridBean.getGridWidth();
int startY = gridBean.getOffset() + gridBean.getGridWidth() * point.getY();
int stopY = startY + +gridBean.getGridWidth();
canvas.drawRect(startX, startY, stopX, stopY, paint);
}
}
private void drawGrid(Canvas canvas) {
//畫豎線
for (int i = 0; i <= gridBean.getGridSize(); i++) {
int startX = gridBean.getOffset() + gridBean.getGridWidth() * i;
int stopX = startX;
int startY = gridBean.getOffset();//+gridBean.getGridWidth() * i
int stopY = startY + gridBean.getLineLength();//
canvas.drawLine(startX, startY, stopX, stopY, paint);
}
//畫橫線
for (int i = 0; i <= gridBean.getGridSize(); i++) {
int startX = gridBean.getOffset();//+gridBean.getGridWidth() * i
int stopX = startX + gridBean.getLineLength();
int startY = gridBean.getOffset() + gridBean.getGridWidth() * i;
int stopY = startY;
canvas.drawLine(startX, startY, stopX, stopY, paint);
}
}
public void refreshView(boolean isAdd){
List<PointBean> pointList = snakeBean.getSnake();
PointBean point = pointList.get(0);
PointBean pointNew = null;
if (control == Control.LEFT) {
pointNew = new PointBean(point.getX() - 1, point.getY());
} else if (control == Control.RIGHT) {
pointNew = new PointBean(point.getX() + 1, point.getY());
} else if (control == Control.UP) {
pointNew = new PointBean(point.getX(), point.getY() - 1);
} else if (control == Control.DOWN) {
pointNew = new PointBean(point.getX(), point.getY() + 1);
}
if (pointNew != null) {
pointList.add(0, pointNew);
if(!isAdd){
pointList.remove(pointList.get(pointList.size() - 1));
}
}
invalidate();
//此處只是刷新頁(yè)面
//刷新頁(yè)面會(huì)重新繪制
}
}
運(yùn)行的小伙伴有沒有發(fā)現(xiàn),蛇跑出格子了,沒錯(cuò)這里沒有加任何的判斷,蛇會(huì)一直往上走。
這里我們要做的就是添加一個(gè)判斷結(jié)束的方法
這里只簡(jiǎn)單寫一個(gè)判斷蛇越界算輸?shù)姆椒ǎ』锇榭梢宰约和晟祈?xiàng)目做更多的判斷輸贏
private boolean isFailed( PointBean point){
if (point.getX() == 0 && control == Control.UP ) {
return true;
} else if ( point.getY() == 0 && control == Control.LEFT) {
return true;
} else if (point.getX() == gridBean.getGridSize() - 1 && control == Control.DOWN ) {
return true;
} else if (point.getY() == gridBean.getGridSize() - 1 && control == Control.RIGHT ) {
return true;
}
return false;
}
下面我們要為游戲添加手勢(shì)了喲
這個(gè)地方其實(shí)網(wǎng)上很多地方都有,就是獲取滑動(dòng)方向的,
獲取完方向如何使用呢。因?yàn)橹坝昧藗€(gè)小技巧,這里其實(shí)做的就是改變之前control的值而已,沒有其他任何操作,直接上代碼
int x;
int y;
@Override
public boolean onTouchEvent(MotionEvent event) { //通過手勢(shì)來改變蛇體運(yùn)動(dòng)方向
int action = event.getAction() & MotionEvent.ACTION_MASK;
LogUtil.e("x =" + x + " y = " + y + " action() = " + action);
// TODO Auto-generated method stub
if (action == KeyEvent.ACTION_DOWN) {
//每次Down事件,都置為Null
x = (int) (event.getX());
y = (int) (event.getY());
}
if (action== KeyEvent.ACTION_UP) {
//每次Down事件,都置為Null
int x = (int) (event.getX());
int y = (int) (event.getY());
// 新建一個(gè)滑動(dòng)方向,
Control control = null ;
// 滑動(dòng)方向x軸大說明滑動(dòng)方向?yàn)?左右
if (Math.abs(x - this.x) > Math.abs(y - this.y)) {
if (x > this.x) {
control = Control.RIGHT;
LogUtil.i("用戶右劃了");
}
if (x < this.x) {
control = Control.LEFT;
LogUtil.i("用戶左劃了");
}
}else{
if (y < this.y) {
control = Control.UP;
LogUtil.i("用戶上劃了");
}
if (y > this.y) {
control = Control.DOWN;
LogUtil.i("用戶下劃了");
}
}
if (this.control == Control.UP || this.control == Control.DOWN) {
if(control==Control.UP ||Control.UP==Control.DOWN ){
LogUtil.i("已經(jīng)是上下移動(dòng)了,滑動(dòng)無效");
}else{
this.control = control;
}
} else if (this.control == Control.LEFT || this.control == Control.RIGHT) {
if(control==Control.LEFT ||Control.UP==Control.RIGHT ){
LogUtil.i("已經(jīng)是左右移動(dòng)了,滑動(dòng)無效");
}else{
this.control = control;
}
}
}
//Log.e(TAG, "after adjust mSnakeDirection = " + mSnakeDirection);
return super.onTouchEvent(event);
}
代碼有點(diǎn)多
簡(jiǎn)單分析下,
根據(jù)用戶手指落下 和抬起位置的差值,計(jì)算用戶是哪個(gè)方向滑動(dòng)了。
如果用戶滑滑動(dòng)方向是左 判斷用戶是否可以左滑,比如用戶之前是往右是不允許左劃的 ,滑動(dòng)也不會(huì)有任何處理。這里就是幾個(gè)簡(jiǎn)單判斷,實(shí)際使用的就是 把Control的方向改為用戶滑動(dòng)成功的方向。
這樣整個(gè)程序基本就做完了。
剩下的就是優(yōu)化項(xiàng)目了,第一個(gè)就是現(xiàn)在程序的蛇只有一個(gè)點(diǎn),我們希望蛇是不斷增加的
那么我們可以設(shè)置一個(gè)方法讓蛇 在一段時(shí)間就增加一個(gè)格子 也可以隨機(jī)生成一個(gè) 點(diǎn),然后蛇碰到這個(gè)點(diǎn)就增加一個(gè)一個(gè)點(diǎn),第二個(gè)就是速度問題,我們希望隨著時(shí)間的推移蛇越來越快
這里我先設(shè)置個(gè)計(jì)步器,count 每20步 我就讓蛇添加1格
https://github.com/zujianhua/Snake
完整源碼發(fā)布到Github上,如有問題請(qǐng)指正。
歡迎加上面的群來交流