TS练习 贪吃蛇案例

TS练习 贪吃蛇案例

环境配置

使用npm管理包,初始化项目,生成包管理文件package.json

npm init -y

安装打包工具webpack
webpack命令行工具webpack-cli可以通过命令行使用webpack
ts-loader:ts编译器在webpack中使用

cnpm i -D webpack webpack-cli typescript ts-loader # -D 开发依赖

package.json 新增build:"webpack"可以使用build来使用webpack

在这里插入图片描述

ts编译文件tsconfig.json

{
  "compilerOptions": {
    "module": "ES2015",
    "target": "ES2015",
    "strict": true,
    "noEmitOnError": true
  }
}

webpack配置文件webpack.config.js

创建src/index.ts为入口文件

需求1:将ts转换为js文件

// webpack中的所有的配置信息都应该写在module.exports中
module.exports = {
	mode:"development",//开发模式
    // 指定入口文件
    entry: "./src/index.ts",

    // 指定打包文件所在目录
    output: {
        // 指定打包文件的目录
        path: path.resolve(__dirname, 'dist'),
        // 打包后文件的文件
        filename: "bundle.js",
    },

    // 指定webpack打包时要使用模块
    module: {
        // 指定要加载的规则
        rules: [
            {
                // test指定的是规则生效的文件
                test: /\.ts$/,
                // 要使用的loader
                use: 'ts-loader',
                // 要排除的文件,b不需要被编译
                exclude: /node-modules/
            },
        ]
    },
};

需求2: 配置的目的是将js放入网页html中,html文件由webpack自动创建,html中需要引入哪些资源由webpack根据配置引入

1.需要下载插件html-webpack-plugin,帮助我们自动生成html文件 --> 下载成功package.json中会有记录

cnpm i -D html-webpack-plugin

2.修改webpack,增加如下代码

// 1.引入html插件
const HTMLWebpackPlugin = require('html-webpack-plugin');
//2.配置webpacl插件
module.exports = {
//.....

    // 配置Webpack插件
    plugins: [
    	//传实例对象,参数是配置项,可以在src下新建index.html作为模板,生成的dist的html会参考设置的模板
        new HTMLWebpackPlugin(),
    ],
}

使用npm run build 打包,打包成功
在这里插入图片描述

需求3:当项目修改后,自动重新构建项目,浏览器自动刷新展示最新结果 --> 实时更新修改

webpack的开发服务器webpack-dev-server

cnpm i -D webpack-dev-server

修改package.json

module.exports = {
//.....
"scripts": {
//....
    "start": "webpack serve --open" //使用start命令启动webpack服务器用浏览器打开
 }
//使用:npm run start,启动后自动打开服务器
}

**需求4:**默认打包时不会删除旧文件,当文件名一样时新文件会替换旧文件。需要每次编译前,先清空dist文件夹(防止删除了一些不要的文件后旧得dist还存在),然后再放入新文件

1.需要下载插件clean-webpack-plugin 帮助我们删除dist文件夹

cnpm i -D clean-webpack-plugin

2.修改webpack,增加如下代码

//1.引入clean插件
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
//2.注册插件
module.exports = {
//.....
    plugins: [
    	//....
        new CleanWebpackPlugin()//webpack5可以不用这个插件
    ]
}

需求5: 设置哪些文件可以作为模块去引用

修改webpack,增加resolve配置

module.exports = {
//.....
    // 用来设置引用模块
    resolve: {
        extensions: ['.ts', '.js']//ts和js结尾的都可以作为模块使用
    }
}

**需求6:**设置less文件的处理

1.安装less依赖

cnpm i -D less less-loader css-loader style-loader postcc postcss-loader postcss-preset-env

2.修改webpack文件

module: {
     // 指定要加载的规则
     rules: [
    // 设置less文件的处理
         {
           test: /\.less$/,
           use: [
              "style-loader",
               "css-loader",

               // 引入postcss
               // 类似于babel,把css语法转换兼容旧版浏览器的语法
               {
               loader: "postcss-loader",
                options: {
                postcssOptions: {
                   plugins: [
                          [// 浏览器兼容插件
                                        "postcss-preset-env",
                        {
                                            // 每个浏览器最新两个版本
                                            browsers: 'last 2 versions'
                                        }
                                    ]
                                ]
                            }
                        }
                    },
                    "less-loader"
         ]
}

新建style/index.less 文件编写样式
在index.ts中引入样式文件import './style/index.less'

babel 处理兼容问题

ts编译配置可以使用target指定编译之后的版本,主要是进行语法转换。但是还有很多功能不能仅仅通过语法转化。

babel: 新语法–> 旧语法,支持promise、类等新技术

Babel进行ES6代码转ES5代码时,转换后默认严格模式

1.下载babel
@bable/core:npm核心包
@babel/preset-env:预设不同环境,不同环境有不同转码规则?
xx-loader:与webpack结合的一个工具
core.js: 模拟js运行环境,老版本浏览器用到新技术

cnpm i -D @babel/core @babel/preset-env babel-loader core.js

2.修改webpack,增加加载器,加载器的加载顺序从后往前加载

module: {
    // 指定要加载的规则
    rules: [
       {
        // test指定的是规则生效的文件
        test: /\.ts$/,
        // 要使用的loader
         use: [
         {//使用对象配置babel
         	loader:"babel-loader"
         	options:{//设置预设环境
				  presets: [
                                [
                                    // 指定环境的插件
                                    "@babel/preset-env",
                                    // 配置信息
                                    {
                                        // 要兼容的目标浏览器
                                        targets: {
                                            "chrome": "58",
                                            "ie": "11"
                                        },
                                        // 指定corejs的版本
                                        "corejs": "3",
                                        // 使用corejs的方式 "usage" 表示按需加载
                                        "useBuiltIns": "usage"
                                    }
                                ]
                            ]
			}
         },
         'ts-loader'
         ]
        // 要排除的文件,b不需要被编译
         exclude: /node-modules/
         }
      ]
}

编写ts代码

界面

我上传到github了

食物类 Food.ts

创建src/moduls/Food.ts

类中应该有一个属性表示食物本身,由于在页面中设置好了食物,我们可以从页面中获取食物。
所以食物的数据类型是HTMLElement

说明
编译器认为document.getElementById(‘food’)可能为null,但是我们知道食物一定不为空,所以使用非空断言操作符!告诉编译器一定不为空

class Food{
    //定义一个属性表示食物对应的元素,食物在页面中设置好了,所以类型为HTMLElement
    element:HTMLElement;
    constructor(){
        //!非空断言,表示document.getElementById('food')不可能为空
        this.element = document.getElementById('food')!;
    }
}

判断食物是否被蛇吃的依据是:食物的左边和蛇的坐标一致,所以需要两个方法来返回食物的x轴和y轴

get X(){
  return this.element.offsetLeft;
}
get Y(){
    return this.element.offsetTop;
}

说明
存取器方法,当使用X的时候调用get X(),使用Y的时候调用get Y()

食物被吃掉之后,需要改变位置随机到另外的位置去,这是行为可以写成一个函数
根据舞台大小,食物的范围应该是[0,290],但是蛇每次移动一格一个是10px,为了蛇能吃到食物,食物的位置应该是10的倍数Math.round(Math.random()*29)*10

    change(){
        //生成一个随机的位置[0,290],蛇每次移动一格一个是10px,为了蛇能吃到食物,食物的位置应该是10的倍数
        const top = Math.round(Math.random()*29)*10;//Math.round四舍五入取整
        const left = Math.round(Math.random()*29)*10;
        this.element.style.left = `${top}px`;
        this.element.style.top = `${left}px`;
    }

完整代码

//定义食物类
class Food{
    //定义一个属性表示食物对应的元素,食物在页面中设置好了,所以类型为HTMLElement
    element:HTMLElement;
    constructor(){
        //!非空断言,表示document.getElementById('food')不可能为空
        this.element = document.getElementById('food')!;
    }
    //获取食物坐标,当蛇的坐标和食物坐标重叠后,表示食物被蛇吃了
    get X(){
        return this.element.offsetLeft;
    }
    get Y(){
        return this.element.offsetTop;
    }
    change(){
        //生成一个随机的位置[0,290],蛇每次移动一格一个是10px,为了蛇能吃到食物,食物的位置应该是10的倍数
        const top = Math.round(Math.random()*29)*10;//Math.round四舍五入取整
        const left = Math.round(Math.random()*29)*10;
        this.element.style.left = `${top}px`;
        this.element.style.top = `${left}px`;
    }
}
export default Food;

记分类 ScorePanel.ts

创建src/moduls/ScorePanel.ts

与食物类一样,记分类也需要和页面元素进行一一对应。

class ScorePanel{
    score=0;
    level=1;
    maxLeval:number;//设置一个变量限制等级
    upScore:number;//设置一个变量表示多少分升级
    scoreEle:HTMLElement;
    levelEle:HTMLElement;
    constructor(maxLeval:number=10,upScore:number=10){//不传值就默认10
        this.scoreEle = document.getElementById('score')!;
        this.levelEle = document.getElementById('level')!;
        this.maxLeval = maxLeval;
        this.upScore = upScore;
    }
}

分数和等级是可以增加的,所以需要有修改这两个值的方法。没10分会升级一次

//设置加分的方法
addScore(){
     this.score++;
     this.scoreEle.innerHTML = this.score + '';
     if(this.score % this.upScore ===0 ){
        this.levelUp();
    }
}
//设置等级升级的方法
levelUp(){
     if(this.level<maxLeval){//等级有上限
        this.level++;
        this.levelEle.innerHTML = this.level + ''; //需要是字符串类型
        }
}

完整代码

class ScorePanel{
    score=0;
    level=1;
    maxLeval:number;//设置一个变量限制等级
    upScore:number;//设置一个变量表示多少分升级
    scoreEle:HTMLElement;
    levelEle:HTMLElement;
    constructor(maxLeval:number=10,upScore:number=10){//不传值就默认10
        this.scoreEle = document.getElementById('score')!;
        this.levelEle = document.getElementById('level')!;
        this.maxLeval = maxLeval;
        this.upScore = upScore;
    }
    //设置加分的方法
    addScore(){
        this.score++;
        this.scoreEle.innerHTML = this.score + '';
        if(this.score % this.upScore ===0 ){
            this.levelUp();
        }
    }
    //设置等级升级的方法
    levelUp(){
        if(this.level<this.maxLeval){//等级有上限
            this.level++;
            this.levelEle.innerHTML = this.level + ''; //需要是字符串类型
        }
    }
}
export default ScorePanel;

蛇类snake

snake只是容器,我们真正想操作的是该容器下的div,比如蛇变长,div增加

   <div id="snake">
       <!--snake内部的div 表示蛇的各部分-->
          <div></div>
</div>

我们将蛇分为蛇头和蛇身,蛇头回去吃食物,蛇身会增加

class Snake {
      head:HTMLElement;//head表示蛇头
     //表示蛇的身体。包括蛇头,当snake里添加新元素HTMLCollection会自动收集
    bodies:HTMLCollection;
    //获取蛇容器
    element:HTMLElement;
    constructor() {
        this.head = document.querySelector("#snake>div")!;
        //querySelectorAll返回的是NodeList,获取当前状态的节点,如果有新增节点,需要重新获取
        //this.bodies = document.querySelectorAll("#snake>div")!;
        this.element = document.querySelector("#snake")!;
        this.bodies = this.element!.getElementsByTagName('div');
    }
}

说明
HTMLCollection:如果有新增元素会自动收集
NodeList:querySelectorAll返回的是NodeList,获取当前状态的节点,如果有新增节点,需要重新获取

蛇吃食物的标准是蛇头与食物的坐标一致,那么我们需要获取蛇的坐标,吃到食物后蛇身体要变长

//获取蛇的坐标
get top(){
    return this.head.offsetTop;
}
get left(){
    return this.head.offsetLeft;
}
set top(value){
    if(this.top ===value)return;//没有发生变化就不修改
    this.head.style.top = value +'px';
}
set left(value){
     if(this.top ===value)return;//没有发生变化就不修改
     this.head.style.left = value +'px';
}
 //蛇吃到食物会增加身体
addBody(){
        //在结束标签之前添加元素
        this.element.insertAdjacentHTML("beforeend","<div></div>");
}

游戏控制器 FanmeControl.ts

需要将各个类联系起来,控制游戏的发展
比如蛇吃到食物后,记分牌增加等等,所以我们需要获取各个类的实例,类是抽象的,实例才是具体的

import Food from "./Food";
import Snake from "./snake";
import ScorePanel from "./ScorePanel";

//游戏控制器,控制其他所有类
class GameControl{
    snake:Snake;
    food:Food;
    scorePanel:ScorePanel;
    constructor(){//获取实例
        this.snake = new Snake();
        this.food = new Food();
        this.scorePanel = new ScorePanel();
        this.init();//游戏的初始化调用后表示游戏开始.后面会写
    }
}
export default GameControl;

此时GameControl.ts就作为游戏的入口了,修改index.ts

import './style/index.less'
//游戏控制
import GameControl from './moduls/GameControl'
new GameControl();

游戏的初始化,需要初始化什么?
游戏开始后,蛇可以根据上下左右开始动了,所以我们需要监听键盘按下的事件
使用字符串存储按键方便之后使用

//创建一个属性来存储蛇的移动方向,比如蛇往左移动后不能立即向右移动,所以我们需要先保存起来
direction='';
    
//游戏的初始化,调用后游戏开始
init(){
      document.addEventListener('keydown',this.keydownHandle.bind(this));
    }
    //创建一个键盘按下的响应函数
     /* event.key,字符串
        ArrowRight
        ArrowLeft
        ArrowUp
        ArrowDown
    */
    
keydownHandle(event:KeyboardEvent){
    this.direction=event.key;    
}

说明
如果监听的时候使用的是this.keydownHandle
事件监听绑定在document元素上,所以回调是绑定在document元素上,那么keydownHandle函数里的this是指向的document元素,我们并不能获取到GameControl里的direction属性
所以我们需要改变keydownHandle里的this指向GameControl的实例,这样才可以获取到GameControl里的属性

监听按键之后,蛇就需要动起来了,蛇怎么才能动起来?改变偏移位置
蛇需要一直动,我们可以在run里开启一个定时器setTimeout到时间会调用一次
给游戏设置一个属性表示游戏是否结束,当游戏没结束时按下按键蛇才会动,游戏结束后蛇不会动了

isLive = true;//表示游戏的是否结束
//游戏的初始化,调用后游戏开始
init(){
       document.addEventListener('keydown',this.keydownHandle.bind(this));
        this.run();
    }
    run(){//根据方法改变蛇的位置,方向可能是四个方法
        //这里top和left只是计算,不会调用snake的setter
        
        let X = this.snake.left;
        let Y = this.snake.top;
        switch (this.direction) {
            case "ArrowUp":
                Y-=10;
                break;
            case "ArrowDown":
                Y+=10;
                break;
            case "ArrowLeft":
                X-=10;
                break;    
            case "ArrowRight":
                X+=10;
                break;
        }
        if(this.checkEat(X,Y)){
            console.log("吃到食物了");
            
        }
        
        //修改蛇的坐标值,调用snake的setter
        try{
            this.snake.top = Y;
            this.snake.left = X;
        }catch(e){
            alert("蛇撞墙了,游戏结束了")
            this.isLive = false;
        }
        //开启定时调用,定时器回调window调用
        this.isLive && setTimeout(this.run.bind(this), 300-(this.scorePanel.level-1)*30); //蛇开始动了应该一直动
    }
蛇穿墙和吃食检测

蛇撞墙死了应该是蛇自己的行为,那么我们写在Snake.ts里,蛇撞墙了就抛出错误,GameControl类捕获错误

//snake.ts
    set top(value){
        if(this.top ===value)return;//没有发生变化就不修改
        //top的合法范围在0-290之间,蛇撞墙了
        if(value<0 || value>290){
            throw new Error("蛇撞墙了");
        }
        this.head.style.top = value +'px';
    }
    set left(value){
        if(this.left===value)return;//没有发生变化就不修改
         //left的合法范围在0-290之间,蛇撞墙了
         if(value<0 || value>290){
            throw new Error("蛇撞墙了");
        }
        this.head.style.left = value +'px';
    }
//GameControl.ts
    run(){
    //......
        //修改蛇的坐标值,调用snake的setter
        try{
            this.snake.top = Y;
            this.snake.left = X;
        }catch(e){
            alert("蛇撞墙了,游戏结束了")
            this.isLive = false;
        }
       //.....
    }

检查蛇是否吃到了食物,这里涉及到蛇和食物,当蛇头的坐标和食物坐标一致就吃到了,所以我们放在GameControl里。
吃到食物后
1.分数增加
2.食物的位置需要进行重置
3.蛇身体要增加一节

run(){
	//...
	this.checkEat(X,Y);
	//....
}
 //定义一个方法,用来检查蛇是否吃到食物
    checkEat(X:number,Y:number){ 
        if(X===this.food.X && Y===this.food.Y){
                //食物的位置要进行重置
                this.food.change();
                //分数增加
                this.scorePanel.addScore();  
                //蛇要增加一节
                this.snake.addBody();   
        }
    }
蛇身体的移动

我们发现蛇的身体是增加了一节,但是蛇身是没有动的,所以我们在snake类中修改

蛇的身体怎么移动?
每一块的新坐标应该变成它前一块的旧坐标,修改应该从后往前修改,这样才知道它前一块的旧位置在哪里

set top(value){
    if(this.top ===value)return;//没有发生变化就不修改
    //top的合法范围在0-290之间,蛇撞墙了
    if(value<0 || value>290){
       throw new Error("蛇撞墙了");
     }
     this.moveBody();//从后往前变
     this.head.style.top = value +'px';    
}
set left(value){
    if(this.left ===value)return;//没有发生变化就不修改
    //left的合法范围在0-290之间,蛇撞墙了
    if(value<0 || value>290){
        throw new Error("蛇撞墙了");
    }
    this.moveBody(); //从后往前变
    this.head.style.left = value +'px';
}
//蛇身体移动
moveBody(){
    /*
    每一块的新坐标应该变成它前一块的旧坐标,修改应该从后往前修改,这样才知道它前一块的旧位置在哪里
    */
   for(let i=this.bodies.length-1;i>0;i--){  
        /*
        bodies是HTMLCollection类型是Element接口,实际类型是HTMLElement,
        所以会报错Element没有xxx属性,HTMLElement是Eelement的子类,增加断言
        */
        (<HTMLElement>this.bodies[i]).style.left = (<HTMLElement>this.bodies[i-1]).offsetLeft +'px';
        (<HTMLElement>this.bodies[i]).style.top = (<HTMLElement>this.bodies[i-1]).offsetTop +'px';
    }    
}

判断蛇是否撞上自己判断的就是蛇头的XY有没有和身体中的其中一个坐标重合

set top(value){
	//...
    this.checkHeadBody();
}
set left(value){
  //....
  this.checkHeadBody();
}
// 检查蛇是否撞上了自己的身体
checkHeadBody() {
    for (let i = 1; i < this.bodies.length; i++) {
        let body = <HTMLElement>this.bodies[i];
         if (this.left === body.offsetLeft && this.top === body.offsetTop) {
            throw new Error("蛇撞自己了")
      }
    }
  }

其次是掉头问题,蛇是不可以掉头的,还有如果本来就是这个方向了,在按这个方向也应该是不起作用的。还有一个问题,按除了方向键之外的按键时,蛇会暂停。

修改的是GameControl.ts的keydownHander方法

keydownHandle(event:KeyboardEvent){
       //如果按键和当前方向相同,则不改变
       if (this.direction === event.key) return;
       //蛇不能掉头,那么遇见当前水平方向应该也是不起作用的,也就是不修改当前方向
       switch(event.key){
		case"ArrowUp":
		case"ArrowDown":
			if(this.direction === "ArrowUp"||this.direction ==="ArrowDown")return;
			this.direction = event.key;
			break;

		case"ArrowLeft":
		case"ArrowRight":
			if(this.direction === "ArrowRight"||this.direction ==="ArrowLeft")return;
			this.direction = event.key;
			break;
		//除了方向键之外的键不理会
		default:
            break;
 		}  
}
食物刷新问题

之前食物是随机刷新的,但是存在问题食物会刷新在蛇的身体中,我们需要避免这个问题。
食物的XY如果和蛇的身体坐标一致,那么就重新刷新。

在GameControl.ts的run方法后,checkEat来判断是否蛇吃到了食物,如果吃到了食物刷新调用change(),我们可以在这里传递蛇,修改change方法

//GameControl.ts
checkEat(X:number,Y:number,){ 
    if(X===this.food.X && Y===this.food.Y){
         //食物的位置要进行重置
       this.food.change(this.snake.bodies);
        //分数增加
        this.scorePanel.addScore();  
        //蛇要增加异界
        this.snake.addBody();   
        }
} 
//Food.ts
change(snakeBody:HTMLCollection){
        //生成一个随机的位置[0,290],蛇每次移动一格一个是10px,为了蛇能吃到食物,食物的位置应该是10的倍数
   const top = Math.round(Math.random()*29)*10;//Math.round四舍五入取整
   const left = Math.round(Math.random()*29)*10;
   let foodInSnake = false;
   for(let i=0;i<snakeBody.length;i++){//判断食物是不是刷新在了蛇身里
      let body = <HTMLElement>snakeBody[i];
      if (left === body.offsetLeft && top === body.offsetTop) {
       foodInSnake = true
       break; //找到了就不用再找了
       }
}
        if(foodInSnake){
            this.change(snakeBody);//重新生成新坐标
        }else{
            this.element.style.left = `${top}px`;
            this.element.style.top = `${left}px`;
        }
       
    }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值