游戏效果如动图所示:蛇吃一个食物就涨一截,不能撞墙和撞自己,每10分升一级,level越高速度越快。
定义食物的类,Food.ts
// 定义食物的类
class Food {
// 定义一个属性表示食物所对应的元素
element: HTMLElement;
constructor() {
// 获取页面中的food元素并赋值给element属性
this.element = document.getElementById("food")!;
}
// 定义一个获取食物X轴坐标的方法
get X() {
return this.element.offsetLeft;
}
// 定义一个获取食物Y轴坐标的方法
get Y() {
return this.element.offsetTop;
}
// 修改食物的位置
change() {
// 生成一个随机位置
// 食物的位置最小是0 最大是290
// 蛇移动一次就是一格, 一格大小就是10, 所以要求食物的位置是整10
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;
定义计分板的类,ScorePanel.ts
// 定义表示记分牌的类
class ScorePanel {
// score 用来记录分数
score = 0;
// level 用来记录等级
level = 1;
// 分数和等级所在的元素, 在构造函数中进行初始化
scoreEle: HTMLElement;
levelEle: HTMLElement;
// 设置一个变量表示最大等级
maxLevel: number;
// 设置一个变量表示多少分时升级
upScore: number;
constructor(maxLevel: number = 10, upScore: number = 10) {
this.scoreEle = document.getElementById("score")!;
this.levelEle = document.getElementById("level")!;
this.maxLevel = maxLevel;
this.upScore = upScore;
}
// 设置一个加分的方法
addScore() {
// 使分数自增并更新dom
this.scoreEle.innerHTML = (++this.score).toString();
// 当分数达到一定值时,升级。默认是整除10时升级,也可以通过修改upScore的值来改变升级分数
if (this.score % this.upScore === 0) {
this.levelUp();
}
}
// 提升等级的方法
levelUp() {
if (this.level < this.maxLevel) {
this.levelEle.innerHTML = (++this.level).toString();
}
}
}
export default ScorePanel;
定义蛇的类,Snake.ts
class Snake {
// 表示蛇头的元素
head: HTMLElement;
// 蛇的身体(包括蛇头)
bodies: HTMLCollection;
// 获取蛇的容器
element: HTMLElement;
constructor() {
this.element = document.getElementById("snake")!;
this.head = document.querySelector("#snake > div") as HTMLElement;
this.bodies = this.element.getElementsByTagName("div");
}
// 获取蛇的x轴坐标(就是蛇头的坐标)
get X() {
return this.head.offsetLeft;
}
// 获取蛇的Y轴坐标
get Y() {
return this.head.offsetTop;
}
// 设置蛇头的坐标
set X(value) {
// 如果新值和旧值相同,则直接返回不再修改
if (this.X === value) {
return;
}
// X的值的合法范围0-290之间
if (value < 0 || value > 290) {
// 抛出异常, 进入判断说明蛇撞墙了
throw new Error("蛇撞墙了!");
}
// 修改x时,是在修改水平坐标,蛇在左右移动,蛇在向左移动时,不能向右掉头,反之亦然
if (
this.bodies[1] &&
(this.bodies[1] as HTMLElement).offsetLeft === value
) {
// console.log("水平方向发生了掉头");
// 如果发生了掉头,让蛇向反方向继续移动
if (this.X > value) {
// 如果新值value大于旧值X,则说明蛇在向右走,此时发生掉头,应该使蛇继续向左走
value = this.X + 10;
} else {
// 向左移动
value = this.X - 10;
}
}
// 移动身体
this.moveBody();
this.head.style.left = value + "px";
// 检查有没有撞到自己
this.checkHeadBody();
}
set Y(value) {
// 如果新值和旧值相同,则直接返回不再修改
if (this.Y === value) {
return;
}
// Y的值的合法范围0-290之间
if (value < 0 || value > 290) {
// 抛出异常, 进入判断说明蛇撞墙了
throw new Error("蛇撞墙了!");
}
// 修改y时,是在修改垂直坐标,蛇在上下移动,蛇在向上移动时,不能向下掉头,反之亦然
if (
this.bodies[1] &&
(this.bodies[1] as HTMLElement).offsetTop === value
) {
// console.log("水平方向发生了掉头");
// 如果发生了掉头,让蛇向反方向继续移动
if (value > this.Y) {
value = this.Y - 10;
} else {
value = this.Y + 10;
}
}
// 移动身体
this.moveBody();
this.head.style.top = value + "px";
// 检查有没有撞到自己
this.checkHeadBody();
}
// 蛇增加身体的方法
addBody(){
// 向element中添加一个div
this.element.insertAdjacentHTML("beforeend", "<div></div>");
}
// 添加一个蛇身体移动的方法
moveBody() {
/**
* 将后边的身体设置为前边身体的位置
* 举例子:
* 第4节 = 第3节的位置
* 第3节 = 第2节的位置
* 第2节 = 蛇头的位置
*/
// 遍历获取所有的身体
for (let i = this.bodies.length - 1; i > 0; i--) {
// 获取前边身体的位置
let X = (this.bodies[i - 1] as HTMLElement).offsetLeft;
let Y = (this.bodies[i - 1] as HTMLElement).offsetTop;
// 将值设置到当前身体上
(this.bodies[i] as HTMLElement).style.left = X + "px";
(this.bodies[i] as HTMLElement).style.top = Y + "px";
}
}
// 检查蛇头是否撞到身体的方法
checkHeadBody() {
// 获取所有的身体,检查其是否和蛇头的坐标重叠
for (let i = 1; i < this.bodies.length; i++) {
let bd = this.bodies[i] as HTMLElement;
if (this.X === bd.offsetLeft && this.Y === bd.offsetTop) {
// 进入判断说明蛇头撞到了身体,游戏结束。
throw new Error("撞到自己了!!!");
}
}
}
}
export default Snake;
定义游戏控制的类,GameControl.ts
// 引入其他的类
import Snake from "./Snake";
import Food from "./Food";
import ScorePanel from "./ScorePanel";
// 游戏控制器,控制所有的类
class GameControl {
// 蛇
snake: Snake;
// 食物
food: Food;
// 记分牌
scorePanel: ScorePanel;
// 创建一个属性来存储蛇的移动方向(也就是按键的方向)
direction: string = "";
// 创建一个属性用来记录游戏是否结束
isLive = true;
constructor() {
this.snake = new Snake();
this.food = new Food();
this.scorePanel = new ScorePanel(10, 10);
this.init();
}
// 游戏的初始化方法,调用后游戏开始
init() {
// 绑定键盘按键按下的事件
document.addEventListener("keydown", this.keydownHandler.bind(this));
// 调用run方法,使蛇移动
this.run();
}
// 创建一个键盘按下的响应函数
keydownHandler(event: KeyboardEvent) {
// 需要检查event.key的值是否合法(用户是否按了正确的按键)
this.direction = event.key;
}
// 创建一个控制蛇移动的方法
run() {
/*
* 根据方向(this.direction)来使蛇的位置改变
* 向上 top 减少
* 向下 top 增加
* 向左 left 减少
* 向右 left 增加
* */
// 获取蛇现在坐标
let X = this.snake.X;
let Y = this.snake.Y;
// 根据按键方向来修改X值和Y值
switch (this.direction) {
case "ArrowUp":
case "Up":
// 向上移动 top 减少
Y -= 10;
break;
case "ArrowDown":
case "Down":
// 向下移动 top 增加
Y += 10;
break;
case "ArrowLeft":
case "Left":
// 向左移动 left 减少
X -= 10;
break;
case "ArrowRight":
case "Right":
// 向右移动 left 增加
X += 10;
break;
}
// 检查蛇是否吃到了食物
this.checkEat(X, Y);
//修改蛇的X和Y值
try {
this.snake.X = X;
this.snake.Y = Y;
} catch (error: any) {
// 进入到catch,说明出现了异常,游戏结束,弹出一个提示信息
alert(error.message + " GAME OVER!");
// 将isLive设置为false
this.isLive = false;
}
// 开启一个定时调用
this.isLive &&
setTimeout(
this.run.bind(this),
300 - (this.scorePanel.level - 1) * 30
);
}
// 定义一个方法,用来检查蛇是否吃到食物
checkEat(X: number, Y: number) {
if (X === this.food.X && Y === this.food.Y) {
// 食物的位置要进行重置
this.food.change();
// 分数增加
this.scorePanel.addScore();
// 蛇要增加一节
this.snake.addBody();
}
}
}
export default GameControl;
另外,还有界面的样式,index.less
// 设置变量
@bg-color: #b7d4a8;
// 清除默认样式
* {
margin: 0;
padding: 0;
// 改变盒子模型的计算方式
box-sizing: border-box;
}
body {
font: bold 20px "Courier";
}
// 设置主窗口的样式
#main {
width: 360px;
height: 420px;
background-color: @bg-color;
margin: 100px auto;
border: 10px solid black;
border-radius: 10px;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
#stage {
width: 304px;
height: 304px;
border: 2px solid black;
// 开启相对定位
position: relative;
}
// 设置蛇的样式
#snake {
& > div {
width: 10px;
height: 10px;
background-color: #000;
border: 1px solid @bg-color;
// 开启绝对定位
position: absolute;
}
}
#food {
width: 10px;
height: 10px;
border: 1px solid @bg-color;
// 开启绝对定位
position: absolute;
left: 40px;
top: 100px;
// 开启弹性盒子
display: flex;
// flex-wrap: wrap;
// 设置主轴为横向排列,换行
flex-flow: row wrap;
justify-content: space-between;
align-content: space-between;
& > div {
width: 4px;
height: 4px;
background-color: #000;
transform: rotate(45deg);
}
}
// 记分牌
#score-panel {
width: 300px;
display: flex;
justify-content: space-between;
}
}
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,
const: 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/,
},
// 设置less文件的处理
{
test: /\.less$/,
use: [
// 将js中的css通过创建style标签添加到html文件中
"style-loader",
// 将css文件变成commonjs模块加载到js中
"css-loader",
// 引入postcss
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
[
"postcss-preset-env",
{
// 配置postcss环境
browsers: "last 2 versions",
},
],
],
},
},
},
// 将less文件编译成css文件
"less-loader",
],
},
],
},
// 配置Webpack插件
plugins: [
new CleanWebpackPlugin(),
new HTMLWebpackPlugin({
// title: "这是一个自定义的title"
template: "./src/index.html",
}),
],
// 用来设置引用模块
resolve: {
extensions: [".ts", ".js"],
},
};
package.json
{
"name": "snake",
"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": "^7.1.2",
"html-webpack-plugin": "^4.5.0",
"less": "^4.2.0",
"less-loader": "^12.2.0",
"postcss": "^8.4.38",
"postcss-loader": "^8.1.1",
"postcss-preset-env": "^9.5.14",
"style-loader": "^4.0.0",
"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
{
"compilerOptions": {
"module": "ES2015",
"target": "ES2015",
"strict": true,
"noEmitOnError": true
}
}