|
通过分析,将整个游戏拆分成四个对象,食物、蛇、记分牌和游戏控制器
- 食物类
属性:食物的dom
方法:1. 获取食物dom左上角坐标
2. 食物被吃后,随机修改食物位置的方法
// modules/food.tsexport default class Food { element: HTMLElement; constructor() { this.element = document.getElementById('food')!; } get X() { return this.element.offsetLeft; } get Y() { return this.element.offsetTop; } // 修改食物的位置 change() { // Math.random()生成0~1的隨機數,不包括0和1,乘以29就生成0~29的随机数,不包括0和29,然后再四舍五入,就可以得到包括0~29的数字 // stage的宽高是304,减去边框为300,食物的宽高为10,一次移动一格,所以食物左上角坐标的范围是0~290,且为整十的数 const x = Math.round(Math.random() * 29) * 10; const y = Math.round(Math.random() * 29) * 10; // todo 食物也不应该出现在蛇的身体上 this.element.style.left = x + 'px'; this.element.style.top = y + 'px'; }}
- 记分牌类
属性:最大等级、升级需要的分数、分数的dom、等级的dom、分数、等级
方法:1. 加分
2. 升级
// modules/scorePanel.tsexport default class ScorePanel { score = 0; level = 1; scoreEle: HTMLElement; levelEle: HTMLElement; maxLevel: number; upScore: number; constructor(maxLevel:number = 10, upScore:number = 5) { this.scoreEle = document.getElementById('score')!; this.levelEle = document.getElementById('level')!; this.maxLevel = maxLevel; this.upScore = upScore; } // 加分 addScore() { this.scoreEle.innerHTML = ++this.score + ''; if (this.score % this.upScore === 0) { this.levelUp() } } // 升级 levelUp() { if (this.level < this.maxLevel) { this.levelEle.innerHTML = ++this.level + ''; } }}
- 蛇类
属性:蛇头dom、蛇身体dom(包括蛇头)、蛇容器dom
方法:1. 获取蛇头坐标
2. 设置蛇头坐标(检查是否掉头、检查是否撞墙、移动蛇身体(将每一节身体都移动到前一节身体的位置上)、检查是否撞到自己的身体)
3. 增加蛇身体(吃到食物后)
// modules/snake.tsexport default class Snake { // 蛇的容器 element: HTMLElement; // 蛇头 head: HTMLElement; // 蛇的身体 bodies: HTMLCollection; constructor() { // 这里不加!会ts报错,因为有可能没有#snake这个dom,但我们知道有,通过加一个!阻止报错 this.element = document.getElementById('snake')!; // querySelector方法获取的是一个Element对象,与我们设置的HTMLElement不匹配,所以这里我们使用类型断言as this.head = document.querySelector('#snake > div') as HTMLElement; this.head.style.background = 'red'; this.bodies = this.element.getElementsByTagName('div')!; } // get方法是ts类中的语法糖,你还是可以通过snake1.x的方法获取该属性,但我们可以在这个get方法中写入判断逻辑,将控制权握在开发者手里 // 获取蛇头的x坐标 get X() { return this.head.offsetLeft; } get Y() { return this.head.offsetTop; } set X(value: number) { if (this.X === value) { return; } // 检查是否掉头 value = this.checkIsTurn(value, this.X, 'X') // 检查是否撞墙 this.checkWall(value) // 移动身体 this.moveBodies() this.head.style.left = value + 'px' // 检查是否撞到自己的身体 this.checkBoomBody(value) } set Y(value: number) { if (this.Y === value) { return; } // 检查是否掉头 value = this.checkIsTurn(value, this.Y, 'Y') // 检查是否撞墙 this.checkWall(value) // 移动身体 this.moveBodies() this.head.style.top = value + 'px' // 检查是否撞到自己的身体 this.checkBoomBody(value) } // 检查是否撞墙 checkWall(value:number) { if (value < 0 || value > 290) { throw new Error('蛇撞墙了!'); } } // 检查是否掉头 checkIsTurn(value:number, currentValue: number, type:string) { const firstBody = this.bodies[1] as HTMLElement; const position = type === 'Y' ? firstBody?.offsetTop : firstBody?.offsetLeft; if (value === position) { // 如果发生了掉头,让蛇向反方向继续移动 if (value > currentValue) { value = currentValue - 10 } else { value = currentValue + 10 } } return value } // 检查是否撞到自己的身体 这个方法要在本次蛇的坐标更改后再进行判断 checkBoomBody(value:number) { for(let i = 1; i < this.bodies.length; i++) { const bd = this.bodies as HTMLElement if (this.X === bd.offsetLeft && this.Y === bd.offsetTop) { throw new Error('撞到了自己的身体!'); } } } addBodies() { this.element.insertAdjacentHTML('beforeend', "<div></div>") } moveBodies() { // 后面一格身体的坐标修改为前一节身体的坐标,以此类推 // 这里for循环的停止条件是i > 0,因为蛇头的位置在外面已经设置了 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 as HTMLElement).style.left = X + 'px'; (this.bodies as HTMLElement).style.top = Y + 'px'; } }}
- 控制器类
属性:蛇、食物、记分牌(引用并创建实例)、蛇移动的方向(按键方向)、游戏是否结束
方法:1. 开始游戏(绑定键盘事件、开启定时器更改蛇的坐标)
2. 蛇吃食物
// modules/gameController.tsimport Food from './food'import Snake from './snake'import ScorePanel from './scorePanel'export default class GameController { food: Food; snake: Snake; scorePanel: ScorePanel; // 按键的方向(蛇移动的方向) direction: string = ''; // 游戏是否结束 isLive = true; constructor() { this.food = new Food() this.snake = new Snake() this.scorePanel = new ScorePanel() // 创建对象后即游戏开始 this.init() } init() { console.log('游戏开始') // 绑定键盘事件 document.addEventListener('keydown', this.keydownHandler.bind(this)) this.run() } /** * ArrowUp Up * ArrowDown Down * ArrowLeft Left * ArrowRight Right * 上面是键盘按上下左右后返回的event.key * 由于IE浏览器不合群,所以事件名称和其他浏览器有区别 * */ keydownHandler(event: KeyboardEvent) { // 根据event.key来判断上下左右 this.direction = event.key; } 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; default: } // 检查蛇是否吃到了食物 this.checkEat(X, Y) try { this.snake.X = X; this.snake.Y = Y; } catch (e) { alert(e.message+ 'Game Over!') 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.snake.addBodies() this.food.change() this.scorePanel.addScore() } }}
以上,整个贪吃蛇的游戏就完成了,大家可以在以下链接下载完成代码
https://gitee.com/loversong/snake
npm inpm run start
命令行执行以上代码即可体验。
以上代码还遗留了一个bug,就是在移动食物的时候,没有考虑到食物随机分布到蛇身体上的情况,大家可以自己尝试补一下。
还有什么问题,欢迎评论区指正。 |
|