目录
前言
❤️欢迎来到我的博客,今天,我要向大家介绍一款前端游戏项目——带商店和可设置的贪吃蛇大作战。此项目总计耗时一天完成,主要是帮我那不争气的表弟写的大一的期末项目。现在免费分享给大家。
功能介绍
此游戏的玩法是控制蛇吃食物并躲避地雷,地雷会随着时间增加越来越多,吃到食物则可加分并减少场上的一颗地雷。
分数可以在商店购买物品和属性。在游玩时不要光顾着吃食物哦,如果你撞到了地雷则游戏直接结束,不过,如果你在商店购买了“宝珠”则可以抵消一次死亡。
如果觉得难度有点大可以在游戏中按下ESC键可,进到设置界面中关闭地雷功能
游戏用到到语言和框架:
- vue
- js+ts
- html
- css
游戏截图
主界面
商店-皮肤
商店-道具
查看快捷键
地雷设置
代码
代码结构
主要代码
// index.vue
<template>
<div class="game">
<div class="game_header">
<div class="game_shop" @click="isShowStore = true">商店</div>
<div class="game_hear_font">贪吃蛇大作战</div>
<div class="game_score">得分:{{ scoreNum }} 分</div>
<div class="game_speed">速度:{{ level }} 级</div>
<div class="game_deposits">宝珠:{{ deposits }} 颗</div>
</div>
<div id="game_body" class="game_body">
<div class="game_pause font_css" v-show="isPause">暂停中,按空格键继续</div>
<div class="game_over" v-show="gameOver">游戏失败,按Enter键重新开始</div>
<div class="game_init font_css" v-show="isInit">按任意方向键开始游戏</div>
<div class="snake" v-for="(item, index) in snakeNums" :key="index">
<div v-if="index === 0" style="z-index: 2; position: absolute">
<img
v-show="currentDirection === 'ArrowUp' || currentDirection === ''"
style="height: 20px; width: 20px"
src="@/assets/img/up.png"
alt=""
/>
<img
v-show="currentDirection === 'ArrowDown'"
style="height: 20px; width: 20px"
src="@/assets/img/down.png"
alt=""
/>
<img
v-show="currentDirection === 'ArrowLeft'"
style="height: 20px; width: 20px"
src="@/assets/img/left.png"
alt=""
/>
<img
v-show="currentDirection === 'ArrowRight'"
style="height: 20px; width: 20px"
src="@/assets/img/right.png"
alt=""
/>
</div>
<div v-else style="z-index: 1; position: absolute">
<img style="height: 20px; width: 20px" src="@/assets/img/body.png" alt="" />
</div>
</div>
</div>
<game-setting
:visible="isShowSetting"
@clickCheck="isCheck = !isCheck"
@close="isShowSetting = false"
:is-check="isCheck"
/>
<game-store
:visible="isShowStore"
@close="isShowStore = false"
@purchase="handlePurchase"
:scoreNum="scoreNum"
@setColor="setColor"
/>
</div>
</template>
<script lang="ts" setup>
import { computed, inject, onMounted, onUnmounted, ref, watch } from 'vue';
import GameSetting from '@/views/componets/GameSetting.vue';
import GameStore from '@/views/componets/GameStore.vue';
import message from '@/components/Message/message';
const snakeNums = ref(10);
const scoreNum = ref(0);
const timer: any = ref();
const currentDirection = ref(''); // 前进方向
const isPause = ref(false); // 暂停标识
const gameOver = ref(false); // 游戏失败标识
const isInit = ref(true); // 初始话标识
const isShowSetting = ref(false); // 显示设置框
const isShowStore = ref(false); // 显示游戏商店
const isCheck = ref(true); //是否开启地雷
const reloadPage: any = inject('reloadPage'); // 获取重置页面的方法
function clickKeyBoard(event) {
// 只有按下方向键、空格和Enter键才进行后面的操作,不然在移动中如果连续按下其他无关按键也会进行判断,占用资源,导致移动变卡顿
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Enter', ' ', 'Escape', 'p'].includes(event.key)) {
return;
}
if (event.key === 'Escape') {
// 商店弹框打开时,不能打开设置
if (isShowStore.value) {
return;
}
isShowSetting.value = true;
if (!isInit.value && !gameOver.value) {
isPause.value = true;
if (timer.value) {
clearTimeout(timer.value);
}
}
return;
}
if (event.key === 'p') {
// 设置弹框打开时,不能打开商店
if (isShowSetting.value) {
return;
}
isShowStore.value = !isShowStore.value;
if (!isInit.value && !gameOver.value) {
isPause.value = true;
if (timer.value) {
clearTimeout(timer.value);
}
}
return;
}
// 游戏未开始或者游戏失败时,暂停无效
if (event.key === ' ' && !isInit.value && !gameOver.value) {
isPause.value = !isPause.value;
gameOver.value = false;
if (timer.value) {
clearTimeout(timer.value);
}
if (isPause.value) {
return;
}
}
// 如果按的是方向键,存储前进的方向,为了点击继续后仍然按照原来的方向移动,同时退出初始化状态
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key) && !isPause.value && !gameOver.value) {
currentDirection.value = event.key;
isInit.value = false;
}
// 游戏没有暂停、没有失败时按方向键才有用
if (
['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(currentDirection.value) &&
!isPause.value &&
!gameOver.value
) {
handleMover();
}
// 当游戏失败时,按Enter键重新开始游戏
if (gameOver.value && event.key === 'Enter') {
reloadPage();
return;
}
}
// 移动相关逻辑
const speed = ref(100);
function handleMover() {
if (timer.value) {
clearTimeout(timer.value);
}
timer.value = setInterval(() => {
let snakes: any = document.getElementsByClassName('snake');
let lodSnakes = [];
for (let i = 0; i < snakes.length; i++) {
lodSnakes.push({
style: {
top: snakes[i].style.top,
left: snakes[i].style.left,
visibility: snakes[i].style.visibility
}
});
}
// 向上移动
if (currentDirection.value === 'ArrowUp') {
// 碰到上边界,游戏失败
if (Number(lodSnakes[0].style.top.replace('px', '')) - 20 < 0) {
clearTimeout(timer.value);
gameOver.value = true;
return;
}
//
for (let i = 0; i < snakes.length; i++) {
if (i === 0) {
snakes[0].style.top = Number(lodSnakes[0].style.top.replace('px', '')) - 20 + 'px';
} else {
snakes[i].style.top = lodSnakes[i - 1].style.top;
snakes[i].style.left = lodSnakes[i - 1].style.left;
}
}
// 向下移动
} else if (currentDirection.value === 'ArrowDown') {
// 碰到下边界,游戏失败
if (Number(lodSnakes[0].style.top.replace('px', '')) + 20 > 780) {
clearTimeout(timer.value);
gameOver.value = true;
return;
}
for (let i = 0; i < snakes.length; i++) {
if (i === 0) {
snakes[0].style.top = Number(lodSnakes[0].style.top.replace('px', '')) + 20 + 'px';
} else {
snakes[i].style.top = lodSnakes[i - 1].style.top;
snakes[i].style.left = lodSnakes[i - 1].style.left;
}
}
// 向左移动
} else if (currentDirection.value === 'ArrowLeft') {
// 碰到左边界,游戏失败
if (Number(lodSnakes[0].style.left.replace('px', '')) - 20 < 0) {
clearTimeout(timer.value);
gameOver.value = true;
return;
}
for (let i = 0; i < snakes.length; i++) {
if (i === 0) {
snakes[0].style.left = Number(lodSnakes[0].style.left.replace('px', '')) - 20 + 'px';
} else {
snakes[i].style.top = lodSnakes[i - 1].style.top;
snakes[i].style.left = lodSnakes[i - 1].style.left;
}
}
// 向右移动
} else if (currentDirection.value === 'ArrowRight') {
// 碰到右边界,游戏失败
if (Number(lodSnakes[0].style.left.replace('px', '')) + 20 > 780) {
clearTimeout(timer.value);
gameOver.value = true;
return;
}
for (let i = 0; i < snakes.length; i++) {
if (i === 0) {
snakes[0].style.left = Number(lodSnakes[0].style.left.replace('px', '')) + 20 + 'px';
} else {
snakes[i].style.top = JSON.parse(JSON.stringify(lodSnakes[i - 1].style.top));
snakes[i].style.left = JSON.parse(JSON.stringify(lodSnakes[i - 1].style.left));
}
}
}
}, speed.value);
}
// 添加食物
function addFood(number) {
let body: any = document.getElementById('game_body');
for (let i = 0; i < number; i++) {
let food = document.createElement('div');
food.className = 'food';
food.style.height = '20px';
food.style.width = '20px';
food.style.position = 'absolute';
food.style.borderRadius = '50%';
food.style.backgroundColor = '#f6d77e';
food.style.left = 20 * Math.floor(Math.random() * 40) + 'px';
food.style.top = 20 * Math.floor(Math.random() * 40) + 'px';
body.appendChild(food);
}
}
// 添加地雷
function addMine(number) {
let body: any = document.getElementById('game_body');
for (let i = 0; i < number; i++) {
let mine = document.createElement('div');
mine.className = 'mine';
mine.style.height = '20px';
mine.style.width = '20px';
mine.style.position = 'absolute';
mine.style.borderRadius = '50%';
mine.style.backgroundColor = 'black';
mine.style.left = 20 * Math.floor(Math.random() * 40) + 'px';
mine.style.top = 20 * Math.floor(Math.random() * 40) + 'px';
body.appendChild(mine);
}
}
// 监听地雷勾选框
watch(
() => isCheck.value,
(val) => {
let mines: any = document.getElementsByClassName('mine');
for (let i = 0; i < mines.length; i++) {
if (val) {
mines[i].style.display = '';
} else {
// 隐藏地雷
mines[i].style.display = 'none';
}
}
}
);
function handlePurchase(item) {
if (item === '宝珠') {
scoreNum.value -= 200;
deposits.value++;
} else if (item === '速度') {
if (speed.value === 25) {
message({ type: 'warn', text: '已达到速度上限制' });
return;
}
scoreNum.value -= 100;
speed.value /= 2;
}
}
const deposits = ref(0);
const level = computed(() => {
if (speed.value === 100) {
return 1;
}
if (speed.value === 50) {
return 2;
} else {
return 3;
}
});
function setColor(type, color) {
if (type === 'food') {
let foods: any = document.getElementsByClassName('food');
for (let i = 0; i < foods.length; i++) {
foods[i].style.backgroundColor = color;
}
} else {
let mines: any = document.getElementsByClassName('mine');
for (let i = 0; i < mines.length; i++) {
mines[i].style.backgroundColor = color;
}
}
}
const startListener = ref();
const mineListener = ref();
onMounted(() => {
window.addEventListener('keyup', clickKeyBoard);
let snakes: any = document.getElementsByClassName('snake');
for (let i = 0; i < snakes.length; i++) {
snakes[i].style.left = 100 + 'px';
snakes[i].style.top = 100 + i * 20 + 'px';
snakes[i].style.visibility = 'unset';
}
addFood(5);
addMine(2);
// 监听间隔需要小于或等于蛇移动的间隔
startListener.value = setInterval(() => {
if (isInit.value === false && isPause.value === false && gameOver.value === false) {
let body = document.getElementById('game_body');
let foods: any = document.getElementsByClassName('food');
let mines: any = document.getElementsByClassName('mine');
// 开启地雷时,如果蛇头碰撞到地雷游戏结束
if (isCheck.value) {
for (let i = 0; i < mines.length; i++) {
if (mines[i].style.left === snakes[0].style.left && mines[i].style.top === snakes[0].style.top) {
if (deposits.value > 0) {
deposits.value--;
} else {
clearTimeout(timer.value);
gameOver.value = true;
}
return;
}
}
}
for (let i = 0; i < foods.length; i++) {
if (foods[i].style.left === snakes[0].style.left && foods[i].style.top === snakes[0].style.top) {
let snakes: any = document.getElementsByClassName('snake');
// 添加的身体先隐藏,否则会因为渲染导致页面闪一下
snakes[snakes.length - 1].style.visibility = 'unset';
body.removeChild(foods[i]);
// 每吃掉一个食物,则减少一个地雷
if (mines.length > 0) {
body.removeChild(mines[mines.length - 1]);
}
scoreNum.value += 10;
}
}
}
}, 100);
// 监听间隔需要小于或等于蛇移动的间隔
startListener.value = setInterval(() => {
if (isInit.value === false && isPause.value === false && gameOver.value === false) {
let foods: any = document.getElementsByClassName('food');
if (foods.length < 10) {
addFood(5);
}
}
}, 5000);
// 监听间隔需要小于或等于蛇移动的间隔
mineListener.value = setInterval(() => {
if (isInit.value === false && isPause.value === false && gameOver.value === false) {
addMine(3);
}
}, 5000);
});
onUnmounted(() => {
window.removeEventListener('keyup', clickKeyBoard);
if (timer.value) {
clearTimeout(timer.value);
}
clearTimeout(startListener.value);
clearTimeout(mineListener.value);
});
</script>
<style scoped lang="less">
.game {
border: 2px solid sandybrown;
border-radius: 1%;
height: 900px;
width: 800px;
background-color: lavender;
position: relative;
}
.game_header {
position: relative;
height: 100px;
width: 100%;
border-bottom: 2px solid sandybrown;
display: flex;
align-items: center;
justify-content: center;
.game_shop {
position: absolute;
left: 20px;
height: 30px;
width: 50px;
text-align: center;
line-height: 30px;
top: 50%;
color: white;
cursor: pointer;
background-color: yellowgreen;
border-radius: 4px;
border: 1px solid yellow;
&:hover {
color: saddlebrown;
}
}
.game_hear_font {
font-size: 25px;
color: rebeccapurple;
}
.game_score {
position: absolute;
right: 100px;
top: 20%;
width: 120px;
}
.game_speed {
position: absolute;
right: 100px;
top: 45%;
width: 120px;
}
.game_deposits {
position: absolute;
right: 100px;
top: 70%;
width: 120px;
}
}
.game_body {
position: relative;
height: 800px;
width: 100%;
border-radius: 0 0 1% 1%;
display: flex;
align-items: center;
justify-content: center;
}
.game_init {
font-size: 30px;
position: absolute;
color: white;
z-index: 999;
}
@keyframes stream {
//匀速流动
0% {
background-position: 0 0;
}
100% {
background-position: -100% 0;
}
}
.font_css {
background-image: -webkit-linear-gradient(
left,
green,
yellow,
pink,
blue,
red 25%,
green 35%,
blue 50%,
yellow 60%,
red 75%,
pink 85%,
blue 100%
); // 括号内可添加多种颜色,多种百分比
-webkit-text-fill-color: transparent; // 颜色填充 透明
-webkit-background-clip: text; // 背景颜色绘制区域
animation: stream 5s infinite linear; //流动 15秒 循环 直线
background-size: 200% 100%;
}
.game_pause {
font-size: 30px;
position: absolute;
z-index: 999;
color: white;
}
.game_over {
font-size: 30px;
position: absolute;
z-index: 999;
background-image: -webkit-linear-gradient(
left,
green,
yellow,
pink,
blue,
red 25%,
green 35%,
blue 50%,
yellow 60%,
red 75%,
pink 85%,
blue 100%
); // 括号内可添加多种颜色,多种百分比
-webkit-text-fill-color: transparent; // 颜色填充 透明
-webkit-background-clip: text; // 背景颜色绘制区域
animation: cartoon 2s infinite linear;
background-size: 200% 100%;
}
@keyframes cartoon {
0% {
transform: rotate(0deg);
background-position: 0 0;
}
25% {
transform: rotate(6deg);
}
50% {
transform: rotate(0deg);
}
75% {
transform: rotate(-6deg);
}
100% {
transform: rotate(0deg);
background-position: -100% 0;
}
}
.snake {
position: absolute;
visibility: hidden;
}
.food {
position: absolute;
height: 20px;
width: 20px;
border-radius: 50%;
background-color: #f6d77e;
}
</style>
总结
贪吃蛇这个游戏的难度对于刚开始学习前端的小伙伴是刚刚好的,这个我的这个版本还有很多功能可以完善。比如
- 商店里的皮肤中可以加上蛇的皮肤。
- 道具你也可以按自己的想法加一些好玩的功能。
- 可以加入多人对战。
- 加入人机。
希望这个游戏可以帮助到大家,感谢观看❤️,日后会推出更多的前端小游戏,请大家敬请期待哦🙏~