ts基础学习(二)

准备用ts写一个小项目,贪吃蛇。

一、项目搭建

1.1 项目环境配置

生成对应配置文件(里面信息配置因项目需求而异):

  1. tsconfig.json 文件

    1
    tsc --init

    在文件中添加如下一个字段,避免大括号报错问题。

    1
    "$schema": "http://json-schema.org/draft-04/schema#",
  2. package.json文件

    1
    npm init -y
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    {
    "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"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "devDependencies": {
    "@babel/core": "^7.24.5",
    "@babel/preset-env": "^7.24.5",
    "babel-loader": "^9.1.3",
    "clean-webpack-plugin": "^4.0.0",
    "core-js": "^3.37.0",
    "html-webpack-plugin": "^5.6.0",
    "ts-loader": "^9.5.1",
    "typescript": "^5.4.5",
    "webpack": "^5.91.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^5.0.4"
    }
    }
  3. webpack.config.js文件

    手动新建webpack.config.js文件,进行相关配置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    const path = require("path");

    // 引入插件
    const HTMLWebpackPlugin = require("html-webpack-plugin");
    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
    // }
    },
    module: {
    rules: [
    {
    test: /\.ts$/,
    use: [
    {
    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/,
    }
    ]
    },
    plugins: [
    new HTMLWebpackPlugin({
    template: "./src/index.html"
    }),
    new CleanWebpackPlugin(),
    ],

    // 用来设置引用模块
    resolve: {
    extensions: [".ts", ".js"]
    }
    }

    执行npm i安装所需依赖。

  4. 其他依赖

    前面文件里面包含的是常规依赖,此项目需要用到less相关包。

    1
    npm i -D less less-loader css-loader style-loader

    在webpack.config.js进行相关配置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 设置less文件的处理
    {
    test: /\.less$/,
    use: [
    "style-loader",
    "css-loader",
    "less-loader"
    ]
    }

    这是需要兼容性处理,使用的是postcss。

    1
    npm i -D postcss postcss-loader postcss-preset-env
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
     // 设置less文件的处理
    {
    test: /\.less$/,
    use: [
    "style-loader",
    "css-loader",
    // 引入postcss
    {
    loader: "postcss-loader",
    options: {
    postcssOptions: {
    plugins: [
    [
    "postcss-preset-env",
    {
    browsers: "last 2 versions"
    }
    ]
    ]
    }
    }
    },
    "less-loader"
    ]
    }

1.2 项目界面编写

(可以使用npm start实时查看编写情况)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<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="info">
<div>
分数:<span id="score">0</span>
</div>
<div>
长度:<span id="level">1</span>
</div>
</div>
</div>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// 清除默认样式
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}

// 设置变量
@bg-color: #b7d4a8;

body{
font: bold 20px "Courier";
}

// 游戏容器样式
#main{
width: 360px;
height: 420px;
background-color: @bg-color;
margin: 100px auto;
border: 10px solid black;
border-radius: 30px;

display: flex;
flex-flow: column;
align-items: center;
justify-content: space-around;
// 开启绝对定位
position: relative;

// 活动区域
#stage{
width: 300px;
height: 300px;
border: 2px solid black;

// 设置蛇的样式
#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;

display: flex;
flex-flow: row wrap;
justify-content: space-between;
align-content: space-between;
&>div{
width: 4px;
height: 4px;
background-color: black;
transform: rotate(45deg);
}
}
}

// 信息区域
#info{
width: 300px;
display: flex;
justify-content: space-between;
}
}

二、功能实现

整个项目主要有三个主体:食物、蛇、计分板,在此基础上需要对这三类进行操作的control类。

2.1 Food

食物(food)其实核心是随机出现,也就是能随机修改food位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Food{
elem: HTMLElement;

constructor(){
// 获取页面的food元素,将其赋值给elem
this.elem = document.getElementById("food")!;
}

// 获取food的X轴坐标
get X(){
return this.elem.offsetLeft;
}
// 获取food的Y轴坐标
get Y(){
return this.elem.offsetTop;
}

// 修改food位置
change(){
// 范围是[0, 290],且必须整10
let top = Math.round(Math.random() * 29) * 10;
let left = Math.round(Math.random() * 29) * 10;

this.elem.style.top = top + "px";
this.elem.style.left = left + "px";
}
}

2.2 Snake

蛇(snake)的构造是最复杂的,首先要明确蛇的基础属性:蛇头、蛇全身、包含蛇的容器。

  • 蛇头作用

    1. 判断是否吃到食物(蛇头位置和食物位置重叠);

    2. 判断是否撞墙(蛇头位置不能超过游戏区域);

    3. 前进的操控(通过监控按键,来让坐标改变);

    4. 判断蛇头与蛇身体是否接触;

      1
      2
      3
      4
      5
      6
      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("撞到蛇身!");
      }
      }
  • 蛇全身作用

    1. 身体移动(循环遍历身体,将后面身体位置设置为前面身体位置);

      1
      2
      3
      4
      5
      6
      7
      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";
      }
    2. 是否掉头(身体的第二个元素位置与最新蛇头位置是否重叠);

  • 蛇容器作用

    1. 吃到食物,身体增加(向容器尾部添加div)

      1
      this.elem.insertAdjacentHTML("beforeend", "<div></div>");

整体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
class Snake{
// 蛇的头部
head: HTMLElement;
// 蛇全身(包括头)
bodies: HTMLCollection;
// 获取蛇的容器
elem: HTMLElement;

constructor(){
this.elem = document.getElementById("snake")!
this.head = document.querySelector("#snake > div") as HTMLElement;
this.bodies = this.elem.getElementsByTagName("div");
console.log(this.head.offsetLeft)
}

// 获取蛇头坐标
get X(){
return this.head.offsetLeft;
}
get Y(){
return this.head.offsetTop;
}

set X(val: number){
if(this.X === val) return;
if(val < 0 || val > 290){
throw new Error("蛇撞墙了!")
}
if(this.bodies[1] && (this.bodies[1] as HTMLElement).offsetLeft === val){
// 如果发生掉头,方向不能改变
if(val > this.X){
val = this.X - 10;
}else{
val = this.X + 10;
}
}
this.moveBody();
this.head.style.left = val + "px";
this.checkCollision();
}
set Y(val: number){
if(this.Y === val) return;
if(val < 0 || val > 290){
throw new Error("蛇撞墙了!")
}
if(this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === val){
// 如果发生掉头,方向不能改变
if(val > this.Y){
val = this.Y - 10;
}else{
val = this.Y + 10;
}
}
this.moveBody();
this.head.style.top = val + "px";
this.checkCollision();
}

// 蛇增加身体
addBody(){
// 向elem中添加一个div
this.elem.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";
}
}

// 检查是否和身体相撞
checkCollision(){
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("撞到蛇身!");
}
}
}
}

2.3 SocrePanel

计分板(ScorePanel)构造分为两部分:分数(score)、速度(level)。

分数的增加非常简单,就是简单自增。速度增加要设置最大速度上限(MaxLevel)与提升速度一级的分数要求(upScore),我这里默认值都是10,也就是速度等级为[0, 9],分数每过10分提升一级。

1
2
3
4
5
6
7
8
// 加分的方法
addScore(){
this.score++;
this.scoreElem.innerHTML = this.score + "";
if(this.score % this.upScore === 0){
this.levelUp();
}
}
1
2
3
4
5
6
7
// 速度增加
levelUp(){
if(this.level < this.MaxLevel){
this.level++;
this.levelElem.innerHTML = this.level + "";
}
}

整体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class ScorePanel{
score = 0;
level = 1;

// 分数和速度所在元素,在构造函数进行初始化
scoreElem: HTMLElement;
levelElem: HTMLElement;

MaxLevel: number;
upScore: number;

constructor(MaxLevel: number = 10, upScore: number = 10){
this.scoreElem = document.getElementById("score")!;
this.levelElem = document.getElementById("level")!;
this.MaxLevel = MaxLevel;
this.upScore = upScore;
}

// 加分的方法
addScore(){
this.score++;
this.scoreElem.innerHTML = this.score + "";
if(this.score % this.upScore === 0){
this.levelUp();
}
}

// 速度增加
levelUp(){
if(this.level < this.MaxLevel){
this.level++;
this.levelElem.innerHTML = this.level + "";
}
}
}

2.4 GameControl

在引入前三个主体后,第一步进行游戏初始化,监听键盘按下的事件。

1
2
3
4
5
6
7
8
9
10
// 游戏初始化
init(){
// 绑定键盘按下的事件
document.addEventListener("keydown", this.keydownHandler.bind(this));
}

// 创建键盘按下的响应函数
keydownHandler(event: KeyboardEvent){
this.direction = event.key;
}

第二步控制蛇移动,这里要考虑兼容性问题,chrome与IE的方向键是不一样的。同时,根据是否吃到食物,去调整计分板分数与蛇的速度等级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 控制蛇移动
move(){
// 根据direction改变蛇的位置
let X = this.snake.X;
let Y = this.snake.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.checkEat(X, Y);

try{
this.snake.X = X;
this.snake.Y = Y;
}catch(e){
this.isOver = true;
alert("GAEM OVER!");
}

// 开启定时调用
!this.isOver && setTimeout(this.move.bind(this), 300 - (this.scorePanel.level - 1) * 20);
}
1
2
3
4
5
6
7
8
// 检查蛇是否吃到食物
checkEat(X: number, Y: number){
if(X === this.food.X && Y === this.food.Y){
this.food.change();
this.scorePanel.addScore();
this.snake.addBody();
}
}

整体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import Food from "./Food";
import Snake from "./Snake";
import ScorePanel from "./ScorePanel";

// 游戏控制器,控制其他类
class GameControl{
// 游戏相关基础属性
food: Food;
snake: Snake;
scorePanel: ScorePanel;

// 按键方向
direction: string = "";
// 判断游戏是否结束
isOver: boolean = false;

constructor(){
this.food = new Food();
this.snake = new Snake();
this.scorePanel = new ScorePanel(10, 6);

this.init();
this.move();
}

// 游戏初始化
init(){
// 绑定键盘按下的事件
document.addEventListener("keydown", this.keydownHandler.bind(this));
}

// 创建键盘按下的响应函数
keydownHandler(event: KeyboardEvent){
this.direction = event.key;
}

// 控制蛇移动
move(){
// 根据direction改变蛇的位置
let X = this.snake.X;
let Y = this.snake.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.checkEat(X, Y);

try{
this.snake.X = X;
this.snake.Y = Y;
}catch(e){
this.isOver = true;
alert("GAEM OVER!");
}

// 开启定时调用
!this.isOver && setTimeout(this.move.bind(this), 300 - (this.scorePanel.level - 1) * 20);
}

// 检查蛇是否吃到食物
checkEat(X: number, Y: number){
if(X === this.food.X && Y === this.food.Y){
this.food.change();
this.scorePanel.addScore();
this.snake.addBody();
}
}
}

三、效果展示

npm start进行效果展示。

  • 初始化界面

  • 游戏进行界面