package.json
{
"name": "webpack",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack server"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.18.6",
"@babel/preset-env": "^7.18.6",
"babel-loader": "^8.2.5",
"clean-webpack-plugin": "^4.0.0",
"core-js": "^3.23.4",
"css-loader": "^6.7.1",
"html-webpack-plugin": "^5.5.0",
"less": "^4.1.3",
"less-loader": "^11.0.0",
"postcss": "^8.4.14",
"postcss-loader": "^7.0.1",
"postcss-preset-env": "^7.7.2",
"style-loader": "^3.3.1",
"ts-loader": "^9.3.1",
"typescript": "^4.7.4",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3"
}
}
tsconfig.json
{
"compilerOptions": {
"module": "ES2015",
"target": "ES6",
"strict": true,
"noEmitOnError": true
}
}
webpack.config.js
const path = require("path")
// 引入html插件
const HTMLWebpackPlugin = require("html-webpack-plugin")
const { options } = require("nodemon/lib/config")
const {CleanWebpackPlugin} = require("clean-webpack-plugin")
// webpack中的所有的配置信息都应该写在module . exports中
module.exports = {
// 入口文件
entry: "./src/index.ts",
output: {
// 指定打包文件所在的目录
path: path.resolve(__dirname, 'dist'),
// 打包后的文件名
filename: "boundle.js",
// 高速webpack不是用箭头函数
environment: {
arrowFunction:false
}
},
mode: 'development', // 设置mode
// 指定webpack打包是要使用的模块
module: {
// 指定加载规则
rules: [
{
// test指定的是规则生效的文件(以.ts结尾的文件)
test: /\.ts$/,
// 要使用的loader(use从后向前执行,先执行ts-loader)
use: [
// 配置babel-loader
{
// 指定加载器
loader: "babel-loader",
// 设置babel
options: {
// 设置预定义环境
presets: [
[
// 指定环境的插件
"@babel/preset-env",
// 配置信息
{
// 要兼容的目标浏览器
targets:{
// "chrome": "88",
"ie":"11"
},
"corejs": "3",
// s使用corejs方式“usage”表示按需加载
"useBuiltIns":"usage"
}
]
]
}
},
'ts-loader'
],
// 编译要排除的文件,
exclude:/node-modules/
},
{
// 设置less文件的处理
test: /\.less$/,
use: [
"style-loader",
"css-loader",
// 引入postcss
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugin: [
"postcss-preset-env",
{
// 兼容浏览器的最新两个版本
browsers:'last 2 versions'
}
]
}
}
},
"less-loader"
]
}
]
},
// 配置webpack插件
plugins: [
new CleanWebpackPlugin(),
// 自动生成相关文件并配置相关资源
new HTMLWebpackPlugin({
template:"./src/index.html"
})
],
// 配置置引用模块
resolve: {
// 声明.ts,.js结尾的文件为模块
extensions:['.ts','.js']
}
}
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">
<div></div>
</div>
<div id="food">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
<div id="score-panel">
<div>SCORE:0</div>
<div>level:1</div>
</div>
</div>
</body>
</html>
index.less
// 设置变量
@bg-color:#b7d4a8;
*{
margin:0;
padding: 0;
// 设置盒子样式,width代表总宽度的
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: black;
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;
transform: rotate(45deg);
}
}
}
#score-panel{
width: 300px;
display: flex;
justify-content: space-between;
}
// 定义类
class Food{
// 定义一个属性表示食物所对应的元素
element: HTMLElement
constructor() {
// 获取页面的父元素并赋值给element
this.element = document.getElementById("food")!;
}
// 获取食物的坐标
get x() {
return this.element.offsetLeft
}
get y() {
return this.element.offsetTop
}
change() {
// 随机数
// 事物的位置 0-290
// 设移动一次应该是10,所以食物的位置需要时10的倍数
// Math.round(Math.random() * 29):四舍五入取到0-29之间的数
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'
}
}
// const food = new Food();
// console.log(food.x, food.y)
// food.change()
// console.log(food.x, food.y)
export default Food
// 计分牌
class ScorePanel{
score = 0;
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(){
this.scoreEle.innerHTML = 'SCORE:'+ (++this.score)
// 每10分升一级
if (this.score % this.upScore === 0) {
this.levelUp();
}
}
// 等级提升
levelUp() {
if (this.level < this.maxLevel ) {
this.levelEle.innerHTML = 'level:'+(++this.level)
}
}
}
// const scorePanel = new ScorePanel()
// scorePanel.addScore()
// console.log(scorePanel.score)
// for (let i = 0; i < 200; i++){
// scorePanel.addScore();
// }
export default ScorePanel
class Snake{
// 表示蛇的元素
head: HTMLElement;
bodies: HTMLCollection
// 获取蛇的容器
element:HTMLElement
constructor() {
this.head = document.querySelector('#snake>div')! as HTMLElement
// document.getElementById('snake')!.getElementsByTagName("div")这样获取的是一个集合
this.bodies = document.getElementById('snake')!.getElementsByTagName("div")
this.element=document.getElementById('snake')!
}
// 获取蛇(蛇头)的坐标
get x() {
return this.head.offsetLeft;
}
get y() {
return this.head.offsetTop;
}
set x(value: number) {
if (this.x == value) {
return;
}
// 是否撞墙
// x合法值 0-290
if(value < 0 || value > 290) {
throw new Error("蛇撞墙了")
}
// 蛇在左右移动,蛇在向左移动时,不能向右掉头,反之亦然
if (this.bodies[1] && (this.bodies[1] as HTMLElement).offsetLeft === value) {
// console.log("水平方向发生了掉头")
// 蛇按原方向继续移动
if (value > this.x) {
// 想要向右走,说明现在是向左走,为了控制蛇不能掉头,所以继续设置他向左走
value = this.x-10
} else {
value = this.x+10
}
}
// 蛇的移动是通过后面的结点替代前面的结点,所以一定从后向前进行替换,所以先处理身体再处理蛇头
// 身体移动
this.moveBody()
this.head.style.left = value + 'px'
// 蛇头修改完成之后在判断是否撞到自己
this.cheackHeadBody()
}
set y(value: number) {
if (this.y == value) {
return;
}
// 是否撞墙
// y合法值 0-290
if(value < 0 || value > 290) {
throw new Error("蛇撞墙了")
}
// 蛇在上下移动,蛇在向上移动时,不能向下掉头,反之亦然
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.cheackHeadBody()
}
// 蛇增长
addBody() {
// beforeend是说把内容加到element结束标签之前
this.element.insertAdjacentHTML("beforeend","")
}
// 移动身体(蛇头的位置由键盘控制,这里不做修改)
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';
}
}
cheackHeadBody() {
// 获取所有的身体,检查其是否和蛇头的坐标发生重叠
for(let i = 1; i < this.bodies.length; i++) {
if (this.x === (this.bodies[i] as HTMLElement).offsetLeft && this.y === (this.bodies[i] as HTMLElement).offsetTop) {
throw new Error("蛇撞到自己了")
}
}
}
}
export default Snake
import Snake from "./Snake";
import Food from "./Food";
import ScorePanel from "./ScorePanel";
// 游戏控制器
class GameController{
snake: Snake
food: Food
scorePanel: ScorePanel
// 创建属性来记录游戏是否结束
isLive = true
// 创建属性存储按键方向
direction:string='Right'
constructor() {
this.snake = new Snake()
this.food = new Food()
// 可以自己设置等级,和几分升一级
this.scorePanel = new ScorePanel(10,10)
this.init()
this.run()
}
// 游戏初始化
init() {
this.food.change()
// 监听键盘按下事件
// 这里有一个问题,回调函数keydownHander中的this是谁,在js中谁调用这个函数this就是谁,这里显然是document,但是我们希望是这个类中的this。所以可以使用bind函数进行this绑定
// this.keydownHander.bind(this)表示将当前函数的this绑定给keydownHander
document.addEventListener('keydown',this.keydownHander.bind(this))
}
keydownHander(event: KeyboardEvent) {
/**
* this.direction:
* ArrowUp Up
* ArrowDown Down
* ArrowLeft Left
* ArrowRight Right
*/
// 检查方向是否合法
this.direction = event.key
}
run() {
// 获取蛇头位置
let x = this.snake.x
let y = this.snake.y
/**
* 根据this.direction来使蛇的位置改变
* 向上 top减少
* 向下 top增加
* 向左 left减少
* 向右 left增加
*/
switch (this.direction) {
case "ArrowUp":
case "Up":
y -= 10
break;
case "ArrowDown":
case "Down":
y += 10
break
case "ArrowLeft":
case "Left":
x -= 10
break
case "ArrowRight":
case "Right":
x += 10
break
}
// 判断是否吃到食物
this.checkEat(x, y)
// 蛇进行移动
try {
this.snake.x = x
this.snake.y = y
} catch (e) {
console.log(e)
alert(e+",游戏结束!")
this.isLive = false
}
// 由于函数嵌套的作用,每隔300ms调用一次run函数,就会一直移动
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) {
console.log("吃到食物")
// 食物位置改变
this.food.change()
// 分数增加
this.scorePanel.addScore();
// 蛇长的增加
this.snake.addBody()
}
}
}
export default GameController
index.ts
// 引入样式(在ts中引入)
import './style/index.less'
import GameController from './moduls/GameController'
const gameController = new GameController()
源码获取:https://github.com/wangliyang-max/qiandaun/tree/master/TS/%E8%B4%AA%E5%90%83%E8%9B%87
运行方式:
npm i
npm start