先来玩玩再学习吧

玩玩贪吃蛇玩

此篇教程挺长的,也挺详细,按教程一步一步的做下去,你得到的结果是一个可以玩的贪吃蛇游戏。别想做完了上线,这个项目只用来对Dart语言的练习,若想做游戏,请去学习U3D,UE4。望你耐心看完。

贪吃蛇游戏介绍

图中,蓝色为蛇头,红色为蛇的身子,绿色为蛇吃的食物。当蓝色的蛇头碰到绿色的食物时,说明此绿色食物被蛇吃掉了,则蛇的身子将变长,每吃一个食物,蛇身增长一格。

:当食物全部吃完,也就是蛇头和蛇身占满全部网格,游戏结束。

:蛇头移动超出网格边框,游戏结束。蛇头碰到蛇身,游戏结束。

功能

我们做的贪吃蛇游戏主要有一下功能

  • 重置:重新开始游戏
  • 模式:「 我来玩 」是玩家通过键盘的上、下、左、右来操作蛇吃食物。「自己玩」是启用傻瓜AI,自动控制蛇吃食物。
  • 格子数:网格的总数量,也就是蛇可以在多少网格中移动 ,这里的网格都是横纵相等的,且横纵的网格数都是偶数。
  • 速度:蛇的移动速度,就是控制定时器的执行时间,来改变蛇的移动速度。

注意

开发之前你需要安装Dart SDK,若没有安装,请查看以下链接中的教程,或查看Dart官网教程。

Dart SDK 安装

Dart SDK 官网安装

开发本项目你需要具备的知识:

HTML

canvas

CSS

Flex

Dart

新建Dart项目

> mkdir snake // 创建名为 snake 的文件夹
> cd snake // 进入 snake 文件夹
> stagehand web-simple // 下载官方事例
> pub get // 更新依赖包

运行Dart项目

> webdev serve // 运行项目

在浏览器中输入CMD(终端)中所显示的地址,一般都是http://localhost:8080

到这里,项目准备工作已经完成了,下面开始贪吃蛇的开发

贪吃蛇UI界面介绍

界面分为两块。左边功能区,设置贪吃蛇游戏的一些配置。右边游戏区,展示贪吃蛇游戏。

贪吃蛇项目文件介绍

项目开发全在 web 文件夹下的 index.html、main.dart、styles.css中进行

贪吃蛇UI界面搭建

index.html

打开 index.html 内容如上,我们将 body 标签中的内容更改为如下内容

<body>
<div id="root-div">
<div id="output-div"><button id="reset">重置</button> </br></br>
模式:
<button id="me_play">我来玩</button>
<button id="snake_play">自己玩</button></br></br>
格子数:
<button class="grid_num">64</button> <button class="grid_num">100</button>
<button class="grid_num">256</button> <button class="grid_num">400</button>
<button class="grid_num">1024</button> <button class="grid_num">1600</button>
<button class="grid_num">2500</button> <button class="grid_num">6400</button>
<button class="grid_num">10000</button></br></br>
速度:
<button class="speed">1档</button> <button class="speed">2档</button>
<button class="speed">3档</button> <button class="speed">4档</button>
<button class="speed">5档</button>
</div>
<div id="output-div"><canvas id=canvas width_=800 height=800></canvas></div>
</div>
</body>

html布局就用了几个按钮button和一个画布canvas。画布的宽width和height可以随意修改,但建议按照教程里的设置,等到项目结束,再来尝试修改,以免在开发中遇到不必要的BUG。

styles.css

打开 styles.css 内容如上,我们将更改为如下内容

@import url(https://fonts.googleapis.com/css?family=Roboto);

html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
font-family: Roboto, sans-serif;
}

#output-div{
padding: 20px;
text-align: center;
}

#root-div{
display: -webkit-flex; /* Safari */
display: flex;
justify-content: space-around;
align-items: center ;
}

贪吃蛇UI完成效果

左边显示了,功能区,右边却什么都没有,并不是没有内容,只是没有对

canvas 做操作,所以没有内容展示

游戏区背景

游戏区背景就是一个网格,所以我们先画出一个网格,然后在网格中做分析,计划后面的代码流程

main.dart

打开 main.dart 你会看到如下的内容

首先我们需要获取到 html 中的 canvas,我们在 main() 方法中来获取,并生成成员变数 CanvasRenderingContext2D

CanvasRenderingContext2D context2d; // 画布对象

// 获取2D画布对象
CanvasElement canvas = querySelector(#canvas);
context2d = canvas.context2D;

上图中的 #canvas 对应 html 中的 id=canvas

创建成员变数 foodSize ,用来设置网格中的没有个格子的宽度和高度,单位为px。为啥起名为foodSize,后面网格分析的时候会解释

var foodSize = 100; // 食物大小,单位px

新建 drawGrid() 方法 ,用来在画布上画网格

/**
* 画格子
*/
void drawGrid() {
context2d.strokeStyle = "#ccc";// 画笔颜色

// 画竖线
for (var i = 0; i <= context2d.canvas.width; i += foodSize) {
context2d.beginPath();// 路径开始
context2d.moveTo(i, 0);// 起始点
context2d.lineTo(i, context2d.canvas.height);// 画线的重点
context2d.closePath();// 路径结束
context2d.stroke();// 开始绘画
}
// 画横线
for (var j = 0; j <= context2d.canvas.height; j += foodSize) {
context2d.beginPath();
context2d.moveTo(0, j);
context2d.lineTo(context2d.canvas.width, j);
context2d.closePath();
context2d.stroke();
}
}

新建 initData() 方法,用来存放初始化数据。在 initData() 方法中调用 drawGrid() 方法,在 main() 方法中调用 initData() 方法

initData();

void initData() {
drawGrid();
}

在浏览器中你会看到右边的游戏区出现了一个网格

context2d.canvas.width; 和 context2d.canvas.height; 是获取 html 中 canvas 的宽和高。相信你看了上面这个图,在结合 drawGrid() 方法中的代码,分析一下,也应该知道,格子是怎么画出来的了。

蛇和食物

我们吧空白格子都看作是蛇的食物,只是还没有显示出来而已,每一次只会显示一个食物,也就是绿色的那块,蛇初始的时候有3块,分别为1个蛇头,2个蛇身,这里你应该知道 foodSize 为什么这样命名了吧

在 web 文件夹下新建 bean 文件夹,新建 Position.dart 坐标类,用于存储食物蛇坐标的对象

import package:quiver/core.dart;

/**
* 坐标bean
*/
class Position {
var x, y;
Position(this.x, this.y);
// 改变对象的比较方式,本来是以对象的hashCode来判断对象是否是同一个对象,
// 改为用x的值和y的值来区分对象是否是同一个对象
// 类似于java中重写equals方法
bool operator ==(o) => o is Position && o.x == x && o.y == y;
int get hashCode => hash2(x.hashCode, y.hashCode);
}

在 main.dart 中创建以下成员变数

import bean/Position.dart;

var snakeColor = #f00; // 蛇的颜色
var snakeHeadColor = #00f; // 蛇头的颜色
var currentFoodColor = #0f0; // 当前食物颜色
var foodColor = #fff; // 剩下食物颜色
List<Position> snakeList = []; // 蛇的List
List<Position> foodList = []; // 食物的List
Position currentFood; // 当前食物

snakeList包含了蛇头和蛇身的坐标,foodList包含了所有食物的坐标

在 main.dart 中新建 initFoodData() 方法,用于初始化食物数据

/**
* 初始化食物数据
*/
void initFoodData() {
// 在html中的canvas的width_=800 height=800,
// 宽和高应该设置成相同的,以保证格子为正方形
var gridNum = context2d.canvas.width / foodSize;

var foodX = 0;
var foodY = 0;
for (var i = 0; i < gridNum * gridNum; i++) {
if (i != 0 && i % gridNum == 0) {
foodX = 0;
foodY = foodY + foodSize;
}
var food = Position(foodX, foodY);
foodList.add(food);
foodX = foodX + foodSize;
}
}

食物初始化数据,所做的操作就是把所有的格子坐标添加到 foodList 中

在 main.dart 中新建 draw(color, grid) 方法,用于画蛇和食物

/**
* 画
*/
void draw(color, grid) {
context2d.fillStyle = color;// 填充颜色
context2d.fillRect(grid.x, grid.y, foodSize, foodSize);// 填充
drawGrid();
}

color是填充格子的颜色,grid是格子的坐标,这里还调用了drawGrid();方法是因为,格子填充完,背景的格子线也被覆盖了,所以再画一次格子

在 main.dart 中新建 initSnakeData() 方法,用于初始化蛇的数据

import dart:math;

/**
* 初始化蛇的位置
*/
void initSnakeData() {
// 蛇头位置
var snakeHead = Position(0, 0);
// 蛇身1位置
var snakebody1 = Position(0, 0);
// 蛇身2位置
var snakebody2 = Position(0, 0);

// 随机产生蛇头位置
var foodIndex = Random().nextInt(foodList.length);
var food = foodList[foodIndex];
snakeHead.x = food.x;
snakeHead.y = food.y;

// 随机蛇身方向
var snakeBodyDirection = Random().nextInt(2);
if (snakeBodyDirection == 0) {
// 横向蛇身
if (food.x - foodSize - foodSize >= 0) {
snakebody1.x = food.x - foodSize;
snakebody1.y = food.y;
snakebody2.x = food.x - foodSize - foodSize;
snakebody2.y = food.y;
} else {
snakebody1.x = food.x + foodSize;
snakebody1.y = food.y;
snakebody2.x = food.x + foodSize + foodSize;
snakebody2.y = food.y;
}
} else {
// 纵向蛇身
if (food.y - foodSize - foodSize >= 0) {
snakebody1.x = food.x;
snakebody1.y = food.y - foodSize;
snakebody2.x = food.x;
snakebody2.y = food.y - foodSize - foodSize;
} else {
snakebody1.x = food.x;
snakebody1.y = food.y + foodSize;
snakebody2.x = food.x;
snakebody2.y = food.y + foodSize + foodSize;
}
}

// 蛇头数据操作
foodList.remove(snakeHead);
snakeList.add(snakeHead);
draw(snakeHeadColor, snakeHead);

// 蛇身1数据操作
foodList.remove(snakebody1);
snakeList.add(snakebody1);
draw(snakeColor, snakebody1);

// 蛇身2数据操作
foodList.remove(snakebody2);
snakeList.add(snakebody2);
draw(snakeColor, snakebody2);
}

做了蛇出现的位置随机操作,调用了 draw(color, grid) 方法 ,画出了蛇

在 main.dart 中新建 initCurrentFoodData() 方法,用于初始化当前显示的食物数据

/**
* 初始化当前食物
*/
void initCurrentFoodData() {
if (foodList.length == 0) return;
var foodIndex = Random().nextInt(foodList.length);
currentFood = Position(foodList[foodIndex].x, foodList[foodIndex].y);
draw(currentFoodColor, currentFood);
}

在 initData() 方法中调用 initFoodData(); initSnakeData(); initCurrentFoodData(); 这几个方法

保存代码,运行,在浏览器中就可以看到如下效果

游戏操作逻辑

在 main.dart 中新建 makeSnakeDate(x, y) 方法,用于处理蛇移动的数据

/**
* 处理蛇的移动数据
*/
void makeSnakeDate(x, y) {
// 是否还有食物
if (foodList.length == 0) {
window.alert("You Win");
reset();
initData();
startGame();
return;
}

var oldSnake = Position(snakeList[0].x, snakeList[0].y);
var newSnake = Position(snakeList[0].x + x, snakeList[0].y + y);

// 判断蛇撞边,撞到自己
if (newSnake.x < 0 ||
newSnake.x >= context2d.canvas.width ||
newSnake.y < 0 ||
newSnake.y >= context2d.canvas.height ||
foodList.indexOf(newSnake) == -1) {
window.alert("Game Over");
reset();
initData();
startGame();
return;
}

// 蛇和食物的数据处理
snakeList.insert(0, newSnake);
foodList.remove(newSnake);

// 将旧的蛇头变为蛇身
draw(snakeColor, oldSnake);
// 新蛇头
draw(snakeHeadColor, newSnake);
// 蛇吃食物
if (newSnake != currentFood) {
var removeSnake = snakeList.removeLast();
removeSnake = Position(removeSnake.x, removeSnake.y);
foodList.add(removeSnake);
// 移除蛇尾
draw(foodColor, removeSnake);
} else {
// 生成食物
initCurrentFoodData();
}
}

这里你的代码会报错,说缺少 reset(); 和 startGame(); 方法,不要著急,下面我们来写这两个方法

在 main.dart 中新建 reset() 方法,用于重置游戏

/**
* 重置游戏
*/
void reset() {
snakeDirection = null;
currentFood = null;
snakeList.clear();
foodList.clear();
timer.cancel();
// 清空页面所有图案
context2d.fillStyle = "#fff";
context2d.fillRect(0, 0, context2d.canvas.width, context2d.canvas.height);
}

这里也会报错, snakeDirection 和 timer 两个变数,snakeDirection 是控制蛇移动方向的变数,timer是游戏定时器,用于让蛇跑起来

在 main.dart 中新建成员变数 snakeDirection 、 timer 和 speed,speed用于控制蛇的移动速度

import dart:async;

var snakeDirection; // 蛇的方向
Timer timer; // 计时器
var speed = 500; // 蛇的速度(单位:毫秒)

在 main.dart 中新建 startGame() 方法,用于开启游戏

/**
* 启动游戏
*/
void startGame() {
timer = Timer.periodic(Duration(milliseconds: speed), (Timer t) {
switch (snakeDirection) {
case KeyCode.UP:
makeSnakeDate(0, -foodSize);
break;
case KeyCode.DOWN:
makeSnakeDate(0, foodSize);
break;
case KeyCode.LEFT:
makeSnakeDate(-foodSize, 0);
break;
case KeyCode.RIGHT:
makeSnakeDate(foodSize, 0);
break;
}
});
}

贪吃蛇移动逻辑写完了,但是贪吃蛇还无法控制他的移动,我们用键盘的上、下、左、右键来控制

键盘控制贪吃蛇移动

在 main.dart 中新建 isRunDirection() 方法,用于 判断蛇开始行走的开始方向

/**
* 判断蛇开始行走的开始方向
*/
bool isRunDirection(x, y) {
var runDirection = Position(snakeList[0].x + x, snakeList[0].y + y);
var indexOf = snakeList.indexOf(runDirection);
if (indexOf == -1) {
return false;
} else {
return true;
}
}

在 main.dart 中新建 keyboardEvents() 方法,用于监听电脑键盘事件

/**
* 键盘监听
*/
void keyboardEvents() {
window.onKeyUp.listen((KeyboardEvent e) {
// 判断是否有蛇
if (snakeList.length == 0) return;
// 键盘按键处理
switch (e.keyCode) {
case KeyCode.UP:
// 判断蛇开始行走的开始方向
if (snakeDirection == null && isRunDirection(0, -foodSize)) return;
// 相同方向不会起效,相反方向不会起效
if (snakeDirection == KeyCode.UP || snakeDirection == KeyCode.DOWN)
return;
snakeDirection = KeyCode.UP;
break;
case KeyCode.DOWN:
if (snakeDirection == null && isRunDirection(0, foodSize)) return;
if (snakeDirection == KeyCode.DOWN || snakeDirection == KeyCode.UP)
return;
snakeDirection = KeyCode.DOWN;
break;
case KeyCode.LEFT:
if (snakeDirection == null && isRunDirection(-foodSize, 0)) return;
if (snakeDirection == KeyCode.LEFT || snakeDirection == KeyCode.RIGHT)
return;
snakeDirection = KeyCode.LEFT;
break;
case KeyCode.RIGHT:
if (snakeDirection == null && isRunDirection(foodSize, 0)) return;
if (snakeDirection == KeyCode.RIGHT || snakeDirection == KeyCode.LEFT)
return;
snakeDirection = KeyCode.RIGHT;
break;
}
});
}

在 main() 方法中调用 keyboardEvents(); 和 startGame(); 方法

保存,运行代码,贪吃蛇可以正常的玩耍了

功能区的按钮功能实现

在 main.dart 中新建 initView() 方法,用于初始化功能区按钮功能

void initView() {
// 开始按钮
var resetBtn = querySelector(#reset);
resetBtn.onClick.listen((event) {
reset();

initData();
startGame();
});

// 选择格子数
var gridNums = document.querySelectorAll(.grid_num);
for (ButtonElement gridNum in gridNums) {
gridNum.onClick.listen((event) {
foodSize =
(context2d.canvas.width / sqrt(int.parse(gridNum.text))) as int;

reset();

initData();
startGame();
});
}

// 选择速度
var speeds = document.querySelectorAll(.speed);
for (ButtonElement speedBtn in speeds) {
speedBtn.onClick.listen((event) {
switch (speedBtn.text) {
case "1档":
speed = 1000;
break;
case "2档":
speed = 750;
break;
case "3档":
speed = 500;
break;
case "4档":
speed = 250;
break;
case "5档":
speed = 0;
break;
}

reset();

initData();
startGame();
});
}
}

在 main() 方法中调用 initView(); 方法

贪吃蛇到这里已经开发完成了,下面是拓展功能

傻瓜AI贪吃蛇

让贪吃蛇自己去吃食物,原理就是人操作的方式用代码来实现

在 web 文件夹中创建 ai 文件夹,新建 AiSnake.dart 文件

import dart:html;

import ../bean/Position.dart;

class AiSnake {
var startAI = false;

/**
* 执行AI前,将蛇头移动到(0,0)位置,蛇身横向,初始化
*/
aiInit(List<Position> snakeList, List<Position> foodList, int foodSize,
int snakeDirection, int gridMaxNum) {
if (startAI) return ai(snakeList, foodList, foodSize, snakeDirection);

var snakeHead = snakeList[0];
var snakeBody1 = snakeList[1];

// 初始化位置
if (snakeHead.x == 0 &&
snakeHead.y == 0 &&
snakeBody1.x == foodSize &&
snakeBody1.y == 0) {
startAI = true;
return KeyCode.DOWN;
}
// 纵向,蛇头在下
if (snakeHead.y - foodSize == snakeBody1.y) {
//蛇头在最下边
if (snakeHead.y == gridMaxNum) {
// 蛇头在最左边
if (snakeHead.x == 0)
return KeyCode.RIGHT;
else
return KeyCode.LEFT;
} else {
// 蛇头在最左边
if (snakeHead.x == 0)
return KeyCode.RIGHT;
else
return KeyCode.LEFT;
}
}
// 纵向,蛇头在上
if (snakeHead.y + foodSize == snakeBody1.y) {
// 蛇头在最上边
if (snakeHead.y == 0) {
// 蛇头在最左边
if (snakeHead.x == 0)
return KeyCode.RIGHT;
else
return KeyCode.LEFT;
} else {
// 蛇头在最左边
if (snakeHead.x == 0) {
return KeyCode.RIGHT;
} else {
return KeyCode.UP;
}
}
}

// 横向,蛇头在右
if (snakeHead.x - foodSize == snakeBody1.x) {
// 蛇头在最上边
if (snakeHead.y == 0) {
return KeyCode.DOWN;
} else {
return KeyCode.UP;
}
}

// 横向,蛇头在左
if (snakeHead.x + foodSize == snakeBody1.x) {
// 蛇头在最上边
if (snakeHead.y == 0) {
return KeyCode.LEFT;
} else {
return KeyCode.UP;
}
}
return null;
}

/**
* 偶数格子演算法
*/
int ai(List<Position> snakeList, List<Position> foodList, int foodSize,
int snakeDirection) {
var snakeHead = snakeList[0];

// 最左边
var left = Position(snakeHead.x - foodSize, snakeHead.y);
if (snakeDirection == KeyCode.LEFT && foodList.indexOf(left) == -1) {
return KeyCode.DOWN;
}
// 最左边,准备向右移动
if (snakeDirection == KeyCode.DOWN && foodList.indexOf(left) == -1) {
return KeyCode.RIGHT;
}

// 右边差一格,不在最下边
var right = Position(snakeHead.x + foodSize + foodSize, snakeHead.y);
var right1 = Position(snakeHead.x, snakeHead.y + foodSize);
if (snakeDirection == KeyCode.RIGHT &&
foodList.indexOf(right) == -1 &&
snakeList.indexOf(right) == -1 &&
snakeList.indexOf(right1) == -1 &&
foodList.indexOf(right1) != -1) {
return KeyCode.DOWN;
}
// 右边差一格,不在最下边,准备向左移动
if (snakeDirection == KeyCode.DOWN &&
foodList.indexOf(right) == -1 &&
snakeList.indexOf(right) == -1 &&
(snakeList.indexOf(right1) != -1 || foodList.indexOf(right1) != -1)) {
return KeyCode.LEFT;
}

// 右下角
var right2 = Position(snakeHead.x + foodSize, snakeHead.y);
if (snakeDirection == KeyCode.RIGHT &&
snakeList.indexOf(right2) == -1 &&
foodList.indexOf(right2) == -1) {
return KeyCode.UP;
}
// 右上角
var up = Position(snakeHead.x, snakeHead.y - foodSize);
if (snakeDirection == KeyCode.UP && foodList.indexOf(up) == -1) {
return KeyCode.LEFT;
}
return null;
}
}

在 main.dart 新建 AiSnake 对象, modePlay 变数,modePlay用于功能区的我来玩、自己玩模式切换

import ai/AiSnake.dart;

var aiSnake = AiSnake(); // AI
var modePlay = 0; // 玩法模式模式,0是我来玩,1是自己玩

在 main.dart 的 startGame() 方法的定时器中调用 ainInit 方法 并把方法返回值赋值给 snakeDirection 变数

if (modePlay == 1) {
var aiInit = aiSnake.aiInit(snakeList, foodList, foodSize,
snakeDirection, context2d.canvas.width);
if (aiInit != null) snakeDirection = aiInit;
}

在 main.dart 的 reset() 方法中改变AI贪吃蛇开始的转态

aiSnake.startAI = false;

在 main.dart 的 initView() 方法中设置 我来玩、自己玩 的点击事件

// 我来玩
var mePlayBtn = querySelector(#me_play);
mePlayBtn.onClick.listen((event) {
modePlay = 0;
reset();

initData();
startGame();
});
// 自己玩
var snakePlayBtn = querySelector(#snake_play);
snakePlayBtn.onClick.listen((event) {
modePlay = 1;
reset();

initData();
startGame();
});

GitHub源码

PS:至此,贪吃蛇游戏制作完成,教程中,有些逻辑思路和最后的傻瓜AI贪吃蛇我没做讲解,都是些简单的代码,相信你应该可以看懂。以后有时间我在把具体思路补上吧

推荐阅读:

相关文章