創建玩家
首先加載貼圖
src/config.ts
......
// 玩家坦克
import imgUrlPlayerTop from './static/images/player/top.gif'
import imgUrlPlayerRight from './static/images/player/right.gif'
import imgUrlPlayerBottom from './static/images/player/bottom.gif'
import imgUrlPlayerLeft from './static/images/player/left.gif'
export default {
......
// 圖片
images: {
......
//玩家坦克
playerTop: imgUrlPlayerTop,
playerRight: imgUrlPlayerRight,
playerBottom: imgUrlPlayerBottom,
playerLeft: imgUrlPlayerLeft
}
}
從Tank.ts複製Player.ts
src/canvas/Player.ts
/**
* 模型
* 敵方坦克
*/
import AbstractModel from "./abstract/AbstractModel";
import {image} from "../service/image";
import {EnumDirection} from "../enum/enumPosition";
import {upperFirst} from 'lodash'
import config from "../config";
import water from "../canvas/Water";
import wallBrick from "../canvas/WallBrick";
import wallSteel from "../canvas/WallSteel";
import player from "../canvas/Player";
import utils from "../utils";
export default class ModelTank extends AbstractModel implements IModel {
name: string = 'player';
// 畫布實例
canvas: ICanvas = player;
// 繼承父類抽象方法:渲染貼圖
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
// 讓坦克動
this.move()
}
// 隨機取用其中一個圖片
getImage(): HTMLImageElement {
return image.get(`${this.name}${upperFirst(this.direction)}` as keyof typeof config.images)!
}
// 坦克行動
protected move(): void {
while (true) {
// 畫布清空
// this.canvas.clearRect(this.x, this.y, config.model.common.width, config.model.common.height);
// ********************* 座標更新 *********************
let x = this.x;
let y = this.y;
switch (this.direction) {
case EnumDirection.top:
y -= 2
break;
case EnumDirection.right:
x += 2
break;
case EnumDirection.bottom:
y += 2
break;
case EnumDirection.left:
x -= 2
break;
}
if (utils.modelTouch(x, y, [
...water.models,// 水域
...wallBrick.models,// 磚牆
...wallSteel.models,// 鋼牆
]) || utils.isCanvasTouch(x, y)) {
// 隨機獲取方向
this.randomDirection()
} else {
this.x = x;
this.y = y;
// 跳出while死循環
break;
}
}
// ********************* 座標更新 *********************
// 畫布重繪, 渲染坦克模型,在這裏調用減少重繪次數
super.draw()
}
}
src/model/Player.ts
/**
* 模型
* 敵方坦克
*/
import AbstractModel from "./abstract/AbstractModel";
import {image} from "../service/image";
import {EnumDirection} from "../enum/enumPosition";
import {random, upperFirst} from 'lodash'
import config from "../config";
import water from "../canvas/Water";
import wallBrick from "../canvas/WallBrick";
import wallSteel from "../canvas/WallSteel";
import player from "../canvas/Player";
import utils from "../utils";
export default class ModelTank extends AbstractModel implements IModel {
name: string = 'player';
// 畫布實例
canvas: ICanvas = player;
// 繼承父類抽象方法:渲染貼圖
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
// 讓坦克動起來:循環定時器
// setInterval(() => {
// this.move()
// }, 50)
// 隨機轉向
if (random(79) == 1) {
this.randomDirection()
}
// 讓坦克動
this.move()
// 增加敵方坦克向下移動的概率
// if (Math.floor(Math.random() * 5) == 1) {
if (random(79) == 1) {
this.direction = EnumDirection.bottom
}
}
// 隨機取用其中一個圖片
getImage(): HTMLImageElement {
return image.get(`${this.name}${upperFirst(this.direction)}` as keyof typeof config.images)!
}
// 坦克行動
protected move(): void {
while (true) {
// 畫布清空
// this.canvas.clearRect(this.x, this.y, config.model.common.width, config.model.common.height);
// ********************* 座標更新 *********************
let x = this.x;
let y = this.y;
switch (this.direction) {
case EnumDirection.top:
y -= 2
break;
case EnumDirection.right:
x += 2
break;
case EnumDirection.bottom:
y += 2
break;
case EnumDirection.left:
x -= 2
break;
}
if (utils.modelTouch(x, y, [
...water.models,// 水域
...wallBrick.models,// 磚牆
...wallSteel.models,// 鋼牆
]) || utils.isCanvasTouch(x, y)) {
// 隨機獲取方向
this.randomDirection()
} else {
this.x = x;
this.y = y;
// 跳出while死循環
break;
}
}
// ********************* 座標更新 *********************
// 畫布重繪, 渲染坦克模型,在這裏調用減少重繪次數
super.draw()
}
}
src/main.ts
import player from "./canvas/Player";
......
const bootstrap = async () => {
......
player.render() // 畫布渲染:玩家坦克
}
......
玩家坦克控制
src/model/Player.ts
/**
* 模型
* 敵方坦克
*/
import AbstractModel from "./abstract/AbstractModel";
import {image} from "../service/image";
import {upperFirst} from 'lodash'
import config from "../config";
import player from "../canvas/Player";
export default class ModelTank extends AbstractModel implements IModel {
name: string = 'player';
// 畫布實例
canvas: ICanvas = player;
// 繼承父類抽象方法:渲染貼圖
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
// 初始化模型
super.draw()
// 控制坦克移動,添加鍵盤監聽事件
document.addEventListener('keydown', this.changeDirection.bind(this))
}
/**
* 方向改變
* @param event 鍵盤對象
*/
changeDirection(event: KeyboardEvent) {
console.log(event)
}
// 隨機取用其中一個圖片
getImage(): HTMLImageElement {
return image.get(`${this.name}${upperFirst(this.direction)}` as keyof typeof config.images)!
}
}
這裏可以看到,鍵盤按鍵區別類別有code和key。這裏我們通過code處理邏輯。
src/model/Player.ts
/**
* 模型
* 敵方坦克
*/
import AbstractModel from "./abstract/AbstractModel";
import {image} from "../service/image";
import {upperFirst} from 'lodash'
import config from "../config";
import player from "../canvas/Player";
import {EnumDirection} from "../enum/enumPosition";
export default class ModelTank extends AbstractModel implements IModel {
name: string = 'player';
// 畫布實例
canvas: ICanvas = player;
// 繼承父類抽象方法:渲染貼圖
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
//初始化時永遠朝上
// this.direction = EnumDirection.top
// 初始化模型
super.draw()
// 控制坦克移動,添加鍵盤監聽事件
document.addEventListener('keydown', this.changeDirection.bind(this))
}
/**
* 方向改變
* @param event 鍵盤對象
*/
changeDirection(event: KeyboardEvent) {
// console.log(event)
switch (event.code) {
case 'KeyW':
case 'ArrowUp':
this.direction = EnumDirection.top
break;
case 'KeyD':
case 'ArrowRight':
this.direction = EnumDirection.right
break;
case 'KeyS':
case 'ArrowDown':
this.direction = EnumDirection.bottom
break;
case 'KeyA':
case 'ArrowLeft':
this.direction = EnumDirection.left
break;
}
// 繪製模型
this.canvas.renderModels()
}
// 隨機取用其中一個圖片
getImage(): HTMLImageElement {
return image.get(`${this.name}${upperFirst(this.direction)}` as keyof typeof config.images)!
}
}
這裏切換方向,會出現卡死。
修復時間無限綁定
在我們的模型代碼中,調用this.canvas.renderModels()
,會執行模型的render()
方法,隨着越按越多,模型會無限增加事件的綁定,最終導致卡死。
解決方法:增加一個事件是否綁定的標記位。
src/model/Player.ts
export default class ModelTank extends AbstractModel implements IModel {
name: string = 'player';
// 畫布實例
canvas: ICanvas = player;
// 事件是否綁定
isBindEvent: boolean = false
// 繼承父類抽象方法:渲染貼圖
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
//初始化時永遠朝上
// this.direction = EnumDirection.top
// 初始化模型
super.draw()
if (!this.isBindEvent) {
// 控制坦克移動,添加鍵盤監聽事件
document.addEventListener('keydown', this.changeDirection.bind(this))
this.isBindEvent = true
}
}
......
}
玩家坦克碰撞檢測
src/model/Player.ts
import utils from "../utils";
......
// 繼承父類抽象方法:渲染貼圖
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
//初始化時永遠朝上
// this.direction = EnumDirection.top
// 初始化模型
super.draw()
if (!this.isBindEvent) {
// 控制坦克移動,添加鍵盤監聽事件
document.addEventListener('keydown', this.changeDirection.bind(this))
document.addEventListener('keydown', this.move.bind(this))
this.isBindEvent = true
}
}
......
//移動
move(event: KeyboardEvent) {
// ********************* 座標更新 *********************
let x = this.x;
let y = this.y;
switch (event.code) {
case 'KeyW':
case 'ArrowUp':
y -= 15
break;
case 'KeyD':
case 'ArrowRight':
x += 15
break;
case 'KeyS':
case 'ArrowDown':
y += 15
break;
case 'KeyA':
case 'ArrowLeft':
x -= 15
break;
}
if (utils.modelTouch(x, y, [
...water.models,// 水域
...wallBrick.models,// 磚牆
...wallSteel.models,// 鋼牆
...boss.models, //boss
...tank.models //敵方坦克
]) || utils.isCanvasTouch(x, y)) {
// 隨機獲取方向
} else {
this.x = x;
this.y = y;
// 繪製模型
this.canvas.renderModels()
}
}
......
src/model/Tank.ts
......
// 坦克行動
protected move(): void {
while (true) {
......
if (utils.modelTouch(x, y, [
...water.models,// 水域
...wallBrick.models,// 磚牆
...wallSteel.models,// 鋼牆
...player.models // 我方坦克
]) || utils.isCanvasTouch(x, y)) {
// 隨機獲取方向
this.randomDirection()
} else {
this.x = x;
this.y = y;
// 跳出while死循環
break;
}
}
......
玩家坦克子彈發射
/**
* 模型
* 敵方坦克
*/
......
import bullet from "../canvas/Bullet";
export default class ModelTank extends AbstractModel implements IModel {
name: string = 'player';
// 畫布實例
canvas: ICanvas = player;
// 事件是否綁定
isBindEvent: boolean = false
// 繼承父類抽象方法:渲染貼圖
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
//初始化時永遠朝上
// this.direction = EnumDirection.top
// 初始化模型
super.draw()
if (!this.isBindEvent) {
this.isBindEvent = true
// 添加鍵盤監聽事件,坦克方向改變
document.addEventListener('keydown', this.changeDirection.bind(this))
// 添加鍵盤監聽事件,坦克移動
document.addEventListener('keydown', this.move.bind(this))
// 添加鍵盤監聽事件,坦克發射子彈
document.addEventListener('keydown', (event: KeyboardEvent) => {
console.log(event);
//發射子彈,空格,回車,小鍵盤迴車
(event.code === 'Space'
|| event.code === 'Enter'
|| event.code === 'NumpadEnter') && bullet.addPlayerBullet();
}
)
}
}
......
}
src/canvas/Bullet.ts
......
// 我方坦克發射子彈
addPlayerBullet() {
this.models.push(new ModelBullet(player.models[0]))
}
......
坦克互戰
src/config.ts
// 草地
import imgUrlStraw from './static/images/straw/straw.png'
// 磚牆
import imgUrlWallBrick from './static/images/wall/wall.gif'
// 水域
import imgUrlWater from './static/images/water/water.gif'
// 鋼牆
import imgUrlWallSteel from './static/images/wall/steels.gif'
// 敵方坦克
import imgUrlTankTop from './static/images/tank/top.gif'
import imgUrlTankRight from './static/images/tank/right.gif'
import imgUrlTankLeft from './static/images/tank/left.gif'
import imgUrlTankBottom from './static/images/tank/bottom.gif'
// 子彈
import imgUrlBullet from './static/images/bullet/bullet.jpg'
// boss,己方老巢
import imgUrlBoss from './static/images/boss/boss.png'
// 玩家坦克
import imgUrlPlayerTop from './static/images/player/top.gif'
import imgUrlPlayerRight from './static/images/player/right.gif'
import imgUrlPlayerBottom from './static/images/player/bottom.gif'
import imgUrlPlayerLeft from './static/images/player/left.gif'
export default {
// 畫布
canvas: {
width: 900,
height: 600,
},
// 模型
model: {
common: {
width: 30,
height: 30,
},
// 子彈
bullet: {
width: 3,
height: 3,
},
// boss,己方老巢
// boss:{
// width: 60,
// height: 60,
// }
},
//草地
straw: {
num: 100,
},
// 磚牆
wallBrick: {
num: 100,
},
// 鋼牆
wallSteel: {
num: 30,
},
// 水域
water: {
num: 40
},
// 敵方坦克
tank: {
num: 40,// 數量
speed: 3// 速度,越大越快(1-100)
},
// 子彈的速度
bullet: {
num: 40,// 數量
speed: 8 // 速度,越大越快(1-100)
},
// 圖片
images: {
// 草地
straw: imgUrlStraw,
// 磚牆
wallBrick: imgUrlWallBrick,
// 鋼牆
wallSteel: imgUrlWallSteel,
// 水域
water: imgUrlWater,
// 敵方坦克
tankTop: imgUrlTankTop,
tankRight: imgUrlTankRight,
tankBottom: imgUrlTankBottom,
tankLeft: imgUrlTankLeft,
// 子彈
bullet: imgUrlBullet,
// boss,己方老巢
boss: imgUrlBoss,
//玩家坦克
playerTop: imgUrlPlayerTop,
playerRight: imgUrlPlayerRight,
playerBottom: imgUrlPlayerBottom,
playerLeft: imgUrlPlayerLeft
}
}
src/canvas/Tank.ts
......
/**
* 畫布是單例模式
* 在一個圖層,所以只需要new一個實例即可。
*/
export default new (class extends AbstractCanvas implements ICanvas {
render(): void {
// super:調用父類的方法
this.createModels()
// 調用渲染模型,防止每次重新渲染時,又生成新的模型實例
super.renderModels();
console.log("tank", Number((100 / config.tank.speed).toFixed(3)))
// 讓坦克畫布實時刷新,每config.tank.speed毫秒擦寫一次,等於速度。
setInterval(() => {
this.renderModels()
}, Number((100 / config.tank.speed).toFixed(3)))
}
......
}
src/canvas/Bullet.ts
......
/**
* 畫布是單例模式
* 在一個圖層,所以只需要new一個實例即可。
*/
export default new (class extends AbstractCanvas implements ICanvas {
render(): void {
// super:調用父類的方法
// super.createModels()
// 調用渲染模型,防止每次重新渲染時,又生成新的模型實例
// super.renderModels();
console.log("bullet", Number((100 / config.bullet.speed).toFixed(3)))
//bind:綁定this
setInterval(() => {
// this.createBullet.bind(this)
//自定義子彈的渲染函數:子彈創建
this.createModelsBullet()
// 調用渲染模型,防止每次重新渲染時,又生成新的模型實例
super.renderModels();
}, Number((100 / config.bullet.speed).toFixed(3)))
}
......
}
src/model/Bullet.ts
......
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
// super.draw()
// ********************* 座標更新 *********************
let x = this.x;
let y = this.y;
// 子彈速度
let speed = this.tank.name === 'player' ? 4 : 2
switch (this.direction) {
case EnumDirection.top:
y -= speed
break;
case EnumDirection.right:
x += speed
break;
case EnumDirection.bottom:
y += speed
break;
case EnumDirection.left:
x -= speed
break;
}
// // ********************* 座標更新 *********************
// 打不掉的障礙
let touchNotBlowModel = utils.modelTouch(x, y, [
...wallSteel.models,// 鋼牆
],
config.model.bullet.width,
config.model.bullet.height
)
// 打掉的障礙
let touchBlowModel = utils.modelTouch(x, y, [
...wallBrick.models,// 磚牆
...boss.models,// boss
],
config.model.bullet.width,
config.model.bullet.height
)
// 如果是我方坦克的子彈,需要判斷是否打中地方坦克
let touchTankModel = undefined
let touchPlayerModel = undefined
if (this.tank.name === 'player') {
touchTankModel = utils.modelTouch(x, y, [
...tank.models,// 敵方坦克
],
config.model.bullet.width,
config.model.bullet.height
)
} else {
touchPlayerModel = utils.modelTouch(x, y, [
...player.models,// 我方坦克
],
config.model.bullet.width,
config.model.bullet.height
)
}
//
if (touchNotBlowModel || utils.isCanvasTouch(x, y, config.model.bullet.width, config.model.bullet.height)) {
// 移除模型
this.destroy()
// 展示爆炸效果
touchNotBlowModel && super.blast(touchNotBlowModel)
} else if (touchPlayerModel || touchTankModel || touchBlowModel) {
// 當前子彈模型消失
this.destroy()
// **************** 可打爆的障礙 ****************
// 展示爆炸效果
touchBlowModel && super.blast(touchBlowModel)
// 碰撞的模型消失
touchBlowModel && touchBlowModel.destroy();
// **************** 可打爆的障礙 ****************
// **************** 玩家坦克打爆敵方坦克 ****************
// 展示爆炸效果
touchTankModel && super.blast(touchTankModel)
// 碰撞的模型消失
touchTankModel && touchTankModel.destroy();
// **************** 玩家坦克打爆敵方坦克 ****************
// **************** 敵方坦克打爆玩家坦克 ****************
// 展示爆炸效果
touchPlayerModel && super.blast(touchPlayerModel)
// 碰撞的模型消失
touchPlayerModel && touchPlayerModel.destroy();
// **************** 敵方坦克打爆玩家坦克 ****************
} else {
this.x = x;
this.y = y;
// 畫布重繪, 渲染坦克模型,在這裏調用減少重繪次數
this.draw()
}
}
......
玩家坦克被打掉了: