TS实战项目--贪吃蛇
项目搭建
项目依赖
{
"name": "part2",
"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": "^5.0.1",
"html-webpack-plugin": "^4.5.0",
"less": "^3.12.2",
"less-loader": "^7.1.0",
"postcss": "^8.1.13",
"postcss-loader": "^4.1.0",
"postcss-preset-env": "^6.7.0",
"style-loader": "^2.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"
}
}
webpack打包规则
// 引入一个包
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:[
"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({
// title: "这是一个自定义的title"
template: "./src/index.html"
}),
],
// 用来设置引用模块
resolve: {
extensions: ['.ts', '.js']
}
};
项目界面
布局
<!DOCTYPE html>
<html lang="zh-CN">
<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">
<!-- 添加四个div设置样式 -->
<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>
样式
// 设置变量
@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: 40px;
// 开启弹性盒模型
display: flex;
// 设置主轴的方向
flex-flow: column;
// 设置侧轴的对齐方式
align-items: center;
// 设置主轴的对齐方式
justify-content: space-around;
// 游戏舞台
#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;
position: absolute;
left: 40px;
top: 100px;
// 开启弹性盒
display: flex;
// 设置横轴为主轴,wrap表示会自动换行
flex-flow: row wrap;
// 设置主轴和侧轴的空白空间分配到元素之间
justify-content: space-between;
align-content: space-between;
&>div{
width: 4px;
height: 4px;
background-color: black;
// 使四个div旋转45度
transform: rotate(45deg);
}
}
}
// 记分牌
#score-panel{
width: 300px;
display: flex;
// 设置主轴的对齐方式
justify-content: space-between;
}
}
定义食物类
- 定义一个属性表示食物对应的元素,获取x轴, y轴坐标
- 生成随机位置,食物的位置最小时0最大是290,
- 蛇移动一次就是一格(10),所以要求食物的坐标必须为整10(向上取整)
//定义食物类
class Food {
//定义一个属性表示食物对应的元素
element: HTMLElement;
constructor() {
//获取页面食物元素并将其赋值给element
this.element = document.getElementById("food")!
}
//定义一个获取食物x轴坐标的方法
get x() {
return this.element.offsetLeft;
}
//定义一个获取食物y轴坐标的方法
get y() {
return this.element.offsetTop;
}
//修改食物位置的方法
chhange() {
//生成随机位置,食物的位置最小时0最大是290,
// 蛇移动一次就是一格(10),所以要求食物的坐标必须为整10(向上取整)
let top = Math.round(Math.random() * 29) * 10
let left = Math.round(Math.random() * 29) * 10
this.element.style.top = top + 'px'
this.element.style.left = left + 'px'
}
}
计分器类
属性
- score记录分数 lebel 等级
- scoreEle,lebelEle 分数和等级所在的元素
- maxLebel来限制等级, upScore设置多少分升级
方法
- 设置加分的方法 分数自增 提升等级的方法 分数取余upScore(默认10)调用 等级方法自增
//定义计分牌的类
class ScorePanel {
//用来记录分数和等级
score = 0;
lebel = 1;
//分数和等级所在的元素,在构造函数中进行初始化
scoreEle: HTMLElement;
lebelEle: HTMLElement;
//设置一个变量来限制等级
maxLebel: number
//设置一个变量设置多少分升级
upScore: number
constructor(maxLebel: number = 10, upScore: number = 10) {
this.scoreEle = document.getElementById('score')!
this.lebelEle = document.getElementById('level')!
this.maxLebel = maxLebel
this.upScore = upScore
}
//设置加分的方法
addScore() {
//使分数自增
this.score++
this.scoreEle.innerHTML = this.score + ''
//判断分数是多少
if (this.score % this.upScore == 0) {
this.lebelUp();
}
}
//提升等级的方法
lebelUp() {
if (this.lebel < this.maxLebel) {
this.lebel++
this.lebelEle.innerHTML = this.lebel + ''
}
}
}
export default ScorePanel
蛇类
属性
- 表示蛇头的元素 head
- 蛇的身体(包括蛇头) bodies
- 获取蛇的容器 element:
方法
getx gety 获取蛇的坐标(蛇头坐标)
set x set y( 设置蛇头的坐标)
-
先设置蛇头 value为设置的新值,
-
如果新值和旧值相同,则直接返回不再修改,
-
蛇撞墙逻辑处理新值的取值范围0-290之间超出这个范围则视为撞墙
-
不能反方向移动, 如果发生了掉头,让蛇向反方向继续移动 如果新值value大于旧值X,则说明蛇在向右走,此时发生掉头,应该使蛇继续向左走
-
移动身体
-
检查有没有撞到自己
addBody蛇增加身体的方法 向element中添加一个div
moveBody 蛇身体移动的方法
- 将后边的身体设置为前边身体的位置 举例子 第4节 = 第3节的位置 第3节 = 第2节的位置 第2节 = 蛇头的位置
- 遍历获取所有的身体,获取前边身体的位置,将值设置到当前身体上
checkHeadBody 检查蛇头是否撞到身体的方法 获取所有的身体,检查其是否和蛇头的坐标发生重叠 进入判断说明蛇头撞到了身体,游戏结束
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');
}
// 获取蛇的坐标(蛇头坐标)
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时,是在修改水平坐标,蛇在左右移动,蛇在向左移动时,不能向右掉头,反之亦然
//this.bodies[1] 第一节身体是否存在,
if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetLeft === value) {
// console.log('水平方向发生了掉头');
// 如果发生了掉头,让蛇向反方向继续移动
if (value > this.x) {
// 如果新值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) {
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;
游戏控制器
属性
- 导入食物 蛇 计分器 控制所有类
- directionL存储蛇移动的方向(也就是按键的方向)
- isLive用来记录游戏是否结束
方法
init/游戏初始化方法 监听键盘事件键盘按下 ,来绑定键盘按下的事件,注意this指向问题 this应该为GameControl类 ,调用run方法使蛇移动
run 创建一个控制蛇移动的方法(根据方向来改变蛇的位置 directionL)
- 获取蛇现在的坐标
- 根据按键方向修改x值y值 ,向上 top减少,向下top增加, 向左left减少 , 向右left增加
- 检测蛇是否吃到了食物 传入蛇的当前坐标
- 修改蛇的x,y 值,进入catch 说明出现异常,游戏结弹出提示信息(异常情况 撞墙,身体重合在蛇类抛出了异常在这里做处理 catah接错)
checkEat 测蛇是否吃到了食物 蛇的坐标跟食物坐标做对比是否坐标相等
- 食物位置进行重置
- 分数增加
- 蛇增加一结
//导入其他类
import Snake from "./snake"
import Food from "./Food"
import ScorePanel from "./ScorePanel"
//游戏控制器,控制其他所有类
class GameControl {
snake: Snake;
food: Food;
Scorepanel: ScorePanel;
//创建一个属性来存储蛇移动的方向(也就是按键的方向)
directionL = ''
//创建一个属性用来记录游戏是否结束
isLive = true;
constructor() {
this.snake = new Snake()
this.food = new Food()
this.Scorepanel = new ScorePanel()
this.init()
}
//游戏初始化方法 ,调用后游戏即开始
init() {
// 来绑定键盘按下的事件,注意this指向问题 this应该为GameControl类
document.addEventListener('keydown', this.keydownHandler.bind(this))
// 调用run方法使蛇移动
this.run()
}
//创建键盘按下的响应函数
keydownHandler(e: KeyboardEvent) {
//需要检测event.key是否合法(用户按了正确的按键)
//修改directionL属性
this.directionL = e.key
}
// 创建一个控制蛇移动的方法(根据方向来改变蛇的位置 directionL)
run() {
//获取蛇现在的坐标
let x = this.snake.x
let y = this.snake.y
//根据按键方向修改x值y值
switch (this.directionL) {
// 向上 top减少
case 'ArrowUp':
y -= 10;
break;
case 'ArrowDown':
// 向下top增加
y += 10
break;
case 'ArrowLeft':
// 向左left减少
x -= 10
break;
case 'ArrowRight':
// 向右left增加
x += 10
break;
}
//检测蛇是否吃到了食物
this.checkEat(x, y)
//修改蛇的x,y 值
try {
this.snake.x = x
this.snake.y = y
} catch (e: any) {
//进入catch 说明出现异常,游戏结弹出提示信息
alert(e.message)
this.isLive = false
}
this.isLive && setTimeout(this.run.bind(this), 300 - (this.Scorepanel.lebel - 1) * 30)
}
//检测蛇是否吃到了食物 蛇的坐标
checkEat(x: number, y: number) {
if (x == this.food.x && y == this.food.y) {
//食物位置进行重置
this.food.chhange();
//分数增加
this.Scorepanel.addScore()
//蛇增加一结
this.snake.addBody()
}
}
}
export default GameControl
index.ts
创建控制器实例开始游戏
import './style/index.less'
// import Food from './moduls/Food';
import GameControl from "./moduls/GameControl";
new GameControl()