100行代码教你实现贪吃蛇小游戏
最近项目中内置了一些比如贪吃蛇,俄罗斯方块,井字棋等小游戏. 这里逐一将实现步骤分享出来供大家学习. 如有不足或错误,请在评论去指正
因为只是个示例, 界面可能并不好看, 这些你们可以使用更好看的资源来替换方格,最终实现效果如下:
其实这些小游戏的实现并不复杂,只要理清楚思路, 一步一步构建下去, 就会发现其实原理很简单.只是我们想复杂了. 完整Demo地址: 贪吃蛇游戏Demo. 自定义视图的源码可以直接看这里. 贪吃蛇游戏视图
实现步骤及说明如下:
1. 自定义游戏界面
游戏的话不用多说,肯定是自定义视图来实现界面的绘制. 这里我们直接继承View, 重写draw方法来绘制我们的游戏界面, 因为小游戏比较轻量级,这里我没有使用SurfaceView来绘制界面
2. 游戏属性值的定义
贪吃蛇应该都是有玩过的, 这里我们简单定义出 方向,行列格子数,蛇的颜色及食物颜色, 蛇运动方向,代码及说明如下:
/**
* 蛇头方向常量
*/
public final static int S_LEFT = 0;
public final static int S_TOP = 1;
public final static int S_RIGHT = 2;
public final static int S_BOTTOM = 3;
public final static int S_BASE_SPEED = 300;//基础速度,刷新毫秒
int mColNum = 18;//列数
int mRowNum = 18;//列数
float mColWidth;//列宽
int mBgColor = Color.parseColor("#a7a6ab");//背景颜色
int mGridLineColor = Color.parseColor("#838692");//网格线颜色
int mSnackBodyColor = Color.WHITE;//蛇身颜色
int mSnackHeadColor = Color.WHITE;//蛇头颜色
int mFoodColor = Color.parseColor("#fcae14");//食物颜色
int mSnackDefLen = 3;//蛇身默认长度
int mDirec = S_LEFT;//蛇头方向
int mSpeed = S_BASE_SPEED;//移动速度
boolean mGamePause = false;
LinkedList<SnackBody> mSnackBodys = new LinkedList<>();//蛇身
SnackBody mFood = new SnackBody(0, 0);//食物
3. 常量及思路说明.
这里蛇身使用的是双向链表来实现的,简化添加移除操作. 食物和身体都是方块构成, 所以这里直接使用单个SnackBody 类来表示食物
4. 界面绘制.
界面的绘制可以简化成背景 ,网格 食物和蛇的绘制, 代码如下:
protected void onDraw(Canvas canvas) {
canvas.drawColor(mBgColor);
drawFood(canvas);
drawSnack(canvas);
drawGridBg(canvas);
}
5. 界面绘制- 绘制网格.
即画横线和竖线, 这里方便理解使用了两个循环. (可以合并成一个)
/**
* 绘制网格背景
* @param canvas
*/
private void drawGridBg(Canvas canvas) {
int colNum = this.mColNum;
int rowNum = (int) (getHeight() / mColWidth);//计算行数
//绘制网格
mPaint.setColor(mGridLineColor);
//绘制列
for (int i = 0; i <= colNum; i++) {
float startX = i * mColWidth;
canvas.drawLine(startX, 0, startX, getHeight(), mPaint);
}
//绘制行
for (int i = 0; i <= rowNum; i++) {
float startY = i * mColWidth;
canvas.drawLine(0, startY, getWidth(), startY, mPaint);
}
}
6. 界面绘制- 绘制食物
/**
* 绘制食物
* @param canvas
*/
private void drawFood(Canvas canvas) {
SnackBody mFood = this.mFood;
mPaint.setColor(mFoodColor);
float left = mFood.x * mColWidth;
float top = mFood.y * mColWidth;
canvas.drawRect(left, top, left + mColWidth, top + mColWidth, mPaint);
}
7. 界面绘制-绘制蛇
/**
* 绘制蛇
* @param canvas
*/
private void drawSnack(Canvas canvas) {
LinkedList<SnackBody> mSnackBodys = this.mSnackBodys;
for (int i = mSnackBodys.size() - 1; i >= 0; i--) {
SnackBody mSnackBody = mSnackBodys.get(i);
mPaint.setColor(i == 0 ? mSnackHeadColor : mSnackBodyColor);
float left = mSnackBody.x * mColWidth;
float top = mSnackBody.y * mColWidth;
canvas.drawRect(left, top, left + mColWidth, top + mColWidth, mPaint);
}
}
8. 相关逻辑处理.
贪吃蛇的逻辑比较简单.可以分为三个块. 食物随机逻辑 蛇移动逻辑 游戏状态检查
9. 食物随机逻辑
//随机食物
private void randomFood() {
LinkedList<SnackBody> mSnackBodys = this.mSnackBodys;
if (mSnackBodys.size() >= mColNum * mRowNum) {
return;//满了,这么吊的么
}
SnackBody first = mSnackBodys.getFirst();
SnackBody food = new SnackBody(0, 0);
do {
//随机一个点,不在蛇头上就行. 这里允许食物和身体重叠
food.setPositon(random(0, mColNum), random(0, mRowNum));
} while (first.equals(food));
this.mFood = food;
}
10. 蛇移动逻辑
/**
* 蛇的移动
*/
private void snackMove() {
if (mGamePause) return;
LinkedList<SnackBody> mSnackBodys = this.mSnackBodys;
SnackBody nextHead = new SnackBody(0, 0);
SnackBody snackHead = mSnackBodys.getFirst();
switch (mDirec) {
case S_LEFT:
nextHead.setPositon(snackHead.x - 1, snackHead.y);
break;
case S_TOP:
nextHead.setPositon(snackHead.x, snackHead.y - 1);
break;
case S_RIGHT:
nextHead.setPositon(snackHead.x + 1, snackHead.y);
break;
case S_BOTTOM:
nextHead.setPositon(snackHead.x, snackHead.y + 1);
break;
}
mSnackBodys.addFirst(nextHead);
mSnackBodys.removeLast();
//检查是否吃到食物了,
SnackBody mFood = this.mFood;
if (nextHead.equals(mFood)) {
mSnackBodys.addFirst(mFood);
mSpeed = getSpeed();//计算新速度
if (onEatFoodListener != null) {
onEatFoodListener.eatFood(mSpeed);
}
//追加到头部,重新随机食物
randomFood();
}
checkGameEnd();
}
11. 游戏状态检查逻辑
private void checkGameEnd() {
LinkedList<SnackBody> mSnackBodys = this.mSnackBodys;
SnackBody headBody = mSnackBodys.getFirst();
//查询头部与身体的交集
int indexOf = mSnackBodys.lastIndexOf(headBody);
int cellNum = mColNum * mRowNum;
int snackLen = mSnackBodys.size();
//边界判断,咬到自己的判断,和占满了
if (headBody.x < 0 ||
headBody.y < 0 ||
headBody.x >= mColNum ||
headBody.y >= mRowNum ||
//0 是蛇头 1是刚吃到的食物 不可能咬到脖子(第二格)
(indexOf != 0 &&indexOf != 1 && indexOf != -1)||
snackLen>=cellNum
) {
mGamePause = true;
if (onGameOverListener != null) {
onGameOverListener.gameOver(snackLen, cellNum);
}
Logger.D("游戏结束");
}
}
12. 游戏界面刷新重绘逻辑
private void beginGameRun() {
removeCallbacks(runnable);
final Runnable localRun = new Runnable() {
@Override
public void run() {
int speed = mIsQuickMove ? mQuickSpeed : mSpeed;
snackMove();//自动移动
invalidate();
postDelayed(runnable, speed);
}
};
postDelayed(this.runnable = localRun, mSpeed);
}
13.方向控制逻辑
public void turnTo(int direc) {
switch (direc) {
case S_LEFT:
if (mDirec != S_RIGHT) {
mDirec = direc;
}
break;
case S_TOP:
if (mDirec != S_BOTTOM) {
mDirec = direc;
}
break;
case S_RIGHT:
if (mDirec != S_LEFT) {
mDirec = direc;
}
break;
case S_BOTTOM:
if (mDirec != S_TOP) {
mDirec = direc;
}
break;
}
beginGameRun();//重置重绘时间
snackMove();//移动蛇
invalidate();//刷新界面
}
14.蛇身体方块实体类
class SnackBody {
public int x, y;//坐标
public void setPositon(int x, int y) {
this.x = x;
this.y = y;
}
public SnackBody(int x, int y) {
this.x = x;
this.y = y;
}
//以下为自动生成代码,由坐标来判断方块是否一样
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SnackBody snackBody = (SnackBody) o;
if (x != snackBody.x) return false;
return y == snackBody.y;
}
@Override
public int hashCode() {
int result = x;
result = 31 * result + y;
return result;
}
}