多模块搭建
在项目开发中我们不可能把所有的代码写到一个文件中,所以项目开发必须会灵活运用模块化开发思想,把实现的功能细化成一个个模块。
完成Food(食物)类
//定义食物类 class Food { element : HTMLElement; constructor() { //获取页面中的food元素并赋给element this.element = document.getElementById('food')!; } //获取食物x轴坐标的方法 get X() { return this.element.offsetLeft; } //获取食物y轴坐标的方法 get Y() { return this.element.offsetTop; } //修改食物位置的方法 change() { //生成随机位置 //食物的最小位置是0 最大是290 let left = Math.round(Math.random() * 29) * 10 let top = Math.round(Math.random() * 29) * 10 this.element.style.left = left + 'px'; this.element.style.top = top + 'px'; } } export default Food
代码分析:
- 由于在配置typescript时我们设置了strict(严格)模式,因此
this.element = document.getElementById('food')!中如果我们不加!会让编译器不确定我们是否会获取到food的dom元素而发生报错。
- 准备了get()方法可以在控制模块中随时获取food的具体定位。
- change()方法为随机刷新一次food的位置。
- export default Food 代码加在最后。为的是把food成为全局模块暴露出去,这样的话其他的模块可以调用这个food模块。
完成ScorePanel(记分牌)类
//定义表示记分牌的类 class ScorePanel { score : number = 0; level : number = 1; scoreSpan :HTMLElement; levelEle : HTMLElement; //设置变量限制等级 maxLevel : number; //设置一个变量多少分升级 upScore : number; constructor(maxLevel : number = 10,Score : number = 10) { this.scoreSpan = document.getElementById('score')!; this.levelEle = document.getElementById('level')!; this.maxLevel = maxLevel this.upScore = Score } //设置加分的方法 AddScore() { this.score++; this.scoreSpan.innerHTML = this.score + '' if (this.score % this.upScore === 0 ) { this.AddLevel() } } //提升等级 AddLevel() { if (this.level < this.maxLevel) { this.levelEle.innerHTML = ++this.level +'' } } } export default ScorePanel
代码分析:
在记分牌模块主要是两种方法AddScore()和AddLevel(),分别用来控制分数增加和等级提升,重点也有设置一个变量来限制等级和设置变量来判断多少分上升一个等级
完成Snake(蛇)类
class Snake { //表示蛇头的元素 head : HTMLElement; bodies : HTMLCollectionOf<HTMLElement>; //获取蛇的容器 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; } get Y() { return this.head.offsetTop; } set X(value) { if(this.X === value) { return; } if(value < 0 || value > 290) { throw new Error('蛇撞墙了!') } //修改x时,是在修改水平坐标,蛇在左右移动,蛇在向左移动时,不能向右掉头 if(this.bodies[1] && (this.bodies[1] as HTMLElement).offsetLeft === value) { //如果发生的掉头,让蛇向反方向继续移动 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; } 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大于旧值X,则说明蛇在向右走,此时应该发生掉头,应该使蛇继续向左走 value = this.Y - 10 } else { value = this.Y + 10 } } this.moveBody() this.head.style.top = value + 'px' this.checkHeadBody() } //蛇增加身体的方法 addBody() { this.element.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'; } } //检查蛇头是否撞到身体 checkHeadBody() { //获取所有的身体,检查其是否和蛇头的坐标发生重叠 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('撞到自己了') } } } } export default Snake
代码分析:
- 首先它自身只添加了三个功能函数addbody,movebody和checkHeadBody
- movebody的实现逻辑非常的巧妙,根据从后往前的顺序来确定位置,根据前一节的位置,从而让后边的位置替换到前一节的位置上,从而实现蛇可以移动的逻辑。
- 为什么get,set,判断蛇是否死亡机制以及之后的蛇移动的代码一定要写在constructor()函数中而不是写在外面?
在后面还有一个控制模块中
首先利用get()方法获得蛇头坐标,当蛇头移动一次以后,立刻刷新后的蛇头坐标反馈给蛇对象
蛇这个对象更新以后constructor代码就会执行一遍,执行过程中首先蛇头的坐标用set()函数重新设置,然后蛇的movebody函数就会执行一次。最后对蛇进行判断死没死。
这样一次代码就执行完成啦。此时整条蛇都前进了一次。然后我们通过定时器定个时间不断让蛇移动就可以了。
完成GameControl(控制)类
import Food from "./food"; import Snake from "./Snake"; import ScorePanel from "./ScorePanel"; //游戏控制器,控制其他所有类 class GameControl { snake : Snake; food : Food; scorePanel : ScorePanel direction : string = ''; //创建一个变量来判断游戏是否结束 isLive : boolean = true; constructor() { this.snake = new Snake() this.food = new Food() this.scorePanel = new ScorePanel() this.init() } //游戏的初始化,调用后游戏将开始 init() { document.addEventListener('keydown',this.keydownHandler.bind(this)) //调用run this.run() } /*ArrowUp ArrowDown ArrowLeft ArrowRight */ //创建一个键盘按下的响应函数 keydownHandler(event: KeyboardEvent) { console.log(event.key) this.direction = event.key } //创建一个控制蛇移动的方法 /* * 根据方向(this.direction)来使蛇位置发生改变 * * */ run() { 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 { //修改X和Y的值 this.snake.X = X; this.snake.Y = Y; }catch (e) { //进入到catch出现异常 alert((e as any).message + '游戏结束了,老表!'); this.isLive = false; } 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) { //食物的位置要进行重置 this.food.change() //分数增加 this.scorePanel.AddScore() //蛇要增加一节 this.snake.addBody() } } } export default GameControl
代码分析:
我们设置控制类主要目的在于整合之前的三个类,从而在这个类中调用之前声明的类,在该类中重点在于初始化游戏、控制蛇的移动、检查蛇是否吃到食物,这个类相当于一个总开关。
这里还有一个重点在于this指向问题,这里使用了bind()函数,bind最直接的定义就是将this指向到当前的对象。
完成index类(启动项目)
import './style/index.less' import GameControl from './modules/GameControl' new GameControl()
代码分析:
大家都非常清楚,想要让对象执行,我们必须要进行实例化,这里只用new一下进行调用即可,项目就可以执行了
项目启动
最后我们打开终端输入npm start或者npm run build,项目就跑起来了,它可以自动打开浏览器进行执行
总结
学习完了typescript,其实最主要的在于运用它实现面向对象的开发,我们在日常开发中基本不会用到面向对象,就算es6中涉及到类、接口等等,但是在实际中很少人去使用,面向对象的开发中其实使得项目变得更加的严谨和合理化,我们在书写代码的时候会更加的规范,ts的类型严格更加的使它方便大型项目开发!