通過(guò)分析裆熙,將整個(gè)游戲拆分成四個(gè)對(duì)象端礼,食物、蛇入录、記分牌和游戲控制器
- 食物類
屬性:食物的dom
方法:1. 獲取食物dom左上角坐標(biāo)
2. 食物被吃后蛤奥,隨機(jī)修改食物位置的方法
// modules/food.ts
export 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的隨機(jī)數(shù),不包括0和1僚稿,乘以29就生成0~29的隨機(jī)數(shù)凡桥,不包括0和29,然后再四舍五入蚀同,就可以得到包括0~29的數(shù)字
// stage的寬高是304唬血,減去邊框?yàn)?00,食物的寬高為10唤崭,一次移動(dòng)一格拷恨,所以食物左上角坐標(biāo)的范圍是0~290,且為整十的數(shù)
const x = Math.round(Math.random() * 29) * 10;
const y = Math.round(Math.random() * 29) * 10;
// todo 食物也不應(yīng)該出現(xiàn)在蛇的身體上
this.element.style.left = x + 'px';
this.element.style.top = y + 'px';
}
}
- 記分牌類
屬性:最大等級(jí)谢肾、升級(jí)需要的分?jǐn)?shù)腕侄、分?jǐn)?shù)的dom、等級(jí)的dom芦疏、分?jǐn)?shù)冕杠、等級(jí)
方法:1. 加分
2. 升級(jí)
// modules/scorePanel.ts
export 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()
}
}
// 升級(jí)
levelUp() {
if (this.level < this.maxLevel) {
this.levelEle.innerHTML = ++this.level + '';
}
}
}
- 蛇類
屬性:蛇頭dom、蛇身體dom(包括蛇頭)酸茴、蛇容器dom
方法:1. 獲取蛇頭坐標(biāo)
2. 設(shè)置蛇頭坐標(biāo)(檢查是否掉頭分预、檢查是否撞墻、移動(dòng)蛇身體(將每一節(jié)身體都移動(dòng)到前一節(jié)身體的位置上)薪捍、檢查是否撞到自己的身體)
3. 增加蛇身體(吃到食物后)
// modules/snake.ts
export default class Snake {
// 蛇的容器
element: HTMLElement;
// 蛇頭
head: HTMLElement;
// 蛇的身體
bodies: HTMLCollection;
constructor() {
// 這里不加笼痹!會(huì)ts報(bào)錯(cuò),因?yàn)橛锌赡軟](méi)有#snake這個(gè)dom酪穿,但我們知道有凳干,通過(guò)加一個(gè)!阻止報(bào)錯(cuò)
this.element = document.getElementById('snake')!;
// querySelector方法獲取的是一個(gè)Element對(duì)象被济,與我們?cè)O(shè)置的HTMLElement不匹配救赐,所以這里我們使用類型斷言as
this.head = document.querySelector('#snake > div') as HTMLElement;
this.head.style.background = 'red';
this.bodies = this.element.getElementsByTagName('div')!;
}
// get方法是ts類中的語(yǔ)法糖,你還是可以通過(guò)snake1.x的方法獲取該屬性只磷,但我們可以在這個(gè)get方法中寫入判斷邏輯经磅,將控制權(quán)握在開(kāi)發(fā)者手里
// 獲取蛇頭的x坐標(biāo)
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)
// 移動(dòng)身體
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)
// 移動(dòng)身體
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) {
// 如果發(fā)生了掉頭,讓蛇向反方向繼續(xù)移動(dòng)
if (value > currentValue) {
value = currentValue - 10
} else {
value = currentValue + 10
}
}
return value
}
// 檢查是否撞到自己的身體 這個(gè)方法要在本次蛇的坐標(biāo)更改后再進(jìn)行判斷
checkBoomBody(value:number) {
for(let i = 1; i < this.bodies.length; i++) {
const bd = this.bodies[i] as HTMLElement
if (this.X === bd.offsetLeft && this.Y === bd.offsetTop) {
throw new Error('撞到了自己的身體预厌!');
}
}
}
addBodies() {
this.element.insertAdjacentHTML('beforeend', "<div></div>")
}
moveBodies() {
// 后面一格身體的坐標(biāo)修改為前一節(jié)身體的坐標(biāo)阿迈,以此類推
// 這里for循環(huán)的停止條件是i > 0,因?yàn)樯哳^的位置在外面已經(jīng)設(shè)置了
for(let i = this.bodies.length - 1; i > 0; i--) {
// 獲取前一格身體的坐標(biāo)
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';
}
}
}
- 控制器類
屬性:蛇、食物配乓、記分牌(引用并創(chuàng)建實(shí)例)、蛇移動(dòng)的方向(按鍵方向)惠毁、游戲是否結(jié)束
方法:1. 開(kāi)始游戲(綁定鍵盤事件犹芹、開(kāi)啟定時(shí)器更改蛇的坐標(biāo))
2. 蛇吃食物
// modules/gameController.ts
import Food from './food'
import Snake from './snake'
import ScorePanel from './scorePanel'
export default class GameController {
food: Food;
snake: Snake;
scorePanel: ScorePanel;
// 按鍵的方向(蛇移動(dòng)的方向)
direction: string = '';
// 游戲是否結(jié)束
isLive = true;
constructor() {
this.food = new Food()
this.snake = new Snake()
this.scorePanel = new ScorePanel()
// 創(chuàng)建對(duì)象后即游戲開(kāi)始
this.init()
}
init() {
console.log('游戲開(kāi)始')
// 綁定鍵盤事件
document.addEventListener('keydown', this.keydownHandler.bind(this))
this.run()
}
/**
* ArrowUp Up
* ArrowDown Down
* ArrowLeft Left
* ArrowRight Right
* 上面是鍵盤按上下左右后返回的event.key
* 由于IE瀏覽器不合群,所以事件名稱和其他瀏覽器有區(qū)別
*
*/
keydownHandler(event: KeyboardEvent) {
// 根據(jù)event.key來(lái)判斷上下左右
this.direction = event.key;
}
run() {
// 獲取蛇現(xiàn)在的坐標(biāo)
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
}
// 開(kāi)啟一個(gè)定時(shí)器
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()
}
}
}
以上腰埂,整個(gè)貪吃蛇的游戲就完成了,大家可以在以下鏈接下載完成代碼
https://gitee.com/loversong/snake
npm i
npm run start
命令行執(zhí)行以上代碼即可體驗(yàn)蜈膨。
以上代碼還遺留了一個(gè)bug屿笼,就是在移動(dòng)食物的時(shí)候,沒(méi)有考慮到食物隨機(jī)分布到蛇身體上的情況翁巍,大家可以自己嘗試補(bǔ)一下驴一。
還有什么問(wèn)題,歡迎評(píng)論區(qū)指正灶壶。