数据蛇vip课程(canvas 300行代码实现一个贪吃蛇)
突然心血来潮,花了一个小时写了个贪吃蛇 ^_^ !
规则
- 整图为一个 64 * 40 大小的矩形
- 随机在空白坐标点生成食物
- 蛇不能撞墙和吃自己
- 每吃一个食物身体长一格
- 初始速度为 150ms 移动一次,随着食物越多,速度越快,最快 50ms 移动一次
- 方向的改变用键盘的方向键控制,不能直接往反方向转向
实现思路
整体思路为:
- 使用 canvas 作为基础实现方式
- 蛇的主体采用一个二维数组来实现绘制
- 蛇的移动去掉蛇数据结构的最后一位,根据方向和蛇头的数据做相应更改再添加到蛇主体头部,同时判断是否结束
- 蛇将要移动的坐标点是否是食物的位置,如果是则不删除蛇的最后一位数据
- 如果蛇的长度为 64 * 40 大小,占满整个矩形,则游戏通关完成
html + css
<!-- css -->
<style>
body {
background-color: #eee;
}
.container {
text-align: center;
}
.top {
margin: 20px auto;
width: 640px;
}
#score {
float: left;
}
.main {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 642px;
height: 402px;
}
#snake {
border: 1px solid #000;
width: 640px;
height: 400px;
display: inline-block;
z-index: 99;
background-color: rgba(0, 0, 0, .1);
}
#mask {
background-color: rgba(0, 0, 0, .5);
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 100;
display: block;
color: #fff;
line-height: 400px;
text-align: center;
font-size: 30px;
cursor: pointer;
}
</style>
<!-- DOM -->
<div class="container">
<div class="top">
<span id="score">Score: 0</span>
<button id="restart">重新开始</button>
<button id="stop">暂停</button>
<button id="continue">继续</button>
</div>
<div class="main">
<canvas id="snake" width="640" height="400"></canvas>
<div id="mask">开始</div>
</div>
</div>
构造规则
先定义所有行为的方法,然后再进行组合。
初始构造方法
<script>
let greedySnake = null
let score = document.querySelector('#score')
let restart = document.querySelector('#restart')
let stop = document.querySelector('#stop')
let conti = document.querySelector('#continue')
let mask = document.querySelector('#mask')
class GreedySnake {
constructor() {
this.canvas = document.querySelector('#snake')
this.ctx = this.canvas.getContext('2d')
this.maxX = 64 // 最大行
this.maxY = 40 // 最大列
this.itemWidth = 10 // 每个点的大小
this.direction = 'right'// up down right left 方向
this.speed = 150 // ms 速度
this.isStop = false // 是否暂停
this.isOver = false // 是否结束
this.isStart = false // 是否开始
this.score = 0 // 分数
this.timer = null // 移动定时器
this.j = 1 // 食物闪烁辅助变量
this.canChange = true // 是否能改变防线
this.grid = new Array() // 计算得到所有坐标点
for (let i = 0; i < this.maxX; i++) {
for (let j = 0; j < this.maxY; j++) {
this.grid.push([i, j])
}
}
}
}
// 初始实例化
greedySnake = new GreedySnake()
</script>
初始化蛇数据
在中间位置取一组数据定义为蛇的初始位置
// 创建蛇主体
createSnake() {
this.snake = [
[4, 25],
[3, 25],
[2, 25],
[1, 25],
[0, 25]
]
}
食物生成
// 取坐标点
createPos() {
let [x, y] = this.grid[(Math.random() * this.grid.length) | 0]
// 取的位置不能是蛇数据内的坐标
for (let i = 0; i < this.snake.length; i++) {
if (this.snake[i][0] == x && this.snake[i][1] == y) {
return this.createPos()
}
}
return [x, y]
}
// 生成食物
createFood() {
this.food = this.createPos()
// 每一次食物生成,表明已被吃掉一个,则更新分数
score.innerHTML = 'Score: '+ this.score++
// 更新速度,最大速度为 50ms
if (this.speed > 50) {
this.speed--
}
}
网格线绘制
// 网格线
drawGridLine() {
for (let i = 1; i < this.maxY; i++) {
this.ctx.moveTo(0, i * this.itemWidth)
this.ctx.lineTo(this.canvas.width, i * this.itemWidth)
}
for (let i = 1; i < this.maxX; i++) {
this.ctx.moveTo(i * this.itemWidth, 0)
this.ctx.lineTo(i * this.itemWidth, this.canvas.height)
}
this.ctx.lineWidth = 1
this.ctx.strokeStyle = '#ddd'
this.ctx.stroke()
}
绘制蛇和食物
// 绘制
draw() {
// 清空画布
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
// 绘制网格
this.drawGridLine()
// 绘制食物
this.ctx.fillStyle="#000"
this.ctx.fillRect(
this.food[0] * this.itemWidth + this.j,
this.food[1] * this.itemWidth + this.j,
this.itemWidth - this.j * 2,
this.itemWidth - + this.j * 2
)
this.j ^= 1
// 绘制蛇头
this.ctx.fillStyle="green"
this.ctx.fillRect(
this.snake[0][0] * this.itemWidth + 0.5,
this.snake[0][1] * this.itemWidth + 0.5,
this.itemWidth - 1,
this.itemWidth - 1
)
// 绘制蛇身
this.ctx.fillStyle="red"
for (let i = 1; i < this.snake.length; i++) {
this.ctx.fillRect(
this.snake[i][0] * this.itemWidth + 0.5,
this.snake[i][1] * this.itemWidth + 0.5,
this.itemWidth - 1,
this.itemWidth - 1
)
}
}
暂停与继续
// 暂停游戏
stop() {
if (this.isOver) return
this.isStop = true
mask.style.display = 'block'
mask.innerHTML = '暂停'
}
// 继续游戏
continue() {
if (this.isOver) return
this.isStop = false
this.move()
mask.style.display = 'none'
}
蛇的方向控制
监听方向键的按压,来改变方向
getDirection() {
// 上38 下40 左37 右39 不能往相反的方向走
document.onkeydown = (e) => {
// 在贪吃蛇移动的间隔内不能连续改变两次方向
if (!this.canChange) return
switch(e.keyCode) {
case 37:
if (this.direction !== 'right') {
this.direction = 'left'
this.canChange = false
}
break
case 38:
if (this.direction !== 'down') {
this.direction = 'up'
this.canChange = false
}
break
case 39:
if (this.direction !== 'left') {
this.direction = 'right'
this.canChange = false
}
break
case 40:
if (this.direction !== 'up') {
this.direction = 'down'
this.canChange = false
}
break
case 32:
// 空格暂停与继续
if (!this.isStop) {
this.stop()
} else {
this.continue()
}
break
}
}
}
是否违规结束
// 结束
over([x, y]) {
if (x < 0 || x >= this.maxX || y < 0 || y >= this.maxY) {
return true
}
if (this.snake.some(v => v[0] === x && v[1] === y)) {
return true
}
}
是否通关完成
// 完成
completed() {
if (this.snake.length == this.maxX * this.maxY) {
return true
}
}
蛇的移动
蛇移动是整个贪吃蛇游戏的逻辑实现,内部需要组合其它的各种方法来完成。
// 移动
move() {
if (this.isStop) return
let [x, y] = this.snake[0]
switch(this.direction) {
case 'left':
x--
break
case 'right':
x++
break
case 'up':
y--
break
case 'down':
y++
break
}
// 如果下一步不是食物的位置,则删掉最后一位数据
if (x !== this.food[0] || y !== this.food[1]) {
this.snake.pop()
} else { // 如果是食物则不删掉最后一个并再生成一个食物
this.createFood()
}
// 判断是否结束
if (this.over([x, y])) {
this.isOver = true
mask.style.display = 'block'
mask.innerHTML = '结束'
return
}
// 判断是否完成
if (this.completed()) {
mask.style.display = 'block'
mask.innerHTML = '恭喜您,游戏通关'
return
}
// 将坐标点放进蛇头部
this.snake.unshift([x, y])
this.draw() // 绘制
this.canChange = true // 可以更改方向
// 递归绘制,实现蛇的一直爬行
this.timer = setTimeout(() => this.move(), this.speed)
}
最后外加几个按钮来实现交互
restart.onclick = () => {
if (!greedySnake.isStart) return
greedySnake.start()
}
stop.onclick = () => {
if (greedySnake.isStop || !greedySnake.isStart) return
greedySnake.stop()
}
conti.onclick = () => {
if (!greedySnake.isStop || !greedySnake.isStart) return
greedySnake.continue()
}
mask.onclick = () => {
if (!greedySnake.isStart) {
greedySnake.start()
} else {
greedySnake.continue()
}
}
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>贪吃蛇</title>
<style>
body {
background-color: #eee;
}
.container {
text-align: center;
}
.top {
margin: 20px auto;
width: 640px;
}
#score {
float: left;
}
.main {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 642px;
height: 402px;
}
#snake {
border: 1px solid #000;
width: 640px;
height: 400px;
display: inline-block;
z-index: 99;
background-color: rgba(0, 0, 0, .1);
}
#mask {
background-color: rgba(0, 0, 0, .5);
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 100;
display: block;
color: #fff;
line-height: 400px;
text-align: center;
font-size: 30px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="container">
<div class="top">
<span id="score">Score: 0</span>
<button id="restart">重新开始</button>
<button id="stop">暂停</button>
<button id="continue">继续</button>
</div>
<div class="main">
<canvas id="snake" width="640" height="400"></canvas>
<div id="mask">开始</div>
</div>
</div>
<script>
let greedySnake = null
let score = document.querySelector('#score')
let restart = document.querySelector('#restart')
let stop = document.querySelector('#stop')
let conti = document.querySelector('#continue')
let mask = document.querySelector('#mask')
restart.onclick = () => {
if (!greedySnake.isStart) return
greedySnake.start()
}
stop.onclick = () => {
if (greedySnake.isStop || !greedySnake.isStart) return
greedySnake.stop()
}
conti.onclick = () => {
if (!greedySnake.isStop || !greedySnake.isStart) return
greedySnake.continue()
}
mask.onclick = () => {
if (!greedySnake.isStart) {
greedySnake.start()
} else {
greedySnake.continue()
}
}
// 大小为64 * 40
class GreedySnake {
constructor() {
this.canvas = document.querySelector('#snake')
this.ctx = this.canvas.getContext('2d')
this.maxX = 64 // 最大行
this.maxY = 40 // 最大列
this.itemWidth = 10 // 每个点的大小
this.direction = 'right'// up down right left 方向
this.speed = 150 // ms 速度
this.isStop = false // 是否暂停
this.isOver = false // 是否结束
this.isStart = false // 是否开始
this.score = 0 // 分数
this.timer = null // 移动定时器
this.j = 1
this.canChange = true
this.grid = new Array()
for (let i = 0; i < this.maxX; i++) {
for (let j = 0; j < this.maxY; j++) {
this.grid.push([i, j])
}
}
this.drawGridLine()
this.getDirection()
}
// 开始
start() {
if (this.timer) {
clearTimeout(this.timer)
}
if (!this.isStart) {
this.isStart = true
}
this.score = 0
this.speed = 150
this.isStop = false
this.isOver = false
this.direction = 'right'
this.createSnake()
this.createFood()
this.draw()
this.move()
mask.style.display = 'none'
}
// 创建蛇主体
createSnake() {
this.snake = [
[4, 25],
[3, 25],
[2, 25],
[1, 25],
[0, 25]
]
}
// 移动
move() {
if (this.isStop) return
let [x, y] = this.snake[0]
switch(this.direction) {
case 'left':
x--
break
case 'right':
x++
break
case 'up':
y--
break
case 'down':
y++
break
}
// 如果下一步不是食物的位置
if (x !== this.food[0] || y !== this.food[1]) {
this.snake.pop()
} else {
this.createFood()
}
if (this.over([x, y])) {
this.isOver = true
mask.style.display = 'block'
mask.innerHTML = '结束'
return
}
if (this.completed()) {
mask.style.display = 'block'
mask.innerHTML = '恭喜您,游戏通关'
return
}
this.snake.unshift([x, y])
this.draw()
this.canChange = true
this.timer = setTimeout(() => this.move(), this.speed)
}
// 暂停游戏
stop() {
if (this.isOver) return
this.isStop = true
mask.style.display = 'block'
mask.innerHTML = '暂停'
}
// 继续游戏
continue() {
if (this.isOver) return
this.isStop = false
this.move()
mask.style.display = 'none'
}
getDirection() {
// 上38 下40 左37 右39 不能往相反的方向走
document.onkeydown = (e) => {
// 在贪吃蛇移动的间隔内不能连续改变两次方向
if (!this.canChange) return
switch(e.keyCode) {
case 37:
if (this.direction !== 'right') {
this.direction = 'left'
this.canChange = false
}
break
case 38:
if (this.direction !== 'down') {
this.direction = 'up'
this.canChange = false
}
break
case 39:
if (this.direction !== 'left') {
this.direction = 'right'
this.canChange = false
}
break
case 40:
if (this.direction !== 'up') {
this.direction = 'down'
this.canChange = false
}
break
case 32:
// 空格暂停与继续
if (!this.isStop) {
this.stop()
} else {
this.continue()
}
break
}
}
}
createPos() {
let [x, y] = this.grid[(Math.random() * this.grid.length) | 0]
for (let i = 0; i < this.snake.length; i++) {
if (this.snake[i][0] == x && this.snake[i][1] == y) {
return this.createPos()
}
}
return [x, y]
}
// 生成食物
createFood() {
this.food = this.createPos()
// 更新分数
score.innerHTML = 'Score: '+ this.score++
if (this.speed > 50) {
this.speed--
}
}
// 结束
over([x, y]) {
if (x < 0 || x >= this.maxX || y < 0 || y >= this.maxY) {
return true
}
if (this.snake.some(v => v[0] === x && v[1] === y)) {
return true
}
}
// 完成
completed() {
if (this.snake.length == this.maxX * this.maxY) {
return true
}
}
// 网格线
drawGridLine() {
for (let i = 1; i < this.maxY; i++) {
this.ctx.moveTo(0, i * this.itemWidth)
this.ctx.lineTo(this.canvas.width, i * this.itemWidth)
}
for (let i = 1; i < this.maxX; i++) {
this.ctx.moveTo(i * this.itemWidth, 0)
this.ctx.lineTo(i * this.itemWidth, this.canvas.height)
}
this.ctx.lineWidth = 1
this.ctx.strokeStyle = '#ddd'
this.ctx.stroke()
}
// 绘制
draw() {
// 清空画布
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.drawGridLine()
this.ctx.fillStyle="#000"
this.ctx.fillRect(
this.food[0] * this.itemWidth + this.j,
this.food[1] * this.itemWidth + this.j,
this.itemWidth - this.j * 2,
this.itemWidth - + this.j * 2
)
this.j ^= 1
this.ctx.fillStyle="green"
this.ctx.fillRect(
this.snake[0][0] * this.itemWidth + 0.5,
this.snake[0][1] * this.itemWidth + 0.5,
this.itemWidth - 1,
this.itemWidth - 1
)
this.ctx.fillStyle="red"
for (let i = 1; i < this.snake.length; i++) {
this.ctx.fillRect(
this.snake[i][0] * this.itemWidth + 0.5,
this.snake[i][1] * this.itemWidth + 0.5,
this.itemWidth - 1,
this.itemWidth - 1
)
}
}
}
greedySnake = new GreedySnake()
</script>
</body>
</html>
作者:对半
链接:https://juejin.cn/post/6959789039566192654
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
0人
相关文章