目录
一、项目搭建
1.1 初始化项目
二、项目界面布局
三、完成Food类
四、完成记分牌类
五、初步完成snake类
六、创建游戏控制器类 - 键盘事件
七、GameControl - 使蛇移动
八、蛇撞墙和吃食检测
一、项目搭建
1.1 初始化项目
1.使用init命令生成package.json文件
npm init -y
2.在根目录创建src文件夹、tsconfig.json文件、weboack.config.js文件
3.在src文件夹下创建index.ts、index.html、style文件夹、module文件夹
4.在style文件夹内创建index.less文件
5.运行以下命令,安装所需的18个依赖
npm i webpack webpack-cli webpack-server typescript ts-loader style-loader less-loader less css-loader postcss postcss-loader postcss-preset-env core-js html-webpack-plugin clean-webpack-plugin babel-loader @babel/preset-env @babel/core -D
安装完毕后,你的文件结构应该是这样
安装依赖说明
6.配置package.json
在"script"对象中,添加如下命令
"build": "webpack",
"start": "webpack server --open"
7.配置tsconfig.json
{
"compilerOptions": {
"module": "ES2015",
"target": "ES2015",
"strict": true,
"noEmitOnError": true
}
}
8.配置webpack.config.js
// 引入一个包
const path = require("path")
// 引入hmtl插件
const HtmlWebpackPlugin = require("html-webpack-plugin")
const { CleanWebpackPlugin } = require("clean-webpack-plugin")
module.exports = {
entry: "./src/index.ts",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
// 告诉webpack不适用箭头函数
environment: {
arrowFunction: false,
const: false,
},
},
mode: 'development' ,
// 指定webpack打包时使用的模块
module: {
// 指定要加载的规则
rules: [
{
// test指定规则生效的文件
test: /\.ts$/,
use: [
// 配置babel
{
// 指定加载器
loader: "babel-loader",
// 设置babel
options: {
// 设置定义的环境
presets: [
[
"@babel/preset-env",
{
// 要兼容的目标浏览器
targets: {
browsers: ["last 2 versions"],
ie: 11,
},
// 指定corejs的版本
corejs: 3,
// 使用corejs的方式,"usage"表示按需加载
useBuiltIns: "usage",
},
],
],
},
},
"ts-loader",
],
exclude: /node_modules/,
},
{
test: /\.less$/,
use: [
"style-loader",
"css-loader",
// 引入postcss
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [[
"postcss-preset-env",
{
browsers: ["last 2 versions"],
},
],]
},
},
},
"less-loader",
],
},
],
},
// 配置webpack插件
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: "./src/index.html",
filename: "index.html",
}),
],
// 用来设置模块
resolve: {
extensions: [".ts", ".js"],
},
}
二、项目界面布局
1.打开src文件夹下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="snake">
<!-- snake内部的div 表示蛇的各部分 -->
<div></div>
</div>
<!-- 设置食物 -->
<div id="food">
<!-- 添加4个div 设置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>
2.在index.ts中引入index.less
三、完成Food类
1.在modules文件夹中创建Food.ts文件
2.定义获取食物的x,y坐标的方法,修改食物的位置方法等
// 定义食物类Food
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;
四、完成记分牌类
1.在modules文件夹下创建ScorePanel.ts文件
2.定义属性:分数、等级、最大等级以及升级所需分数
3.定义方法:加分方法、提升等级方法
// 定义表示记分牌的类
class ScorePanel {
// score和level用来记录分数和等级
score: number = 0;
level: number = 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() {
this.scoreEle.innerHTML = ++this.score + "";
// 判断分数是多少
if (this.score % this.upScore === 0) {
this.levelUp();
}
}
// 提升等级的方法
levelUp() {
if (this.level < this.maxLevel) this.levelEle.innerHTML = ++this.level + "";
}
}
export default ScorePanel;
五、初步完成snake类
1.在modules文件夹中创建Snake.ts文件
2.定义属性:蛇头、蛇的身体
3.定义方法:获取蛇的x,y坐标方法、设置x,y坐标的方法,蛇的身体增加的方法
class Snake {
// 表示蛇头的元素
hand: HTMLElement;
// 蛇的身体(包括舌头)
bodies: HTMLCollection;
// 获取蛇的容器
element: HTMLElement;
constructor() {
this.element = document.getElementById("snake")!;
this.hand = document.querySelector("#snake > div")!;
this.bodies = this.element.getElementsByTagName("div")!;
}
// 获取蛇的x坐标
get X() {
return this.hand.offsetLeft;
}
// 获取蛇的y坐标
get Y() {
return this.hand.offsetTop;
}
set X(value) {
this.hand.style.left = `${value}px`;
}
set Y(value) {
this.hand.style.top = `${value}px`;
}
// 蛇增加身体的方法
addBody() {
// 向element中添加一个div
this.element.insertAdjacentHTML("beforeend", "<div></div>");
}
}
export default Snake;
六、创建游戏控制器类 - 键盘事件
1.在modules文件夹中创建GameControl.ts文件
2.引入其他3个文件
3.定义属性:蛇、食物、记分牌、移动方向
4.定义方法:游戏初始化、键盘按下事件
// 引入其他类
import Snake from "./Snake";
import Food from "./Food";
import ScorePanel from "./ScorePanel";
// 游戏控制器,控制其他的所有类
class GameControl {
// 定义三个属性
// 蛇
snake: Snake;
food: Food;
scorePanel: ScorePanel;
// 创建一个属性来存储蛇的移动方向(也就是按键的方向)
direction: string = "";
constructor() {
this.snake = new Snake();
this.food = new Food();
this.scorePanel = new ScorePanel();
this.init();
}
// 游戏初始化方法,调用后游戏即开始
init() {
// 绑定键盘按下事件
document.addEventListener("keydown", this.keydownHandle.bind(this));
}
/*
Chrome IE
ArrowUp Up
ArrowDown Down
ArrowLeft Left
ArrowRight Right
*/
// 创建一个键盘按下的相应函数
keydownHandle(event: KeyboardEvent) {
// 修改direction属性
this.direction = event.key;
}
}
export default GameControl;
七、GameControl - 使蛇移动
1.创建方法:使蛇移动
// 创建一个控制蛇移动的方法
run() {
/* 根据方向(this.direction)来使蛇的位置改变
向上 top 减少
向下 top 增加
向左 left 减少
向右 left 增加
*/
// 获取蛇现在的坐标
let x: number = this.snake.X;
let y: number = this.snake.Y;
// 根据舍得方向计算坐标值
switch (this.direction) {
case "ArrowUp":
case "Up":
case "w":
case "W":
// 向上移动 top 减少
y -= 10;
break;
case "ArrowDown":
case "Down":
case "s":
case "S":
// 向下移动 top 增加
y += 10;
break;
case "ArrowLeft":
case "Left":
case "a":
case "A":
// 向左移动 left 减少
x -= 10;
break;
case "ArrowRight":
case "Right":
case "d":
case "D":
// 向右移动 right 增加
x += 10;
break;
}
// 修改蛇的位置
this.snake.X = x;
this.snake.Y = y;
}
2.在index.ts中引入GameControl文件,并new GameControl()调用
// 引入样式
import "./style/index.less";
import GameControl from "./modules/GameControl";
new GameControl();
3.此时我们的蛇已经可以移动了,但是它不是实时移动的,接下来我们添加定时调用方法
setTimeout(this.run.bind(this), 300);
4.接下来完成一些逻辑补充,创建一个变量用来检测蛇是不是还活着
// 创建一个属性用来记录游戏是否结束
isLive: boolean = true;
八、蛇撞墙和吃食检测
1.修改Snake.ts中set x和 set y 的内容
set X(value) {
// 如果新值和旧值相同,则直接返回不再修改
if (this.X === value) return;
// x的值的合法范围0-290之间
if (value < 0 || value > 290) {
// 进入判断说明蛇撞墙了,抛出一个异常
throw new Error("蛇撞墙了");
}
this.hand.style.left = `${value}px`;
}
set Y(value) {
// 如果新值和旧值相同,则直接返回不再修改
if (this.Y === value) return;
// x的值的合法范围0-290之间
if (value < 0 || value > 290) {
// 进入判断说明蛇撞墙了,抛出一个异常
throw new Error("蛇撞墙了!");
}
this.hand.style.top = `${value}px`;
}
2.在GameControl中添加异常捕获
// 使用异常捕获来处理所有的游戏结束事件,减少代码
try {
// 修改蛇的位置
this.snake.X = x;
this.snake.Y = y;
} catch (e: any) {
// 进入到catch,说明出现了异常,游戏结束,弹出一个提示信息
alert(e.message + "GAME OVER,请刷新页面后重新开始游戏");
// 将isLive设置为false
this.isLive = false;
}
3. 此时我们控制蛇撞墙就会弹窗,但是我们的蛇还能掉头,接下来我们处理这个问题
4.打开Snake文件,增加set x 和 set y 的逻辑
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
) {
// 如果发生了掉头,让蛇向反方向继续移动
if (value > this.X) {
// 如果新值value大于旧值x,则说明蛇在向右走,此时发生掉头,应该使蛇继续向左走
value = this.X - 10;
} else {
value = this.X + 10;
}
}
this.hand.style.left = `${value}px`;
}
set Y(value) {
// 如果新值和旧值相同,则直接返回不再修改
if (this.Y === value) return;
// x的值的合法范围0-290之间
if (value < 0 || value > 290) {
// 进入判断说明蛇撞墙了,抛出一个异常
throw new Error("蛇撞墙了!");
}
// 修改y时,是在修改垂直坐标,蛇在上下移动,蛇在向上移动时,不能向下掉头,反之亦然
if (
this.bodies[1] &&
(this.bodies[1] as HTMLElement).offsetLeft === value
) {
// 如果发生了掉头,让蛇向反方向继续移动
if (value < this.Y) {
// 如果新值value大于旧值y,则说明蛇在向上走,此时发生掉头,应该使蛇继续向下走
value = this.Y - 10;
} else {
value = this.Y + 10;
}
}
this.hand.style.top = `${value}px`;
}
5.此时我们掉头蛇就不会掉头了,接下来我们处理我们的蛇的身体
// 添加一个蛇身体移动的方法
moveBody() {
/* 将后边的身体设置为前边身体的位置
举例子:
第四节 = 第三节的位置
第三节 = 第二节的位置
第二节 = 蛇头的位置
*/
//便利获取所有的身体
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`;
}
}
6.在set x,set y中调用moveBody方法
7.现在我们发现我么的蛇还能够穿过自己的身体,接下来我们处理这个问题
// 检查蛇头是否撞到身体的方法
checkHeadBody() {
// 获取所有的身体,检查其是否和蛇头的坐标发生重叠
for (let i = 1; i < this.bodies.length; i++) {
let bd = this.bodies[i] as HTMLElement;
console.log(this.X, bd.offsetLeft, this.Y, bd.offsetTop);
if (this.X === bd.offsetLeft && this.Y === bd.offsetTop) {
// 进入判断说明蛇头撞到了身体,游戏结束
throw new Error("撞到自己了!");
}
}
}
8.在set x,set y中调用checkHeadBody方法
9.完整Snake文件
class Snake {
// 表示蛇头的元素
hand: HTMLElement;
// 蛇的身体(包括舌头)
bodies: HTMLCollection;
// 获取蛇的容器
element: HTMLElement;
constructor() {
this.element = document.getElementById("snake")!;
this.hand = document.querySelector("#snake > div")!;
this.bodies = this.element.getElementsByTagName("div")!;
}
// 获取蛇的x坐标
get X() {
return this.hand.offsetLeft;
}
// 获取蛇的y坐标
get Y() {
return this.hand.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
) {
// 如果发生了掉头,让蛇向反方向继续移动
if (value > this.X) {
// 如果新值value大于旧值x,则说明蛇在向右走,此时发生掉头,应该使蛇继续向左走
value = this.X - 10;
} else {
value = this.X + 10;
}
}
// 移动的时候调用身体方法
this.moveBody();
this.hand.style.left = `${value}px`;
// 检查有没有撞到自己
this.checkHeadBody();
}
set Y(value) {
// 如果新值和旧值相同,则直接返回不再修改
if (this.Y === value) return;
// x的值的合法范围0-290之间
if (value < 0 || value > 290) {
// 进入判断说明蛇撞墙了,抛出一个异常
throw new Error("蛇撞墙了!");
}
// 修改y时,是在修改垂直坐标,蛇在上下移动,蛇在向上移动时,不能向下掉头,反之亦然
if (
this.bodies[1] &&
(this.bodies[1] as HTMLElement).offsetLeft === value
) {
// 如果发生了掉头,让蛇向反方向继续移动
if (value < this.Y) {
// 如果新值value大于旧值y,则说明蛇在向上走,此时发生掉头,应该使蛇继续向左走
value = this.Y - 10;
} else {
value = this.Y + 10;
}
}
// 移动的时候调用身体方法
this.moveBody();
this.hand.style.top = `${value}px`;
// 检查有没有撞到自己
this.checkHeadBody();
}
// 蛇增加身体的方法
addBody() {
// 向element中添加一个div
this.element.insertAdjacentHTML("beforeend", "<div></div>");
}
// 添加一个蛇身体移动的方法
moveBody() {
/* 将后边的身体设置为前边身体的位置
举例子:
第四节 = 第三节的位置
第三节 = 第二节的位置
第二节 = 蛇头的位置
*/
//便利获取所有的身体
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;
console.log(this.X, bd.offsetLeft, this.Y, bd.offsetTop);
if (this.X === bd.offsetLeft && this.Y === bd.offsetTop) {
// 进入判断说明蛇头撞到了身体,游戏结束
throw new Error("撞到自己了!");
}
}
}
}
export default Snake;
10.完整GameControl文件
// 引入其他类
import Snake from "./Snake";
import Food from "./Food";
import ScorePanel from "./ScorePanel";
// 游戏控制器,控制其他的所有类
class GameControl {
// 定义三个属性
// 蛇
snake: Snake;
food: Food;
scorePanel: ScorePanel;
// 创建一个属性来存储蛇的移动方向(也就是按键的方向)
direction: string = "";
// 创建一个属性用来记录游戏是否结束
isLive: boolean = true;
constructor() {
this.snake = new Snake();
this.food = new Food();
this.scorePanel = new ScorePanel();
this.init();
}
// 游戏初始化方法,调用后游戏即开始
init() {
// 绑定键盘按下事件
document.addEventListener("keydown", this.keydownHandle.bind(this));
this.run();
}
/*
Chrome IE
ArrowUp Up
ArrowDown Down
ArrowLeft Left
ArrowRight Right
*/
// 创建一个键盘按下的相应函数
keydownHandle(event: KeyboardEvent) {
// 修改direction属性
this.direction = event.key;
}
// 创建一个控制蛇移动的方法
run() {
/* 根据方向(this.direction)来使蛇的位置改变
向上 top 减少
向下 top 增加
向左 left 减少
向右 left 增加
*/
// 获取蛇现在的坐标
let x: number = this.snake.X;
let y: number = this.snake.Y;
// 根据舍得方向计算坐标值
switch (this.direction) {
case "ArrowUp":
case "Up":
case "w":
case "W":
// 向上移动 top 减少
y -= 10;
break;
case "ArrowDown":
case "Down":
case "s":
case "S":
// 向下移动 top 增加
y += 10;
break;
case "ArrowLeft":
case "Left":
case "a":
case "A":
// 向左移动 left 减少
x -= 10;
break;
case "ArrowRight":
case "Right":
case "d":
case "D":
// 向右移动 right 增加
x += 10;
break;
}
// 检查蛇是否吃到了食物
this.checkEat(x, y);
// 使用异常捕获来处理所有的游戏结束事件,减少代码
try {
// 修改蛇的位置
this.snake.X = x;
this.snake.Y = y;
} catch (e: any) {
// 进入到catch,说明出现了异常,游戏结束,弹出一个提示信息
alert(e.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;
gitee地址:Snake_Game: 使用ts实现一个简单的贪吃蛇小游戏