Я участвую в командном соревновании игрового творческого конкурса сообщества Nuggets Подробности см. в разделе Конкурс творческого вклада в игру
Сегодня я поделюсь с вами небольшой игрой greedy Snake, html + css + js
реализованной в чистом vue + uniCloud
виде. Поскольку стек технологий очень неглубокий, я надеюсь, что большие ребята не распылят его. Без дальнейших церемоний, давайте начнем прямо.
Схема эффекта выглядит следующим образом
Инициализировать проект
Этот проект uni-app
основан на разработке, поэтому лучше использовать его HBuilder
в качестве редактора по вашему выбору.
Новый проект
Откройте HBuilder
, создайте новый проект, назовите проект snake_eat_worm
(змея ест жуков), я дал ему длинное имя: Snake Fighting Group Worm. Полные оценки за имя, поставить себе куриную ножку. Выберите шаблон по умолчанию, установите флажок «Включить uniCloud
» (по умолчанию используется Alibaba Cloud) и нажмите «Создать».
Это
HBuilder
автоматически создаст каталог проекта:
Запускаем проект напрямую:
Следующий интерфейс указывает на то, что проект выполняется успешно.
Подготовка игровых ресурсов
поместить ресурсы в static/images
каталог
карта
Откроем pages/index
страницу, изменим ее и добавим свои вещи:
<template>
<view class="content">
贪吃蛇~
</view>
</template>
复制代码
ок, страница уже под нашим контролем, мы собираемся ее официально запустить.
рисовать землю
Мы устанавливаем землю как сетку 10 * 10 и используем div для рисования сетки 10 * 10?
Юный герой! Подождите, просто div достаточно! Если вы мне не верите, см.:
<template>
<view class="content">
<view class="game-field">
<view class="block"></view>
<view class="block"></view>
<view class="block"></view>
<view class="block"></view>
<!-- 此处省略 100 - 4 个 -->
</view>
</view>
</template>
<style>
.content {
height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 0;
}
.game-field {
display: flex;
flex-wrap: wrap;
}
.block {
width: 10vw;
height: 10vw;
display: flex;
justify-content: center;
align-items: center;
background-color: rgb(232, 235, 178);
background-repeat: no-repeat;
background-position: center;
background-size: cover;
box-sizing: border-box;
outline: 2upx solid;
}
</style>
复制代码
Я считаю, что друзья, которые знают только элементы div, должны быть лучше знакомы с рендерингом повторяющихся элементов div, чем я:
<template>
<view class="content">
<view class="game-field">
<view class="block" v-for="x in 100" :key="x"></view>
</view>
</view>
</template>
复制代码
Молодцы, теперь в нашем игровом интерфейсе появился ком желтой земли, давайте посмотрим, как положить несколько жуков на землю.
баги рисования
Прежде чем рисовать, давайте подумаем, как использовать div для рисования разных узоров (змей и жуков или земли) в разных сетках. Мы могли бы также думать об этих 100 сетках как о массиве. Каждая сетка соответствует индексам 0-99 массива. Мы инициализируем массив 100 нулями и используем число 0 для обозначения земли. Используйте номер 1 для жука и номер 2 для змеи. Мы немного изменили приведенный выше код:
<template>
<view class="content">
<view class="game-field">
<view class="block" v-for="(x,i) in blocks" :key="i">{{x}}</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
blocks: new Array(100).fill(0), // 格子
worms: [6, 33, 87] // 初始化三只虫子,数字代表它所处的格子的下标
}
},
onLoad() {
this.worms.forEach(x=> {
this.blocks[x] = 1;
})
this.$forceUpdate();
}
}
</script>
复制代码
可以看到在黄色土地里有3个格子的数字是1,其他是0。这样就好办了,我们只需把数字是1的格子的背景图变成一只小虫子就好了:
<template>
<view class="content">
<view class="game-field">
<view class="block"
:style="{'background-image': bg(x)}"
v-for="(x, i) in blocks" :key="i">
{{x}}
</view>
</view>
</view>
</template>
<script>
import worm from "../../static/images/worm.png";
export default {
data() {
return {
blocks: new Array(100).fill(0), // 格子
worms: [6, 33, 87] // 初始化三只虫子,数字代表它所处的格子的下标
}
},
onLoad() {
// 将虫子的格子赋值为1
this.worms.forEach(x=> {
this.blocks[x] = 1;
})
},
methods: {
bg(type) {
let bg = "";
switch (type) {
case 0: // 地板
bg = "unset";
break;
case 1: // 虫子
bg = `url(${worm})`;
break;
case 2: // 蛇
// TODO
}
return bg;
}
},
}
</script>
复制代码
至此,虫子的绘制就完成了,接下来我们开始绘制蛇,蛇的绘制可是有点难度哦,各位看官做好准备了吗,只会div的朋友,恐怕你真的要告辞了,哈哈啊哈
绘制蛇
从游戏资源中,我们可以看到我们的蛇是有头有尾的,咱们先不管,咱们先把蛇的身子怼上去,去头去尾拿去炖汤~
<template>
<view class="content">
<view class="game-field">
<view class="block"
:style="{'background-image': bg(x)}"
v-for="(x, i) in blocks" :key="i">
{{x}}
</view>
</view>
</view>
</template>
<script>
import worm from "../../static/images/worm.png";
import snakeBody from "../../static/images/snake_body.png";
export default {
data() {
return {
blocks: new Array(100).fill(0), // 格子
worms: [6, 33, 87], // 初始化三只虫子,数字代表它所处的格子的下标
snakes: [0, 1, 2, 3] // 我们的蛇默认在左上角的位置,占据4个格子
}
},
onLoad() {
// 将虫子的格子赋值为1
this.worms.forEach(x=> {
this.blocks[x] = 1;
})
// 同理将蛇的格子赋值为2
this.snakes.forEach((x) => {
this.blocks[x] = 2;
});
this.$forceUpdate();
},
methods: {
bg(type) {
let bg = "";
switch (type) {
case 0: // 地板
bg = "unset";
break;
case 1: // 虫子
bg = `url(${worm})`;
break;
case 2: // 蛇
bg = `url(${snakeBody})`;
break;
}
return bg;
}
},
}
</script>
复制代码
哈哈,现在我们可以看到一个无头无尾蛇,拿去炖汤。其实啊,完整的蛇更美味呢,接下来我们把蛇头蛇尾补上。这里我们需要记录当前格子的下标,因为我们定义蛇的数组是记录了蛇所处格子的下标的:
绘制蛇头蛇尾
<template>
<view class="content">
<view class="game-field">
<view class="block"
:style="{'background-image': bg(x, i)}"
v-for="(x, i) in blocks" :key="i">
{{x}}
</view>
</view>
</view>
</template>
<script>
import worm from "../../static/images/worm.png";
import snakeBody from "../../static/images/snake_body.png";
import snakeHead from "../../static/images/snake_head.png";
import snakeTail from "../../static/images/snake_tail.png";
export default {
data() {
return {
blocks: new Array(100).fill(0), // 格子
worms: [6, 33, 87], // 初始化三只虫子,数字代表它所处的格子的下标
snakes: [0, 1, 2, 3] // 我们的蛇默认在左上角的位置,占据4个格子
}
},
onLoad() {
// 将虫子的格子赋值为1
this.worms.forEach(x=> {
this.blocks[x] = 1;
})
// 同理将蛇的格子赋值为2
this.snakes.forEach((x) => {
this.blocks[x] = 2;
});
this.$forceUpdate();
},
methods: {
bg(type, index) {
let bg = "";
switch (type) {
case 0: // 地板
bg = "unset";
break;
case 1: // 虫子
bg = `url(${worm})`;
break;
case 2: // 蛇
// 蛇的数组最后一个就是蛇头了
let head = this.snakes[this.snakes.length - 1];
// 同理第一个就是蛇尾
let tail = this.snakes[0];
if (index === head) {
bg = `url(${snakeHead})`;
} else if (index === tail) {
bg = `url(${snakeTail})`;
} else {
bg = `url(${snakeBody})`;
}
break;
}
return bg;
}
},
}
</script>
复制代码
至此,蛇的绘制完成了,但是看着好难受啊,蛇的头身子尾巴都没对上呢
让它看着正常
将图片顺时针旋转90度:
<template>
<view class="content">
<view class="game-field">
<view class="block"
:style="{'background-image': bg(x, i),transform: rotate(90deg)}"
v-for="(x, i) in blocks" :key="i">
{{x}}
</view>
</view>
</view>
</template>
复制代码
欧耶,看着正常了!emmmm...
貌似哪里不对劲,啊呀,虫子怎么倒了~,加个判断,是蛇才旋转90
度:
<template>
<view class="content">
<view class="game-field">
<view class="block"
:style="{'background-image': bg(x, i),
transform: x === 2 ? 'rotate(90deg)': 'rotate(0)'}"
v-for="(x, i) in blocks" :key="i">
{{x}}
</view>
</view>
</view>
</template>
复制代码
嗯,正常了!
开始游戏
蛇和虫子都画完了,接下来我们应该考虑的是如何让蛇动起来
让蛇动起来
要让蛇动,肯定要加个定时器了。我们刚开始是在onLoad
的时候渲染一次界面,要让蛇动起来,我们需要将绘制蛇的方法封装起来,这样才能在定时器中循环执行:
<script>
// ...
export default {
// ...
data() {
return: {
// ...
timer: null // 定时器
}
},
onLoad() {
// ...
this.paint();
this.timer = setInterval(() => {
this.toWards(); // 每秒向前走一格
}, 1000);
},
methods: {
// ...
paint() {
this.worms.forEach((x) => {
this.blocks[x] = 1;
});
this.snakes.forEach((x) => {
this.blocks[x] = 2;
});
this.$forceUpdate();
},
toWards() {
// 头部下标
let head = this.snakes[this.snakes.length - 1];
// 尾部下标
let tail = this.snakes[0];
// 向右走一格
let next = head + 1;
// 那么下一格就应该加入蛇的数组中
this.snakes.push(next);
// 保存下一格是什么类型方便一会判断蛇的数组是否需要弹出尾部元素
let nextType = this.blocks[next];
// 蛇头经过的位置必然变成蛇的格子,于是赋值为2
this.blocks[next] = 2;
// 如果是空白格,则蛇的长度应该保持不变,需要弹出蛇的尾部下标
if (nextType === 0) {
this.snakes.shift();
} else {
// 如果是虫子格,则虫子数组会过滤掉被吃的虫子
this.worms = this.worms.filter((x) => x !== next);
}
// 蛇尾部经过后一定是土地格子了,好好思考下
this.blocks[tail] = 0;
// 绘制
this.paint();
}
},
}
</script>
复制代码
蛇是动起来了,但是好像有点呆,不会拐弯,并且我们发现它会穿过边界,这不是我们想要的效果。我们需要在界面上添加四个按钮,用来操控我们可爱的蛇蛇,绑定上各自的事件:
绘制上下左右键
<template>
<view class="content">
<view class="game-field">
<view class="block"
:style="{'background-image': bg(x, i),transform: rotate(90deg)}"
v-for="(x, i) in blocks" :key="i">
{{x}}
</view>
</view>
<view class="action-field">
<button @click="bindUp">上</button>
<view class="flex">
<button @click="bindLeft">左</button>
<button @click="bindRight">右</button>
</view>
<button @click="bindDown">下</button>
</view>
</view>
</template>
<script>
// ...
export default {
// ...
methods: {
bindUp() {
console.log('up')
},
bindDown() {
console.log('down')
},
bindLeft() {
console.log('left')
},
bindRight() {
console.log('right')
},
},
}
</script>
<style>
/* ... */
.flex {
display: flex;
width: 50%;
justify-content: space-between;
}
.action-field {
display: flex;
flex-direction: column;
width: 100%;
align-items: center;
}
</style>
复制代码
emmmm
我们的蛇只会往右走,我们在toWards
方法里的next
赋值加个判断方向的逻辑:
methods: {
// ...
toWards() {
// ...
let next;
switch (this.direction) {
case "up":
next = head - 10;
break;
case "down":
next = head + 10;
break;
case "left":
next = head - 1;
break;
case "right":
next = head + 1;
break;
}
// ...
this.paint();
},
bindUp() {
this.direction = 'up';
},
bindDown() {
this.direction = 'down';
},
bindLeft() {
this.direction = 'left';
},
bindRight() {
this.direction = 'right';
},
}
复制代码
妈耶!蛇头方向不对啊。之前粗暴的固定旋转90度是不对的,这个背景图的旋转需要具体问题具体分析呢,新增一个计算旋转的方法,此方法需要好好思考:
<template>
<view class="content">
<view class="game-field">
<view class="block"
:style="{'background-image': bg(x, i),
transform: `rotate(${calcRotate(x, i)}deg)`}"
v-for="(x, i) in blocks" :key="i">
{{x}}
</view>
</view>
<view class="action-field">
<button @click="bindUp">上</button>
<view class="flex">
<button @click="bindLeft">左</button>
<button @click="bindRight">右</button>
</view>
<button @click="bindDown">下</button>
</view>
</view>
</template>
<script>
// ...
export default {
// ...
methods: {
// ...
calcRotate(type, index) {
let rotate = 0;
switch (type) {
case 0: // 地板
rotate = 0;
break;
case 1: // 虫子
rotate = 0;
break;
case 2: // 蛇
let length = this.snakes.length;
let head = this.snakes[length - 1];
let tail = this.snakes[0];
// 尾巴的前一个
let tailPre = this.snakes[1];
// 身子的前一个
let bodyPre = this.snakes[this.snakes.indexOf(index) + 1];
if (index === head) {
if (this.direction === "right") {
rotate = 90;
} else if (this.direction === "down") {
rotate = 180;
} else if (this.direction === "left") {
rotate = 270;
} else {
rotate = 0;
}
} else if (index === tail) {
if (tailPre - 1 === tail) {
// 向右走的
rotate = 90;
} else if (tailPre - 10 === tail) {
// 向下走的
rotate = 180;
} else if (tailPre + 1 === tail) {
// 向左走的
rotate = 270;
} else {
// 向上走的
rotate = 0;
}
} else {
if (bodyPre - 1 === index) {
// 向右走的
rotate = 90;
} else if (bodyPre - 10 === index) {
// 向下走的
rotate = 180;
} else if (bodyPre + 1 === index) {
// 向左走的
rotate = 270;
} else {
// 向上走的
rotate = 0;
}
}
break;
}
return rotate;
},
},
}
</script>
复制代码
看,它变得像那么回事了,我们的蛇现在还不会死,我们需要设置它撞到边界,或者自身会死,也就是游戏结束。
<script>
// ...
export default {
// ...
methods: {
// ...
toWards() {
if (this.snakes.length === 100) {
alert("你赢了!");
clearInterval(this.timer);
return;
}
let head = this.snakes[this.snakes.length - 1];
let tail = this.snakes[0];
let next;
switch (this.direction) {
case "up":
next = head - 10;
break;
case "down":
next = head + 10;
break;
case "left":
next = head - 1;
break;
case "right":
next = head + 1;
break;
}
let gameover = this.checkGame(next);
if (gameover) {
console.log("游戏结束");
clearInterval(this.timer);
} else {
// 游戏没结束
this.snakes.push(next);
let nextType = this.blocks[next];
this.blocks[next] = 2;
// 如果是空白格
if (nextType === 0) {
this.snakes.shift();
} else {
// 如果是虫子格
this.worms = this.worms.filter((x) => x !== next);
}
this.blocks[tail] = 0;
this.paint();
}
},
checkGame(next) {
let gameover = false;
let isSnake = this.snakes.indexOf(next) > -1;
if (isSnake) {
gameover = true;
}
switch (this.direction) {
case "up":
if (next < 0) {
gameover = true;
}
break;
case "down":
if (next >= 100) {
gameover = true;
}
break;
case "left":
if (next % 10 === 9) {
gameover = true;
}
break;
case "right":
if (next % 10 === 0) {
gameover = true;
}
break;
}
return gameover;
},
},
}
</script>
复制代码
撞到上边缘,挂了!
随机生成虫子
На данный момент мы написали три дохлых жука.На самом деле наши жуки генерируются случайным образом, и после поедания одного сгенерируется другой, и жук не появится на месте змеи:
methods: {
// ...
toWards() {
if (this.snakes.length === 100) {
alert("你赢了!");
clearInterval(this.timer);
return;
}
let head = this.snakes[this.snakes.length - 1];
let tail = this.snakes[0];
let next;
switch (this.direction) {
case "up":
next = head - 10;
break;
case "down":
next = head + 10;
break;
case "left":
next = head - 1;
break;
case "right":
next = head + 1;
break;
}
let gameover = this.checkGame(next);
if (gameover) {
console.log("游戏结束");
clearInterval(this.timer);
} else {
// 游戏没结束
this.snakes.push(next);
let nextType = this.blocks[next];
this.blocks[next] = 2;
// 如果是空白格
if (nextType === 0) {
this.snakes.shift();
} else {
// 如果是虫子格
this.worms = this.worms.filter((x) => x !== next);
let nextWorm = this.createWorm();
this.worms.push(nextWorm);
}
this.blocks[tail] = 0;
this.paint();
}
},
// 生成下一只虫子
createWorm() {
let blocks = Array.from({length: 100}, (v, k) => k);
let restBlocks = blocks.filter(x => this.snakes.indexOf(x) < 0);
let worm = restBlocks[Math.floor(Math.random() * restBlocks.length)];
return worm;
},
}
复制代码
Детальная оптимизация
Удалите границы сетки и числа в сетке:
На данный момент основные функции Snake завершены, и поздравляем студентов, которые знают только div. В будущем будут обновления, ставьте лайк и подписывайтесь
Функция списка дел
- Может двигаться вверх, вниз, влево и вправо
- Хит четыре стороны игра окончена
- Игра окончена
- Змеи будут расти боком после поедания жуков
- Поедание жука создаст случайный жук
- Рассчитать длину текущей змеи
- Добавлены взрывающиеся ошибки
- Земля загрязнена после взрыва жука
- Поразите загрязненную землю, игра окончена
- добавить пользовательскую систему
- Добавить таблицу лидеров
Особая благодарность
@大 красивая старая обезьяна @Sophora . Спасибо г-ну Дашуаю за руководство командой, оказание материальной и технической поддержки, а также спасибо товарищам по команде.
Адрес исходного кода , комментарии приветствуются, если вы чувствуете себя хорошо, нажмите звездочку. Пока, Цзянху~