JavaScript TypeScript實(shí)現(xiàn)貪吃蛇游戲完整詳細(xì)流程
項(xiàng)目背景及簡介
typescript系列到這篇文章正式進(jìn)入尾聲了,我們通過以上學(xué)習(xí)ts的知識,想要熟悉的掌握必須要寫一個小demo綜合運(yùn)用所學(xué)的知識,這個項(xiàng)目的目的就是綜合ts所學(xué)知識,實(shí)現(xiàn)面向?qū)ο蟮膶?shí)際開發(fā)!
項(xiàng)目地址 : https://gitee.com/liuze_quan/ts-greedy-snake
多模塊需求分析
場景模塊需求
- 具有長和寬的容器,容器內(nèi)分成蛇移動和記分牌兩個板塊
- 蛇移動的場景設(shè)置邊界線,邊界線一旦觸碰直接結(jié)束游戲
- 記分牌記錄蛇吃到食物的分?jǐn)?shù)以及等級
- 蛇吃到食物分?jǐn)?shù)漲一分,每漲一定分?jǐn)?shù)等級提高一級
- 等級設(shè)有上限
- 等級和蛇移動速度有關(guān)
食物類模塊需求
- 在游戲開始時候食物生成在隨機(jī)位置
- 當(dāng)蛇吃掉食物后食物會再次隨機(jī)出現(xiàn)(出現(xiàn)的位置不能與蛇身重合)
記分牌模塊需求
- 設(shè)置限制等級
- 可以提升等級
- 可以增加獲取的分?jǐn)?shù)
蛇類模塊需求
- 游戲開始的時候只有一個方塊(蛇頭),隨后每吃掉一個食物則增加一節(jié)身體
- 當(dāng)游戲進(jìn)行過程中蛇頭碰到身體則結(jié)束游戲
- 蛇的前進(jìn)是持續(xù)的,不能停頓下來,只能去改變方向
控制模塊需求
- 按下方向鍵開始游戲
- 只能通過四個方向鍵改變蛇前進(jìn)的方向
- 判斷游戲是否結(jié)束,蛇是否吃到食物
- 控制分?jǐn)?shù)和等級是否相應(yīng)增長,食物是否刷新
項(xiàng)目搭建
ts轉(zhuǎn)譯為js代碼
我們需要創(chuàng)建tsconfig.json文件,文件代碼如下:
{
"compilerOptions": {
"module": "es6",
"target": "es6",
"strict": true,
"noEmitOnError": true
}
}
package.json包配置文件
- 在這個小項(xiàng)目中我們需要webpack打包工具,所以我們要對
package.json文件進(jìn)行一些配置。 - 選擇該項(xiàng)目在集成終端中打開并輸入代碼
npm init -y進(jìn)行項(xiàng)目初始化,這個時候會在你的項(xiàng)目中生成一個初步的package.json文件,然后我們進(jìn)一步完善 - 在集成終端中輸入指令
npm i -D webpack webpack-cli typescript ts-loader用來下載相關(guān)依賴(如果可以看見package.json的depDependencies中更新了你下載的依賴表示下載成功)。i表示install下載的意思,-D意思是下載的作為依賴使用 - 繼續(xù)輸入指令npm i -D css-loader 等依賴,這些后面都有用
- 請注意上述代碼中scripts中的"build": "webpack"鍵值對,這個設(shè)置說明我們可以用npm run build的代碼來啟用webpack打包工具
{
"name": "part2",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"start": "webpack serve --open"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.18.9",
"@babel/preset-env": "^7.18.9",
"babel-loader": "^8.2.5",
"clean-webpack-plugin": "^4.0.0",
"core-js": "^3.24.0",
"css-loader": "^6.7.1",
"html-webpack-plugin": "^5.5.0",
"less": "^4.1.3",
"less-loader": "^11.0.0",
"postcss": "^8.4.14",
"postcss-loader": "^7.0.1",
"postcss-preset-env": "^7.7.2",
"style-loader": "^3.3.1",
"ts-loader": "^9.3.1",
"typescript": "^4.7.4",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3"
}
}webpack.config.js打包工具配置
webpack打包文件配置中,代碼注釋非常清楚,代碼如下:
//引入一個包
const path = require('path');
//引入html插件
const HTMLWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
// webpack 中所有的配置信息都寫道嗎module.exports中
module.exports = {
//指定入口文件
entry: './src/index.ts',
//指定打包文件所在的目錄
output: {
//指定打包文件的目錄
path: path.resolve(__dirname,'dist'),
//打包后文件的名字
filename: "bundle.js",
//告訴webpack不使用箭頭函數(shù)
environment: {
arrowFunction: false
}
},
mode: 'development',
//指定webpack打包時要使用的模塊
module: {
//指定要加載的規(guī)則
rules: [
{
//test指定規(guī)則生成的文件
test: /\.ts$/,
//要使用的loader
use : [
//配置babel
{
// 指定加載器
loader: "babel-loader",
//設(shè)置babel
options: {
//設(shè)置預(yù)定義的環(huán)境
presets:[
[
//指定環(huán)境的插件
"@babel/preset-env",
//配置信息
{
//要兼容的目標(biāo)瀏覽器
targets : {
"chrome" : "101"
},
//指定core.js的版本
"corejs":"3",
//使用core.js的方式 usage 按需加載
"useBuiltIns": "usage"
}
]
]
}
}
,
'ts-loader'
],
//要排除的文件
exclude: /node-modules/
},
//設(shè)置less文件的處理
{
test : /\.less$/,
use : [
"style-loader",
"css-loader",
//引入postcss
{
loader: "postcss-loader",
options: {
postcssOptions : {
plugins: [
[
"postcss-preset-env",
{
browsers:'last 2 versions'
}
]
]
}
}
},
"less-loader"
]
}
]
},
//配置webpack插件
plugins: [
new CleanWebpackPlugin(),
new HTMLWebpackPlugin({
// title: "自定義的title"
template: "./src/index.html"
}),
],
//用來設(shè)置引用模塊
resolve: {
extensions: ['.ts', '.js']
}
}
到這里,我們要配置的文件都已經(jīng)配置結(jié)束,接下來正式進(jìn)入項(xiàng)目的開發(fā)
項(xiàng)目結(jié)構(gòu)搭建
首先要進(jìn)行我們的html和css樣式搭建,搭建出來項(xiàng)目的頁面!
html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>貪吃蛇</title>
</head>
<body>
<!--創(chuàng)建游戲的主容器-->
<div id="main">
<!--設(shè)置游戲的舞臺-->
<div id="stage">
<!--設(shè)置蛇-->
<div id="snake">
<!--snake內(nèi)部的div 表示蛇的各部分-->
<div></div>
</div>
<!--設(shè)置食物-->
<div id="food">
<!--添加四個小div 來設(shè)置食物的樣式-->
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
<!--設(shè)置游戲的積分牌-->
<div id="score-panel">
<div>
SCORE:<span id="score">0</span>
</div>
<div>
level:<span id="level">1</span>
</div>
</div>
</div>
</body>
</html>
css文件(這里使用的是less)
// 設(shè)置變量
@bg-color: #b7d4a8;
//清除默認(rèn)樣式
* {
margin: 0;
padding: 0;
//改變盒子模型的計(jì)算方式
box-sizing: border-box;
}
body{
font: bold 20px "Courier";
}
//設(shè)置主窗口的樣式
#main{
width: 360px;
height: 420px;
// 設(shè)置背景顏色
background-color: @bg-color;
// 設(shè)置居中
margin: 100px auto;
border: 10px solid black;
// 設(shè)置圓角
border-radius: 40px;
// 開啟彈性盒模型
display: flex;
// 設(shè)置主軸的方向
flex-flow: column;
// 設(shè)置側(cè)軸的對齊方式
align-items: center;
// 設(shè)置主軸的對齊方式
justify-content: space-around;
// 游戲舞臺
#stage{
width: 304px;
height: 304px;
border: 2px solid black;
// 開啟相對定位
position: relative;
// 設(shè)置蛇的樣式
#snake{
&>div{
width: 10px;
height: 10px;
background-color: #000;
border: 1px solid @bg-color;
// 開啟絕對定位
position: absolute;
}
}
// 設(shè)置食物
#food{
width: 10px;
height: 10px;
position: absolute;
left: 40px;
top: 100px;
// 開啟彈性盒
display: flex;
// 設(shè)置橫軸為主軸,wrap表示會自動換行
flex-flow: row wrap;
// 設(shè)置主軸和側(cè)軸的空白空間分配到元素之間
justify-content: space-between;
align-content: space-between;
&>div{
width: 4px;
height: 4px;
background-color: black;
// 使四個div旋轉(zhuǎn)45度
transform: rotate(45deg);
}
}
}
// 記分牌
#score-panel{
width: 300px;
display: flex;
// 設(shè)置主軸的對齊方式
justify-content: space-between;
}
}項(xiàng)目頁面

多模塊搭建
在項(xiàng)目開發(fā)中我們不可能把所有的代碼寫到一個文件中,所以項(xiàng)目開發(fā)必須會靈活運(yùn)用模塊化開發(fā)思想,把實(shí)現(xiàn)的功能細(xì)化成一個個模塊。
完成Food(食物)類
//定義食物類
class Food {
element : HTMLElement;
constructor() {
//獲取頁面中的food元素并賦給element
this.element = document.getElementById('food')!;
}
//獲取食物x軸坐標(biāo)的方法
get X() {
return this.element.offsetLeft;
}
//獲取食物y軸坐標(biāo)的方法
get Y() {
return this.element.offsetTop;
}
//修改食物位置的方法
change() {
//生成隨機(jī)位置
//食物的最小位置是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時我們設(shè)置了strict(嚴(yán)格)模式,因此
this.element = document.getElementById('food')!中如果我們不加!會讓編譯器不確定我們是否會獲取到food的dom元素而發(fā)生報(bào)錯- 準(zhǔn)備了
get()方法可以在控制模塊中隨時獲取food的具體定位 change()方法為隨機(jī)刷新一次food的位置export default Food代碼加在最后。為的是把food成為全局模塊暴露出去,這樣的話其他的模塊可以調(diào)用這個food模塊
完成ScorePanel(記分牌)類
//定義表示記分牌的類
class ScorePanel {
score : number = 0;
level : number = 1;
scoreSpan :HTMLElement;
levelEle : HTMLElement;
//設(shè)置變量限制等級
maxLevel : number;
//設(shè)置一個變量多少分升級
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
}
//設(shè)置加分的方法
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(),分別用來控制分?jǐn)?shù)增加和等級提升,重點(diǎn)也有設(shè)置一個變量來限制等級和設(shè)置變量來判斷多少分上升一個等級
完成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')
}
//獲取蛇的坐標(biāo)
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時,是在修改水平坐標(biāo),蛇在左右移動,蛇在向左移動時,不能向右掉頭
if(this.bodies[1] && (this.bodies[1] as HTMLElement).offsetLeft === value) {
//如果發(fā)生的掉頭,讓蛇向反方向繼續(xù)移動
if(value > this.X) {
//如果value大于舊值X,則說明蛇在向右走,此時應(yīng)該發(fā)生掉頭,應(yīng)該使蛇繼續(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時,是在修改水平坐標(biāo),蛇在上下移動,蛇在向上移動時,不能向下掉頭
if(this.bodies[1] && (this.bodies[1] as HTMLElement).offsetTop === value) {
//如果發(fā)生的掉頭,讓蛇向反方向繼續(xù)移動
if(value > this.Y) {
//如果value大于舊值X,則說明蛇在向右走,此時應(yīng)該發(fā)生掉頭,應(yīng)該使蛇繼續(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() {
/*
*將后邊的身體設(shè)置為前邊身體的位置
* 舉例子:
* 第四節(jié) == 第三節(jié)的位置
* 第三節(jié) == 第二節(jié)的位置
* 第二節(jié) == 第一節(jié)的位置
* */
//遍歷
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;
//將值設(shè)置到當(dāng)前身體上
(this.bodies[i] as HTMLElement).style.left = x +'px';
(this.bodies[i] as HTMLElement).style.top = y +'px';
}
}
//檢查蛇頭是否撞到身體
checkHeadBody() {
//獲取所有的身體,檢查其是否和蛇頭的坐標(biāo)發(fā)生重疊
for(let i =1;i<this.bodies.length;i++) {
if(this.X === this.bodies[i].offsetLeft && this.Y === this.bodies[i].offsetTop) {
//進(jìn)入判斷說明蛇頭撞到了身體,游戲結(jié)束
throw new Error('撞到自己了')
}
}
}
}
export default Snake代碼分析:
首先它自身只添加了三個功能函數(shù)addbody,movebody和checkHeadBody
movebody的實(shí)現(xiàn)邏輯非常的巧妙,根據(jù)從后往前的順序來確定位置,根據(jù)前一節(jié)的位置,從而讓后邊的位置替換到前一節(jié)的位置上,從而實(shí)現(xiàn)蛇可以移動的邏輯。
為什么get,set,判斷蛇是否死亡機(jī)制以及之后的蛇移動的代碼一定要寫在constructor()函數(shù)中而不是寫在外面?
在后面還有一個控制模塊中
首先利用get()方法獲得蛇頭坐標(biāo),當(dāng)蛇頭移動一次以后,立刻刷新后的蛇頭坐標(biāo)反饋給蛇對象
蛇這個對象更新以后constructor代碼就會執(zhí)行一遍,執(zhí)行過程中首先蛇頭的坐標(biāo)用set()函數(shù)重新設(shè)置,然后蛇的movebody函數(shù)就會執(zhí)行一次。最后對蛇進(jìn)行判斷死沒死。
這樣一次代碼就執(zhí)行完成啦。此時整條蛇都前進(jìn)了一次。然后我們通過定時器定個時間不斷讓蛇移動就可以了。
完成GameControl(控制)類
import Food from "./food";
import Snake from "./Snake";
import ScorePanel from "./ScorePanel";
//游戲控制器,控制其他所有類
class GameControl {
snake : Snake;
food : Food;
scorePanel : ScorePanel
direction : string = '';
//創(chuàng)建一個變量來判斷游戲是否結(jié)束
isLive : boolean = true;
constructor() {
this.snake = new Snake()
this.food = new Food()
this.scorePanel = new ScorePanel()
this.init()
}
//游戲的初始化,調(diào)用后游戲?qū)㈤_始
init() {
document.addEventListener('keydown',this.keydownHandler.bind(this))
//調(diào)用run
this.run()
}
/*ArrowUp
ArrowDown
ArrowLeft
ArrowRight
*/
//創(chuàng)建一個鍵盤按下的響應(yīng)函數(shù)
keydownHandler(event: KeyboardEvent) {
console.log(event.key)
this.direction = event.key
}
//創(chuàng)建一個控制蛇移動的方法
/*
* 根據(jù)方向(this.direction)來使蛇位置發(fā)生改變
*
* */
run() {
let X = this.snake.X;
let Y = this.snake.Y;
//根據(jù)方向修改值
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) {
//進(jìn)入到catch出現(xiàn)異常
alert((e as any).message + '游戲結(jié)束了,老表!');
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) {
//食物的位置要進(jìn)行重置
this.food.change()
//分?jǐn)?shù)增加
this.scorePanel.AddScore()
//蛇要增加一節(jié)
this.snake.addBody()
}
}
}
export default GameControl代碼分析:
我們設(shè)置控制類主要目的在于整合之前的三個類,從而在這個類中調(diào)用之前聲明的類,在該類中重點(diǎn)在于初始化游戲、控制蛇的移動、檢查蛇是否吃到食物,這個類相當(dāng)于一個總開關(guān)。
這里還有一個重點(diǎn)在于this指向問題,這里使用了bind()函數(shù),bind最直接的定義就是將this指向到當(dāng)前的對象。
完成index類(啟動項(xiàng)目)
import './style/index.less' import GameControl from './modules/GameControl' new GameControl()
代碼分析:
大家都非常清楚,想要讓對象執(zhí)行,我們必須要進(jìn)行實(shí)例化,這里只用new一下進(jìn)行調(diào)用即可,項(xiàng)目就可以執(zhí)行了
項(xiàng)目啟動
最后我們打開終端輸入npm start或者npm run build,項(xiàng)目就跑起來了,它可以自動打開瀏覽器進(jìn)行執(zhí)行


總結(jié)
學(xué)習(xí)完了typescript,其實(shí)最主要的在于運(yùn)用它實(shí)現(xiàn)面向?qū)ο蟮拈_發(fā),我們在日常開發(fā)中基本不會用到面向?qū)ο?,就算es6中涉及到類、接口等等,但是在實(shí)際中很少人去使用,面向?qū)ο蟮拈_發(fā)中其實(shí)使得項(xiàng)目變得更加的嚴(yán)謹(jǐn)和合理化,我們在書寫代碼的時候會更加的規(guī)范,ts的類型嚴(yán)格更加的使它方便大型項(xiàng)目開發(fā)!
到此這篇關(guān)于JavaScript TypeScript實(shí)現(xiàn)貪吃蛇游戲完整詳細(xì)流程的文章就介紹到這了,更多相關(guān)JS TypeScript貪吃蛇內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript中的for循環(huán)與雙重for循環(huán)詳解
這篇文章主要給大家介紹了關(guān)于JavaScript中for循環(huán)與雙重for循環(huán)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03
Js中的Object.entries()基本知識詳細(xì)分析(附Demo)
Object.entries方法能將對象的可枚舉屬性轉(zhuǎn)為數(shù)組,每個元素是鍵值對數(shù)組,可用于for...of迭代,下面這篇文章主要介紹了Js中的Object.entries()基本知識的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-09-09
Javascript 繼承機(jī)制的實(shí)現(xiàn)
要用ECMAScript實(shí)現(xiàn)繼承機(jī)制,首先從基類入手。所有開發(fā)者定義的類都可作為基類。出于安全原因,本地類和宿主類不能作為基類,這樣可以防止公用訪問編譯過的瀏覽器級的代碼,因?yàn)檫@些代碼可以被用于惡意攻擊。2009-08-08
用JS動態(tài)改變表單form里的action值屬性的兩種方法
下面小編就為大家?guī)硪黄肑S動態(tài)改變表單form里的action值屬性的兩種方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-05-05
javascript獲取不重復(fù)的隨機(jī)數(shù)的方法比較
js永不重復(fù)隨機(jī)數(shù)實(shí)現(xiàn)代碼比較2008-09-09
JavaScript實(shí)現(xiàn)判斷時間間隔是否連續(xù)為一天
這篇文章主要為大家詳細(xì)介紹了如何使用JavaScript實(shí)現(xiàn)判斷時間間隔是否連續(xù)為一天,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2024-01-01

