昨天咱們已經把貪吃蛇的頁面寫好了,今天咱們來寫 TS 部分
TS 我們要用物件導向的形式去編寫我們的功能,所以我們要以一個功能去定義一個物件
把這個專案分成幾個模組,也就是幾個物件功能
寫物件的前提,我們要去定義類,用類去建立物件
首先我們直接 class 一個 Food 類,由於我們的食物是一個 div 所以我們的 Food 類裡面必須有一個屬性來存放我們的元素
// 食物 Food 類 class Food{ element:HTMLElement; constructor(){ // 拿到元素 id this.element = document.getElementById('food')!; } }
元素有了,現在我們想食物還有哪些功能?
我們的食物被蛇吃掉以後是不是要消失啊,那怎麼確定我的蛇吃到食物了呢,看程式碼
// 獲取食物 x 軸座標的方法 get x(){ return this.element.offsetLeft } // 獲取食物 y 軸座標的方法 get y(){ return this.element.offsetTop }
如果我們蛇的座標和食物的座標一樣了,是不是就證明我們吃到了食物,所以我們在 Food 類裡面直接寫倆 get 方法 獲取 offsetTop/Left 就獲取到了 x y 軸座標
座標有了,但我們現在食物的位置是固定的,那能固定嗎?我們的蛇吃完食物,食物的位置是不是要改變,所以我們的食物一定是隨機一個位置
// 修改食物位置 change(){ // 生成一個隨機的位置 食物的位置 最小是 0 最大是 290 // 蛇移動一次是一格,一格大小是 10 所以要求食物的座標必須是整 10 let top = Math.round(Math.random() * 29) * 10 let left = Math.round(Math.random() * 29) * 10 this.element.style.left = left + 'px' this.element.style.top = top + 'px' }
因為我們的盒子是 300 我們的食物是 10 所以我們食物的位置最大是290,最小是0
而且我們設計蛇移動一次是一格也就是 10 所以我們食物的座標必須是10的倍數
這樣我們食物的 Food 類就完成了
接下來我們再寫一個簡單的,我們的計分板,我們的計分板有兩個值,一個是積分一個是等級
還是同理,首先先獲取元素
// 計分板 定義一個 ScorePanel 類 class ScorePanel{ // 積分 score = 0 lecel = 0 // 元素 scoreSpan:HTMLElement lecelSpan:HTMLElement constructor(){ this.scoreSpan = document.getElementById('score')!; this.lecelSpan = document.getElementById('lecel')!; } }
獲取完元素之後,我們的分數和等級是不是要進行一個增加
addScore(){ // this.score++ this.scoreSpan.innerHTML = ++this.score + ''; // 判斷我們多少積分升一級 if(this.score % 10 === 0){ this.lecelup(); } } // 等級提升 lecelup(){ // 判斷是否到達最大等級 if(this.lecel < 10){ this.lecelSpan.innerHTML = ++this.lecel + ''; } }
蛇的類是我們這裡最主要的一個類也是最難寫的一個類,所以我們這裡先寫一個基礎
還是跟前面的一樣,我們先獲取元素,但是蛇這裡我們會麻煩一點,因為蛇是一個容器,我們要給他分為蛇頭和蛇身子
// 設定我們的蛇類 class Snake{ // 蛇的容器 element:HTMLElement // 蛇的頭部 head:HTMLElement // 蛇的身體(包括蛇頭的) HTMLCollection 是一個集合,特點:它是會實時的重新整理的 bodies:HTMLCollection constructor(){ // 容器元素 this.element = document.getElementById('snake')!; // 獲取蛇頭的元素也就是我們元素裡面的第一個 div this.head = document.querySelector('#snake > div')! as HTMLElement; // 獲取容器裡面的所有 div this.bodies = this.element.getElementsByTagName('div') } }
獲取完蛇元素之後,我們還需要獲取到蛇的位置,也就是蛇頭部的位置。蛇身子暫不考慮
// 獲取我們的蛇的座標(蛇頭) get x(){ return this.head.offsetLeft } get y(){ return this.head.offsetTop } // 設定我們蛇頭的座標 set x(value:number){ this.head.style.left = value + '' } set y(value:number){ this.head.style.top = value + '' }
這樣就獲取到了,設定蛇的座標我們現在先寫一個簡單的,等會再完善
現在我們先寫一個吃完食物蛇的身子增加的方法,其實這個增加就是向元素裡新增 div
// 蛇增加身體的一個方法 addBody(){ // 向 element 中新增 div this.element.insertAdjacentHTML('beforeend','<div></div>') }
這樣一個基礎的模型就寫好了,但卻還有問題,我們的座標還沒有完善,而我們蛇還不能動
到現在誰把三個類都放到一個 TS 裡面了 ?
看起來不多嗎,修改起來太墨跡了,所以我們要把這三個類差分,放到不同的 TS 檔案裡,然後每一個檔案都作為一個模組暴露出去
首先再 src 下邊建立一個 modules 的資料夾,然後建立三個 TS 檔案
然後每一個 TS 都作為模組暴露出去,程式碼為
export default class類名;
都弄完了以後我們還需要一個 TS 檔案來把我們這三個 TS 進行一個整合
新建一個 GameControl.ts 它現在就是我們的遊戲控制器,控制其他的所有類
首先先引入其他的類,然後來定義,方便使用,同時 GameControl 這個 TS 也是當一個模組去暴露出去的,給到我們最終的 index.ts
// 引入其他類 import Food from './Food' // 食物類 Food import ScorePanel from './ScorePanel' // 記分類 ScorePanel import Snake from './Snake' // 蛇 Snake // 遊戲控制器,控制其他的所有類 class GameControl{ // 定義三個屬性 food:Food // 食物 scorepanel:ScorePanel // 記分類 snake:Snake // 蛇 constructor(){ this.food = new Food(); this.scorepanel = new ScorePanel(); this.snake = new Snake(); this.init(); } } export default GameControl;
定義完之後,肯定要有一個遊戲的開始,或者一個遊戲的初始化方法
那第一件事就是讓我們的蛇可以動,首先繫結一個鍵盤事件
// 建立一個屬性來儲存我們的按鍵,蛇的移動方向 direction:string = '' // 遊戲初始化方法,呼叫之後遊戲開始 init(){ // 鍵盤摁下的事件 document.addEventListener('keydown',this.keydomnHandler.bind(this)) } // ArrowUp // ArrowDown // ArrowLeft // ArrowRight // 建立一個鍵盤按下的響應 keydomnHandler(event:KeyboardEvent){ // 使用者是否合法,用了正確的摁鍵 // 觸發按下 修改值 // console.log(event.key) this.direction = event.key }
繫結寫完了,我們現在來讓蛇動起來,也就是改蛇的 left 和 top ,向上 top 減少,向下 top 增加,left 同理
// 建立一個屬性來儲存我們的按鍵,蛇的移動方向 direction:string = '' init(){ // 鍵盤摁下的事件 document.addEventListener('keydown',this.keydomnHandler.bind(this)); this.run(); } run(){ /** * 根據方向 this.direction 來讓我們的蛇改變 * 向上 top 減少 * 向下 top 增加 * 向左 left 減少 * 向右 left 增加 */ let x = this.snake.x let y = this.snake.y // 計算我們的 x 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; }; // 修改蛇的 x y 值 this.snake.x = x; this.snake.y = y; setTimeout(this.run.bind(this), 500 - (this.scorepanel.lecel -1) * 50); }
setTinmeout 重複執行我們的程式碼,讓蛇一直動,500 的數值越大,蛇走的越慢,讓它隨著我們的等級來走,等級乘 50 就行
接下來我們來寫遊戲失敗的方法,貪吃蛇失敗無疑就是撞牆或裝自己身體
而我們再寫 class 類的時候要注意,誰的事情,儘量讓誰去處理,蛇死了,是蛇自己的事情,所以我們在 Snake 也就是蛇類,去處理
我們修改蛇的移動是呼叫了,Snake 裡 set x 和 set y,所以只要給這倆前邊加一個限制
// 設定我們蛇頭的座標 set X(value:number){ // 新值和舊值相同,不會去改 if(this.x === value){ return; } // x 值的合法範圍 也就是 0 - 290 之間 if(value < 0 || value > 290){ // 進入判斷說明我的蛇撞牆了 throw new Error('蛇撞牆了!') } this.head.style.left = value + "px" } set Y(value:number){ if(this.y === value){ return; } // x 值的合法範圍 也就是 0 - 290 之間 if(value < 0 || value > 290){ // 進入判斷說明我的蛇撞牆了 throw new Error('蛇撞牆了!') } this.head.style.top = value + 'px' }
接下來就是如何吃到食物了
checkEat(x:number,y:number){ if(x === this.food.x && y === this.food.y){ // 食物改變位置 this.food.change() // 積分版 this.scorepanel.addScore() // 蛇新增一節 this.snake.addBody() } }
我們之前就都定義好了,直接拿來用就行了,剩下的就是我們的身體如何跟頭去移動,還有撞自己身體遊戲結束的方法就完成了我們的貪吃蛇
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'; } }
this.bodies 就是蛇的身體長度,那我們蛇怎麼前進呢,就是後邊 div 的位置等於前邊 div 的位置
這樣我們的蛇身子就動起來了,接下來我們寫蛇頭撞身子游戲結束
checkHeadBody(){ // 獲取所有的身體,檢查是否和我們的蛇頭的座標發生重疊 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("撞自己了") } } }
之後我們把這個方法寫到 set x、y裡面最後一段的位置呼叫
接下來我們固定一下左右,在 set 裡面寫
// 修改 x 時候 是在修改水平座標,蛇向左移動時候不能向右 反之同理 if(this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value){ console.log("水平掉頭了") if(value > this.y){ // 新值 大於 x 舊值 掉頭 應該繼續走 value = this.y - 10 }else{ value = this.y + 10 } }
y 軸是同理的
這樣下來我們整個一個貪吃蛇就完成了
以下貼出所有程式碼
Food.ts
// 食物 Food 類 class Food{ element:HTMLElement; constructor(){ // 拿到元素 id 直接賦值 this.element = document.getElementById('food')!; } // 獲取食物 x 軸座標的方法 get x(){ return this.element.offsetLeft } // 獲取食物 y 軸座標的方法 get y(){ return this.element.offsetTop } // 修改食物位置 change(){ // 生成一個隨機的位置 食物的位置 最小是 0 最大是 290 // 蛇移動一次是一格,一格大小是 10 所以要求食物的座標必須是整 10 let top = Math.round(Math.random() * 29) * 10 let left = Math.round(Math.random() * 29) * 10 this.element.style.left = left + 'px' this.element.style.top = top + 'px' } } export default Food;
ScorePanel.ts
// 計分板 定義一個 ScorePanel 類 class ScorePanel{ // 用來記錄我們的分數和等級 score = 0 lecel = 1 // 記住我們分數和等級 元素,再建構函式中初始化 scoreSpan:HTMLElement lecelSpan:HTMLElement // 設定我們的等級最大值 maxLevel:number // 設定多少積分升一級 upScore:number constructor(maxLevel:number = 10,upScore:number = 10){ this.scoreSpan = document.getElementById('score')!; this.lecelSpan = document.getElementById('lecel')!; this.maxLevel = maxLevel; this.upScore = upScore; } // 設定一個加分的方法 addScore(){ this.score++ this.scoreSpan.innerHTML = this.score + ''; // 判斷我們多少積分升一級 if(this.score % this.upScore === 0){ this.lecelup(); } } // 等級提升 lecelup(){ // 判斷是否到達最大等級 if(this.lecel < this.maxLevel){ this.lecel++ this.lecelSpan.innerHTML = this.lecel + ''; } } } export default ScorePanel;
Snake.ts
// 設定我們的蛇類 class Snake{ // 蛇的容器 element:HTMLElement // 蛇的頭部 head:HTMLElement // 蛇的身體(包括蛇頭的) HTMLCollection 是一個集合,特點:它是會實時的重新整理的 bodies:HTMLCollection constructor(){ // 容器元素 this.element = document.getElementById('snake')!; // 獲取蛇頭的元素也就是我們元素裡面的第一個 div this.head = document.querySelector('#snake > div')! as HTMLElement; // 獲取容器裡面的所有 div this.bodies = this.element.getElementsByTagName('div')!; } // 獲取我們的蛇的座標(蛇頭) get x(){ return this.head.offsetLeft } get y(){ return this.head.offsetTop } // 設定我們蛇頭的座標 set X(value:number){ // 新值和舊值相同,不會去改 if(this.x === value){ return; } // x 值的合法範圍 也就是 0 - 290 之間 if(value < 0 || value > 290){ // 進入判斷說明我的蛇撞牆了 throw new Error('蛇撞牆了!') } // 修改 x 時候 是在修改水平座標,蛇向左移動時候不能向右 反之同理 if(this.bodies[1] && (this.bodies[1] as HTMLElement).offsetLeft === value){ console.log("水平掉頭了") if(value > this.x){ // 新值 大於 x 舊值 掉頭 應該繼續走 value = this.x - 10 }else{ value = this.x + 10 } } this.moveBody(); this.head.style.left = value + "px" ; this.checkHeadBody() } set Y(value:number){ if(this.y === value){ return; } // x 值的合法範圍 也就是 0 - 290 之間 if(value < 0 || value > 290){ // 進入判斷說明我的蛇撞牆了 throw new Error('蛇撞牆了!') } // 修改 x 時候 是在修改水平座標,蛇向左移動時候不能向右 反之同理 if(this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value){ console.log("水平掉頭了") if(value > this.y){ // 新值 大於 x 舊值 掉頭 應該繼續走 value = this.y - 10 }else{ value = this.y + 10 } } this.moveBody(); this.head.style.top = value + 'px'; this.checkHeadBody() } // 蛇增加身體的一個方法 addBody(){ // 向 element 中新增 div 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++){ let bd = this.bodies[i] as HTMLElement if(this.x === bd.offsetLeft && this.y === bd.offsetTop){ // 進入判斷說明撞了,遊戲結束 throw new Error("撞自己了") } } } } export default Snake;
GameControl.ts
// 引入其他類 import Food from './Food' // 食物類 Food import ScorePanel from './ScorePanel' // 記分類 ScorePanel import Snake from './Snake' // 蛇 Snake // 遊戲控制器,控制其他的所有類 class GameControl{ // 定義三個屬性 food:Food // 食物 scorepanel:ScorePanel // 記分類 snake:Snake // 蛇 // 建立一個屬性來儲存我們的按鍵,蛇的移動方向 direction:string = '' // 建立一個屬性來記錄我們遊戲是否結束 isLive = true constructor(){ this.food = new Food(); this.scorepanel = new ScorePanel(); this.snake = new Snake(); this.init(); } // 遊戲初始化方法,呼叫之後遊戲開始 init(){ // 鍵盤摁下的事件 console.log("執行") document.addEventListener('keydown',this.keydomnHandler.bind(this)); this.run(); } // ArrowUp // ArrowDown // ArrowLeft // ArrowRight // 建立一個鍵盤按下的響應 keydomnHandler(event:KeyboardEvent){ // 使用者是否合法,用了正確的摁鍵 // 觸發按下 修改值 console.log(event.key,1111) this.direction = event.key } // 建立蛇移動的方法 run(){ /** * 根據方向 this.direction 來讓我們的蛇改變 * 向上 top 減少 * 向下 top 增加 * 向左 left 減少 * 向右 left 增加 */ let x = this.snake.x let y = this.snake.y // 計算我們的 x 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) // 修改蛇的 x y 值 try{ this.snake.X = x; this.snake.Y = y; }catch{ // 捕獲異常,遊戲結束 alert('GAME OVER!') this.isLive = false } // isLive 開關 ture 的話重複執行 this.isLive && setTimeout(this.run.bind(this), 500 - (this.scorepanel.lecel -1) * 50); } // 定義一個方法,用來檢查是否吃到食物 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;
index.ts
import "./style/index.less" import GameControl from './modules/GameControl' new GameControl();
這裡是六扇有伊人,我們有緣再見