贪吃蛇(Typescript)
效果简单展示
项目环境
package.json
{
"name": "snack",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack serve --open chrome.exe"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.12.9",
"@babel/preset-env": "^7.12.7",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^3.0.0",
"core-js": "^3.8.0",
"css-loader": "^6.7.1",
"html-webpack-plugin": "^4.5.0",
"less": "^4.1.2",
"less-loader": "^11.0.0",
"postcss": "^8.4.13",
"postcss-loader": "^6.2.1",
"postcss-preset-env": "^7.5.0",
"style-loader": "^3.3.1",
"ts-loader": "^8.0.11",
"typescript": "^4.1.2",
"webpack": "^5.6.0",
"webpack-cli": "^4.2.0",
"webpack-dev-server": "^3.11.0"
}
}
tsconfig.json,作用是TS编译配置,如果这里不懂可以看看我这篇文章
{
"compilerOptions": {
"module": "ES2015",
"target": "ES2015",
"strict": true,
"noEmitOnError": true
}
}
webpack.config.js
// 引入一个包
const path = require('path');
// 引入html插件
const HTMLWebpackPlugin = require('html-webpack-plugin');
// 引入clean插件
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// webpack中的所有的配置信息都应该写在module.exports中
module.exports = {
// 指定入口文件
entry: "./src/index.ts",
// 指定打包文件所在目录
output: {
// 指定打包文件的目录
path: path.resolve(__dirname, 'dist'),
// 打包后文件的文件
filename: "bundle.js",
// 告诉webpack不使用箭头
environment:{
arrowFunction: false
}
},
// 指定webpack打包时要使用模块
module: {
// 指定要加载的规则
rules: [
{
// test指定的是规则生效的文件
test: /\.ts$/,
// 要使用的loader
use: [
// 配置babel
{
// 指定加载器
loader:"babel-loader",
// 设置babel
options: {
// 设置预定义的环境
presets:[
[
// 指定环境的插件
"@babel/preset-env",
// 配置信息
{
// 要兼容的目标浏览器
targets:{
"chrome":"58",
"ie":"11"
},
// 指定corejs的版本
"corejs":"3",
// 使用corejs的方式 "usage" 表示按需加载
"useBuiltIns":"usage"
}
]
]
}
},
'ts-loader'
],
// 要排除的文件
exclude: /node-modules/
},
{
test: /\.less$/,
use: [
"style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
[
"postcss-preset-env",
{
browser: 'last 2 versions'
}
]
]
}
}
},
"less-loader"
]
}
]
},
// 配置Webpack插件
plugins: [
new CleanWebpackPlugin(),
new HTMLWebpackPlugin({
// title: "这是一个自定义的title"
template: "./src/index.html"
}),
],
// 用来设置引用模块
resolve: {
extensions: ['.ts', '.js']
},
mode: 'development'
};
要完成本项目,首先要声明一个html文件和样式文件作为贪吃蛇的游戏页面,样式文件采用less预编译语言。
index.html
<!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>
</head>
<body>
<div id="main">
<!-- 蛇移动的区域 -->
<div id="stage">
<!-- 蛇 -->
<div id="snack">
<div></div>
</div>
<!-- 食物 -->
<div id="food">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
<!-- 计分和水平区域 -->
<div id="score-panel">
<div>
SCORE:<span id="score">0</span>
</div>
<div>
level:<span id="level">1</span>
</div>
</div>
</div>
</body>
</html>
style.less
@bg-color: #b7d4a8;
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
#main{
width: 360px;
height: 420px;
background-color: @bg-color;
margin: 100px auto;
border: 10px solid black;
border-radius: 10px;
font: bold 20px "Courier";
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
#stage{
width: 304px;
height: 304px;
border: 2px solid black;
position: relative;
#snack {
&>div {
width: 10px;
height: 10px;
background-color: #000;
border: 1px solid @bg-color;
position: absolute;
}
}
#food {
width: 10px;
height: 10px;
display: flex;
position: absolute;
left: 40px;
justify-content: space-between;
align-content: space-between;
flex-wrap: wrap;
&>div{
width: 4px;
height: 4px;
background-color: #000;
transform: rotateZ(45deg);
}
}
}
#score-panel{
width: 300px;
display: flex;
justify-content: space-between;
}
}
因为TS是静态类型语言,所以它对类的支持很好,我们就采用类的思想进行编写。首先要思考我们需要四个类:
- Control类:是整个项目中的中枢控制,调用另外类的方法完成贪吃蛇效果。
- Food类:食物类,定义食物的属性即操作它的方法。
- Snack类:蛇类,定义了蛇的属性和操作方法。
- ScorePanel类:计分和level类。
类
Food类
属性:
- element:食物的真实dom元素
方法:
-
change:随机改变食物的横纵坐标
注意:因为蛇移动的范围是一个300 X 300的区域,蛇是10 X 10 的,而我们使用offsetLeft和offsetTop获取他的横纵坐标的,所以随机数应该在[0, 290]内。
class Food {
constructor() {
// 这里会报错,因为不一定能拿到该元素,加个!表示一定能拿到
this.element = document.getElementById('food')!;
}
// food真实dom元素
element: HTMLElement;
// 属性访问器,访问食物真实dom元素的横纵坐标
get X() {
return this.element.offsetLeft;
}
get Y() {
return this.element.offsetTop;
}
// 产生随机数改变食物的横纵坐标
change() {
// 产生随机数
let Top = Math.round(Math.random() * 29) * 10;
let Left = Math.round(Math.random() * 29) * 10;
// 改变元素坐标
this.element.style.left = Left + 'px';
this.element.style.top = Top + 'px';
}
}
export default Food;
snack类
属性:
- snackHead: 蛇头的真实dom元素
- snackBody: 蛇身的真实dom元素
- snack: 整个蛇的真实dom元素
方法:
- addBody:蛇身增加一个元素
- moveBody:随蛇头移动蛇身
- checkHeadBody:判断蛇头是否触碰蛇身
class Snack {
constructor() {
this.snack = document.getElementById('snack')!;
this.snackHead = document.querySelector('#snack > div')!;
this.snackBody = this.snack.getElementsByTagName('div')!;
}
// 蛇头的真实dom元素
snackHead: HTMLElement;
// 蛇身的真实dom元素
snackBody: HTMLCollection;
// 整个蛇的真实dom元素
snack: HTMLElement;
// 访问蛇头的横纵坐标
get X() {
return this.snackHead.offsetLeft;
}
get Y() {
return this.snackHead.offsetTop;
}
// 设置蛇头的横纵坐标
set X(left) {
// 若蛇头横坐标与上次比无变化就不做操作
if (this.X === left) return;
// 如果蛇头坐标触碰边界就抛出错误
if (left < 0 || left > 290) {
throw Error();
}
// 限制蛇不能直接掉头穿过自己的身体
if (this.snackBody[1] && (this.snackBody[1] as HTMLElement).offsetLeft === left) {
if (left > this.X) {
this.X -= 10;
} else {
this.X += 10;
}
}
// 蛇身随蛇头移动
this.moveBody();
// 修改蛇头的横坐标
this.snackHead.style.left = left + 'px';
// 限制蛇头不能触碰自己的身体
this.checkHeadBody();
}
set Y(top) {
// 若蛇头横纵坐标与上次比无变化就不做操作
if (this.Y === top) return;
// 如果蛇头坐标触碰边界就抛出错误
if (top < 0 || top > 290) {
throw Error();
}
// 限制蛇不能直接掉头穿过自己的身体
if (this.snackBody[1] && (this.snackBody[1] as HTMLElement).offsetTop === top) {
if (top > this.Y) {
this.Y -= 10;
} else {
this.Y += 10;
}
}
this.moveBody();
this.snackHead.style.top = top + 'px';
this.checkHeadBody();
}
// 吃到食物蛇身增加一个元素
addBody() {
this.snack.insertAdjacentHTML("beforeend", "<div></div>")
}
// 移动身体
moveBody() {
for (let i = this.snackBody.length - 1; i > 0; i--) {
(this.snackBody[i] as HTMLElement).style.left = (this.snackBody[i - 1] as HTMLElement).offsetLeft + 'px';
(this.snackBody[i] as HTMLElement).style.top = (this.snackBody[i - 1] as HTMLElement).offsetTop + 'px';
}
}
// 限制蛇头不能触碰自己的身体
checkHeadBody() {
for (let i = this.snackBody.length - 1; i > 0; i--) {
if (this.X === (this.snackBody[i] as HTMLElement).offsetLeft && this.Y === (this.snackBody[i] as HTMLElement).offsetTop) {
throw Error();
}
}
}
}
export default Snack;
scorePanel
属性:
- score:分数
- level:等级
- scoreEle:显示分数的真实dom元素
- levelEle:显示等级的真实dom元素
- maxLevel:最大等级数
- upScore:每个等级最大分数,每获得这么多分数就升一级
方法:
- addScore:加分
- addLevel:加等级
class ScorePanel {
constructor(maxLevel = 10, upScore = 5) {
this.levelEle = document.getElementById('level')!;
this.scoreEle = document.getElementById('score')!;
this.maxLevel = maxLevel;
this.upScore = upScore;
}
// 分数
score = 0;
// 等级
level = 1;
// 显示分数的真实dom元素
scoreEle: HTMLElement;
// 显示等级的真实dom元素
levelEle: HTMLElement;
// 最大等级数
maxLevel: number;
// 每个等级最大分数,每获得这么多分数就升一级
upScore: number;
// 加分
addScore() {
this.scoreEle.innerHTML = ++this.score + '';
if (this.score % this.upScore === 0) {
this.addLevel();
}
}
// 加等级
addLevel() {
if (this.level < this.maxLevel) {
this.levelEle.innerHTML = ++this.level + '';
}
}
}
export default ScorePanel;
Control类
属性:
- Snack:蛇类的实例
- ScorePanel:分数等级的实例
- Food:食物的实例
- direction:蛇移动的方向
- isLive:游戏是否结束
方法:
- init:初始化
- keyHandleDown:键盘按下的回调函数
- run:蛇移动的逻辑控制
- checkEat:判断蛇是否吃到食物
import Snack from "./snack";
import ScorePanel from "./ScorePanel";
import Food from "./Food";
class Control{
// 蛇类的实例
Snack: Snack;
// 分数等级的实例
ScorePanel: ScorePanel;
// 食物的实例
Food: Food;
// 蛇移动的方向
direction: string = '';
// 游戏是否结束
isLive: boolean;
constructor() {
this.Food = new Food();
this.Snack = new Snack();
this.ScorePanel = new ScorePanel();
this.isLive = true;
this.init();
}
// 初始化
init() {
document.addEventListener('keydown', this.keyHandleDown.bind(this))
this.run();
}
// 键盘按下的回调函数
keyHandleDown(event:KeyboardEvent) {
this.direction = event.key;
}
// 蛇移动的逻辑控制
run() {
let X = this.Snack.X;
let Y = this.Snack.Y;
// 判断蛇移动的方向
switch (this.direction) {
case "ArrowUp":
Y -= 10;
break;
case "ArrowDown":
Y += 10;
break;
case "ArrowLeft":
X -= 10;
break;
case "ArrowRight":
X += 10;
break;
}
// 判断蛇是都吃到食物
this.checkEat(X, Y)
// 修改蛇的坐标
try {
this.Snack.X = X;
this.Snack.Y = Y;
} catch {
// 若修改报错则结束游戏
alert('GAME OVER!!!');
this.isLive = false;
}
// 使蛇能够自己连续移动
this.isLive && setTimeout(this.run.bind(this), 500 - (this.ScorePanel.level - 1) * 50)//
}
// 判断蛇是否吃到食物
checkEat(X: number, Y: number) {
if (X === this.Food.X && Y === this.Food.Y) {
//若蛇吃到食物就改变食物位置,增加分数,蛇身加一个元素
this.Food.change();
this.ScorePanel.addScore();
this.Snack.addBody();
}
}
}
export default Control;
项目的入口文件index.ts
// 引入样式文件
import '../style/index.less'
import Control from './control'
new Control();
项目运行流程
首先入口文件中实例了 Control 类,调用 Control 类的构造函数,构造函数又调用了 init 初始化函数,监听 keydown 事件,调用 run 方法,run 方法内会判断移动方向;调用 checkEat 检查是否吃到食物,若蛇吃到食物就改变食物位置,增加分数,蛇身加一个元素;使用 try,catch 修改坐标(蛇的移动,限制掉头和吃自己),若捕获到错误说明蛇撞墙了则结束游戏。
总结
其实贪吃蛇的实现并不难,关键是要以类的思想来编写代码,要区分每个类的作用以及它携带什么属性拥有什么方法是比较困难的。本项目中control类是控制中枢,它拥有snack、scorePanel和food类的实例,调用他们的属性和方法做一系列逻辑处理最终完成贪吃蛇游戏效果。