master
fantasticbin 2 years ago
commit e1dc2551e2

@ -0,0 +1,80 @@
## **贪吃蛇练习**
使用TypeScript + Webpack + Less实现贪吃蛇的例子
### **项目依赖**
TypeScript
- typescript
- ts-loader
Webpack
- webpack
- webpack-cli
- webpack-dev-server
- html-webpack-plugin
- clean-webpack-plugin
Babel
- core-js
- babel-loader
- @babel/core
- @babel/preset-env
Less & CSS资源
- style-loader
- css-loader
- less
- less-loader
- postcss
- postcss-loader
- postcss-preset-env
### **项目使用**
#### **编译运行**
在确保已经正确安装node和npm的前提下
分别执行下面的命令安装依赖并编译项目:
```bash
# 安装依赖
npm i
# 编译打包
npm run build
```
编译完成后使用浏览器打开dist目录下的`index.html`即可游玩;
根据老师的例子优化部分逻辑,及增加食物刷新时与蛇身重叠规避的逻辑,欢迎提出意见或建议
#### **继续开发**
使用`npm run start`进入开发模式;
默认使用Chrome浏览器打开可以修改`package.json`中的值:
```json
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack serve --open chrome.exe"
}
}
```
### **其他**
视频讲解:
- [尚硅谷2021版TypeScript教程李立超老师TS新课](https://www.bilibili.com/video/BV1Xy4y1v7S2?p=22)
老师源码:
- [JasonkayZK/typescript-learn](https://github.com/JasonkayZK/typescript-learn/tree/greedy-snake)

@ -0,0 +1,34 @@
{
"name": "snake",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --mode development --config webpack.config.js",
"start": "webpack serve --open chrome.exe"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.18.2",
"@babel/preset-env": "^7.18.2",
"babel-loader": "^8.2.5",
"clean-webpack-plugin": "^4.0.0",
"core-js": "^3.22.8",
"css-loader": "^6.7.1",
"html-webpack-plugin": "^5.5.0",
"less": "^4.1.2",
"less-loader": "^11.0.0",
"postcss": "^8.4.14",
"postcss-loader": "^7.0.0",
"postcss-preset-env": "^7.7.1",
"style-loader": "^3.3.1",
"ts-loader": "^9.3.0",
"typescript": "^4.7.3",
"webpack": "^5.73.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.9.1"
}
}

@ -0,0 +1,34 @@
<!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 id="score-panel">
<div>
SCORE:<span id="score">0</span>
</div>
<div>
LEVEL:<span id="level">1</span>
</div>
</div>
</div>
</body>
</html>

@ -0,0 +1,6 @@
// 引入样式
import "./style/index.less";
import GameController from "./modules/GameController";
const game = new GameController();
game.init();

@ -0,0 +1,53 @@
/**
*
*/
class Food
{
protected element: HTMLElement;
constructor()
{
this.element = document.getElementById("food")!;
}
/**
* X
*/
get X(): number
{
return this.element.offsetLeft;
}
/**
* Y
*/
get Y(): number
{
return this.element.offsetTop;
}
/**
*
* @param snakeBodies
*/
public change(snakeBodies: HTMLCollectionOf<HTMLElement>): void
{
// 随机生成一个位置最小是0最大是290每次移动间隔大小是10
let top = Math.round(Math.random() * 29) * 10;
let left = Math.round(Math.random() * 29) * 10;
// 防止食物重置时位置和蛇身重叠
for (let snakeBody of snakeBodies) {
if (top === snakeBody.offsetTop && left === snakeBody.offsetLeft) {
// 递归调用,生成新的位置,并中止当前程序继续执行(一定要加上中止,否则该逻辑无效)
this.change(snakeBodies);
return;
}
}
this.element.style.top = top + "px";
this.element.style.left = left + "px";
}
}
export default Food;

@ -0,0 +1,99 @@
import Snake from "./Snake";
import Food from "./Food";
import ScorePanel from "./ScorePanel";
/**
*
*/
class GameController
{
protected snake: Snake;
protected food: Food;
protected scorePanel: ScorePanel;
// 用于防止调头,记录上一次键盘操作
protected lastKeyDown = '';
// 用于标记游戏是否未结束
protected isLive = true;
constructor()
{
this.snake = new Snake();
this.food = new Food();
this.scorePanel = new ScorePanel();
}
/**
*
*/
public init(): void
{
// 绑定键盘事件,当按下 上/下/左/右 时改变蛇的行动方向
document.addEventListener("keydown", this.keyDownHandle.bind(this));
this.snakeRun();
}
/**
*
* @param event
*/
protected keyDownHandle(event: KeyboardEvent): void
{
const horizontal = ["ArrowLeft", "Left", "ArrowRight", "Right"];
const vertical = ["ArrowUp", "Up", "ArrowDown", "Down"];
// 防止水平调头
if (horizontal.includes(this.lastKeyDown) && horizontal.includes(event.key)) {
return;
}
// 防止垂直调头
if (vertical.includes(this.lastKeyDown) && vertical.includes(event.key)) {
return;
}
// 设置当前蛇的行走方向
this.snake.nowDirection = event.key;
// 记录此次键盘操作
this.lastKeyDown = event.key;
}
/**
*
* @param x X
* @param y Y
*/
protected checkEat(x: number, y: number): void
{
if (x === this.food.X && y === this.food.Y) {
// 食物位置重置
this.food.change(this.snake.nowBodies);
// 分数增加
this.scorePanel.addScore();
// 蛇身增加一节
this.snake.addBody();
}
}
/**
*
*/
protected snakeRun(): void
{
this.checkEat(this.snake.X, this.snake.Y);
try {
this.snake.checkRearEnd();
this.snake.run();
} catch (e: any) {
// 捕获到错误意味着游戏失败
alert((e as Error).message + " GAME OVER!");
this.isLive = false;
}
// 定时调用自身,并根据当前等级动态改变速度
this.isLive && setTimeout(this.snakeRun.bind(this), 300 - (this.scorePanel.nowLevel - 1) * 30);
}
}
export default GameController;

@ -0,0 +1,49 @@
/**
*
*/
class ScorePanel
{
protected score = 0;
protected level = 1;
protected scoreElement: HTMLElement;
protected levelElement: HTMLElement;
constructor(protected maxLavel: number = 10, protected upScore: number = 10)
{
this.scoreElement = document.getElementById("score")!;
this.levelElement = document.getElementById("level")!;
}
/**
*
*/
public addScore(): void
{
this.scoreElement.innerText = (++this.score).toString();
// 分数达到设置的数则升级
if (this.score % this.upScore === 0) {
this.levelUp();
}
}
/**
*
*/
protected levelUp(): void
{
if (this.level <= this.maxLavel) {
this.levelElement.innerText = (++this.level).toString();
}
}
/**
*
*/
get nowLevel(): number
{
return this.level;
}
}
export default ScorePanel;

@ -0,0 +1,157 @@
/**
*
*/
class Snake
{
protected element: HTMLElement;
// 蛇头元素
protected head: HTMLElement;
// 蛇的身体(包括蛇头)
protected bodies: HTMLCollectionOf<HTMLElement>;
// 蛇当前的行走方向
protected direction = "";
constructor()
{
this.element = document.getElementById("snake")!;
this.head = document.querySelector("#snake > div")!;
this.bodies = this.element.getElementsByTagName("div");
}
/**
* X
*/
set X(value: number)
{
if (this.X === value) {
return;
}
if (value < 0 || value > 290) {
throw new Error("蛇撞墙了!");
}
this.moveBody();
this.head.style.left = value + "px";
}
/**
* X
*/
get X(): number
{
return this.head.offsetLeft;
}
/**
* Y
*/
set Y(value: number)
{
if (this.Y === value) {
return;
}
if (value < 0 || value > 290) {
throw new Error("蛇撞墙了!");
}
this.moveBody();
this.head.style.top = value + "px";
}
/**
* Y
*/
get Y(): number
{
return this.head.offsetTop;
}
/**
*
*/
set nowDirection(value: string)
{
this.direction = value;
}
/**
*
*/
get nowBodies(): HTMLCollectionOf<HTMLElement>
{
return this.bodies;
}
/**
*
*/
public addBody(): void
{
const div = document.createElement("div");
this.element.appendChild(div);
}
/**
*
*/
public moveBody(): void
{
for (let i = this.bodies.length - 1; i > 0; i--) {
let x = this.bodies[i - 1].offsetLeft;
let y = this.bodies[i - 1].offsetTop;
this.bodies[i].style.left = x + "px";
this.bodies[i].style.top = y + "px";
}
}
/**
*
*/
public checkRearEnd(): void | never
{
// 只要蛇头与其他任何蛇身重叠则为追尾
for (let i = 1; i < this.bodies.length; i++) {
if (this.X === this.bodies[i].offsetLeft && this.Y === this.bodies[i].offsetTop) {
throw new Error("蛇追尾了!");
}
}
}
/**
*
*/
public run(): void
{
let x = this.X;
let y = this.Y;
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.X = x;
this.Y = y;
}
}
export default Snake;

@ -0,0 +1,74 @@
// 颜色变量
@bg-color: #b7d4a8;
// 清除默认样式
* {
margin: 0;
padding: 0;
// 改变盒子模型的计算方式
box-sizing: border-box;
}
// 页面公共样式
body {
font: bold 20px "Courier";
overflow: hidden;
}
// 主窗口的样式
#main {
width: 360px;
height: 420px;
background-color: @bg-color;
margin: 100px auto;
border: 10px solid black;
border-radius: 20px;
// 开启弹性盒模型
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;
background-color: black;
border: 1px solid @bg-color;
border-radius: 50%;
// 开启绝对定位
position: absolute;
left: 40px;
top: 100px;
}
// 记分牌
#score-panel {
width: 300px;
display: flex;
justify-content: space-between;
}

@ -0,0 +1,8 @@
{
"compilerOptions": {
"module": "ES2015",
"target": "ES2015",
"strict": true,
"noEmitOnError": true
}
}

@ -0,0 +1,110 @@
// 引入一个包
const path = require("path");
// 引入HTML插件
const HTMLWebpackPlugin = require("html-webpack-plugin");
// 引入Clean插件
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
// webpack中的所有配置信息
module.exports = {
mode: "development",
// 指定入口文件
entry: "./src/index.ts",
// 指定打包文件所在目录
output: {
// 指定打包文件的目录
path: path.resolve(__dirname, "dist"),
// 打包后的文件名
filename: "bundle.js",
// 不使用箭头函数的方式定义
environment: {
arrowFunction: false,
const: false
}
},
// 指定webpack打包时要使用的模块
module: {
// 指定要加载的规则
rules: [
{
// 指定规则生效的文件
test: /\.ts$/,
// 要使用的loader
use: [
// 配置babel
{
// 指定加载器
loader: "babel-loader",
// 设置babel
options: {
// 设置预定义环境
presets: [
[
// 指定环境的插件
"@babel/preset-env",
// 配置信息
{
// 要兼容的目标浏览器
targets: {
"chrome": "100",
"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({
template: "./src/index.html"
})
],
// 设置引用模块
resolve: {
extensions: [".ts", ".js"]
}
};
Loading…
Cancel
Save