先来了解一些简单的基础知识

下面是一个20 x 20的矩阵 用来表示贪吃蛇的地图

坐标原点为左下角

我们需要有一个数组来保存当前蛇的蛇身体

snake数组是存在顺序的,数组的第一个元素是蛇头的位置,数组的最后一位就是蛇的尾巴

let snake: Array<number> = [202, 201];

接下来是蛇的移动,也就是数组数据的移动

  • 上边移动 蛇头数据+20
  • 下边移动 蛇头数据-20
  • 左边移动 蛇头数据-1
  • 右边移动 蛇头数据+1

上边只说到蛇头数据,因为我们只需要修改一个蛇头的数据,不用修改整条蛇的数据

蛇的数据

snake = [202, 201];

当前蛇的方向是右边,也就是数据+1,加入一个新的蛇头

snake = [203, 202, 201];

当前蛇的长度变成了3,可是我们的蛇应该只有2的长度,还需要把蛇的尾巴去掉一个

snake = [203, 202];

这样蛇就完成了向右一格的移动 上下左移动都是同理,这里就不赘述了

下面进入正题了

10分钟编写一个贪吃蛇,先确定下规则

  • 贪吃蛇由上下左右键位控制
  • 蛇头能穿越墙壁从另一面出来
  • 吃到食物增加一个长度
  • 撞到自己的身体即为失败

新建一个空的项目

加入一个空节点,用来承载地图,命名为map

  • map需要挂载一个画图的组件

接下来是编写脚本Map.ts

初始化一些变量

snake: Array<number> = [202, 201]; // 定义蛇的数组 
food = 203; // 食物
moveOffset = 1; // 蛇头的变化值
nextIndex = 0; // 下一个蛇头的位置id
g: cc.Graphics = null;
canControl = false; // 是否可以控制

先加入控制的方法

onEnable() {
cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown.bind(this));
}

onDisable() {
cc.systemEvent.off(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown.bind(this));
}

onKeyDown(e: cc.Event.EventKeyboard) {
if (!this.canControl){
return;
}
this.moveOffset = this.snake[1] - this.snake[0] == (this.nextIndex = [-1, 20, 1, -20][e.keyCode - 37] || this.moveOffset) ? this.moveOffset : this.nextIndex;
}

简单分析这段代码:

this.moveOffset = this.snake[1] - this.snake[0] == (this.nextIndex = [-1, 20, 1, -20][e.keyCode - 37] || this.moveOffset) ? this.moveOffset : this.nextIndex;
  • this.moveOffset 是蛇头的移动变量 也就是到下一格变化量
  • this.snake[1] - this.snake[0] 获得当前蛇头不能变化的方法
  • this.nextIndex 在这里做了下临时变量,记录操作值
  • 左上右下的keyCode 分别为37 38 39 40 如果按其他按键就使用原来的移动变量

如果还不理解可以看下运算符的执行顺序

主要的移动和碰撞判断代码,有几个点需要简单分析下

  • [].unshift 从头部加入数组
  • [].indexOf 获得元素在数组中的位置(这里用来判断是否在数组中)
  • 循环遍历食物不存在与当前的蛇体内 不会无限循环 因为覆盖会在上面的函数中跳出
  • 尾部去除的格子用背景色覆盖
move() {
// 增加当前蛇头的位置
this.nextIndex = this.snake[0] + this.moveOffset
// 加入新蛇头
this.snake.unshift(this.nextIndex);
// 判断新蛇头有没撞到自己
if(this.snake.indexOf(this.nextIndex, 1) > 0) {
this.replay.getChildByName('result').getComponent(cc.Label).string = `score: ${this.snake.length * 100}`;
this.replay.active = true;
// 变红
this.snake.forEach(v => {
this.draw(v, cc.Color.RED);
});
return;
}
// 蛇头的越界修复
if (this.moveOffset == -20 && this.nextIndex < 0) {
this.snake[0] = this.snake[0] + 400;
} else if(this.moveOffset == 20 && this.nextIndex > 399) {
this.snake[0] = this.snake[0] - 400;
} else if (this.moveOffset == 1 && (this.nextIndex % 20 == 0 || this.nextIndex > 399)) {
this.snake[0] = this.snake[0] - 20;
} else if (this.moveOffset == -1 && (this.nextIndex % 20 == 19 || this.nextIndex < 0)) {
this.snake[0] = this.snake[0] + 20;
}
// 画出当前的蛇头
this.draw(this.snake[0], cc.Color.GREEN);
// 如果当前的蛇头是产生的糖果
// 产生新的糖果
if(this.nextIndex == this.food){
// 当前产生的块不存在蛇
while(this.snake.indexOf(this.food = ~~(Math.random() * 400)) >= 0);
// 产生新的事物
this.draw(this.food, cc.Color.YELLOW);
}else{
// 删除原来的点
this.draw(this.snake.pop(), cc.Color.GRAY);
}
setTimeout(this.move.bind(this), 1000/10)
};

最简单的Draw方法

  • 画出当前方块的实体
draw(mapIndex: number, color: cc.Color) {
this.g.fillColor = color;
this.g.fillRect(mapIndex % 20 * 20 - 2 , ~~(mapIndex / 20) * 20 - 2, 16, 16)
}

ps 可以扩展的部分

  • 以上就是主要代码的部分,下面是一些扩展和方向,就不属于10分钟内的了
  • 设置蛇头颜色,清理尾部格子的时候顺便清理snake[1]为蛇身颜色即可
  • 多个食物,设置food为数组,判断数组内是否是蛇头就好了
  • 关卡加速,控制帧率越来越高即可 ps: 30差不多就难以操作了

源码地址: https://gitee.com/limo/snake