坦克動起來
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'
export default {
// 畫布
canvas: {
width: 900,
height: 600,
},
// 模型
model: {
common: {
width: 30,
height: 30,
},
// 草地
// straw: {
// width: 30,
// height: 30,
// }
},
//草地
straw: {
num: 100,
},
// 磚牆
wallBrick: {
num: 100,
},
// 鋼牆
wallSteel: {
num: 30,
},
// 水域
water: {
num: 40
},
// 地方坦克
tank: {
num: 40
},
// 圖片
images: {
// 草地
straw: imgUrlStraw,
wallBrick: imgUrlWallBrick,
wallSteel: imgUrlWallSteel,
water: imgUrlWater,
tankTop: imgUrlTankTop,
tankRight: imgUrlTankRight,
tankBottom: imgUrlTankBottom,
tankLeft: imgUrlTankLeft,
}
}
src/model/abstract/AbstractModel.ts
import config from "../../config";
/**
* 抽象類
*/
export default abstract class AbstractModel {
//構造函數渲染
constructor(
protected canvas: CanvasRenderingContext2D,
protected x: number,
protected y: number
) {
}
// 抽象屬性:模型名稱
abstract name: string
// 抽象方法:渲染貼圖
abstract render(): void
// 渲染函數
protected draw(img: HTMLImageElement) {
this.canvas.drawImage(
img,
this.x,
this.y,
config.model.common.width,
config.model.common.height
)
}
}
src/model/Tank.ts
/**
* 模型
* 草地
*/
import AbstractModel from "./abstract/AbstractModel";
import {image} from "../service/image";
import {EnumDirection} from "../enum/enumPosition";
import {upperFirst} from 'lodash'
import config from "../config";
export default class ModelTank extends AbstractModel implements IModel {
name: string = 'tank';
// 方向
protected direction: EnumDirection = EnumDirection.top
// 繼承父類抽象方法:渲染貼圖
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
this.randomDirection();
super.draw(this.randomImage())
// 讓坦克動起來:循環定時器
setInterval(() => {
this.move()
}, 50)
}
// 坦克行動
protected move(): void {
// 畫布清空
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;
}
//x最大的座標邊界
let maxX = config.canvas.width - config.model.common.width;
//x最大的座標邊界
let maxY = config.canvas.height - config.model.common.height;
//座標邊界限制
(x < 0) ? this.x = 0 : (x > maxX) ? this.x = maxX : this.x = x;
(y < 0) ? this.y = 0 : (y > maxY) ? this.y = maxY : this.y = y;
// ********************* 座標更新 *********************
// 畫布重繪
super.draw(this.randomImage())
}
randomDirection() {
// 隨機取一個
const index = Math.floor((Math.random() * 4))
this.direction = Object.keys(EnumDirection)[index] as EnumDirection
}
// 隨機取用其中一個圖片
randomImage(): HTMLImageElement {
return image.get(`${this.name}${upperFirst(this.direction)}` as keyof typeof config.images)!
// let img: HTMLImageElement;
// switch (this.direction) {
// case EnumDirection.top:
// img = image.get('tankTop')!
// break;
// case EnumDirection.right:
// img = image.get('tankRight')!
// break;
// case EnumDirection.bottom:
// img = image.get('tankBottom')!
// break;
// case EnumDirection.left:
// img = image.get('tankLeft')!
// break;
// default:
// img = image.get('tankTop')!
// break;
// }
// return img
}
}
src/service/position.ts
/**
* 服務
* 位置生成
*/
import config from "../config";
type positionType = { x: number, y: number }
class Position {
// 集合包括磚牆、草地、磚塊,都在裏面
collection: positionType[] = []
public getPositionCollection(num: number) {
const collection = [] as { x: number, y: number }[]
for (let i = 0; i < num; i++) {
let count = 0
while (true) {
const position = this.position()
// 從整個集合中防止重疊座標
const exists = this.collection.some(item =>
item.x == position.x && item.y == position.y)
if (!exists || count > 4000) {
collection.push(position)
this.collection.push(position)
break;
}
// 防止死循環
count++;
}
}
return collection
}
// 返回隨機位置
public position() {
let x: number, y: number;
let gridNumX = config.canvas.width / config.model.common.width;
// 隨機格子數量
let leftNumX = Math.floor(Math.random() * gridNumX)
//轉換成px
x = leftNumX * config.model.common.width
let gridNumY = config.canvas.height / config.model.common.height;
// 隨機格子數量
let leftNumY = Math.floor(Math.random() * (gridNumY - 3))
//轉換成px,且頂部空出一格
y = (leftNumY + 1) * config.model.common.height
return {x, y}
}
}
export default new Position()
坦克最終會移動到邊界停下。
重構模型渲染機制
這裏如果有40輛坦克,那就需要插銷40次模型。所以,我們只需要在畫布層面插銷一次,即可將40次插銷變爲一次。
在src/model/Tank.ts中。
......
// 繼承父類抽象方法:渲染貼圖
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
// 坦克方向隨機生成
this.randomDirection();
super.draw(this.randomImage())
// 讓坦克動起來:循環定時器
setInterval(() => {
this.move()
}, 50)
}
......
` this.randomDirection();方法作用於隨機生成坦克方向,但模型不應該重新變換坦克的方向。所以需要將方向的重新生成放到父類中去。
src/model/abstract/AbstractModel.ts
import config from "../../config";
import {EnumDirection} from "../../enum/enumPosition";
/**
* 抽象類
*/
export default abstract class AbstractModel {
// 方向
protected direction: EnumDirection = EnumDirection.top
//構造函數渲染
constructor(
protected canvas: CanvasRenderingContext2D,
protected x: number,
protected y: number
) {
// 方向隨機生成
this.randomDirection();
}
// 抽象屬性:模型名稱
abstract name: string
// 抽象方法:渲染貼圖
abstract render(): void
// 抽象方法:獲取貼圖
abstract getImage(): HTMLImageElement
// 方向隨機生成
randomDirection() {
// 隨機取一個
const index = Math.floor((Math.random() * 4))
this.direction = Object.keys(EnumDirection)[index] as EnumDirection
}
// 渲染函數
protected draw() {
this.canvas.drawImage(
this.getImage(),
this.x,
this.y,
config.model.common.width,
config.model.common.height
)
}
}
src/model/Tank.ts
/**
* 模型
* 草地
*/
import AbstractModel from "./abstract/AbstractModel";
import {image} from "../service/image";
import {EnumDirection} from "../enum/enumPosition";
import {upperFirst} from 'lodash'
import config from "../config";
export default class ModelTank extends AbstractModel implements IModel {
name: string = 'tank';
// 繼承父類抽象方法:渲染貼圖
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
super.draw()
// 讓坦克動起來:循環定時器
setInterval(() => {
this.move()
}, 50)
}
// 坦克行動
protected move(): void {
// 畫布清空
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;
}
//x最大的座標邊界
let maxX = config.canvas.width - config.model.common.width;
//x最大的座標邊界
let maxY = config.canvas.height - config.model.common.height;
//座標邊界限制
(x < 0) ? this.x = 0 : (x > maxX) ? this.x = maxX : this.x = x;
(y < 0) ? this.y = 0 : (y > maxY) ? this.y = maxY : this.y = y;
// ********************* 座標更新 *********************
// 畫布重繪
super.draw()
}
// 隨機取用其中一個圖片
getImage(): HTMLImageElement {
return image.get(`${this.name}${upperFirst(this.direction)}` as keyof typeof config.images)!
// let img: HTMLImageElement;
// switch (this.direction) {
// case EnumDirection.top:
// img = image.get('tankTop')!
// break;
// case EnumDirection.right:
// img = image.get('tankRight')!
// break;
// case EnumDirection.bottom:
// img = image.get('tankBottom')!
// break;
// case EnumDirection.left:
// img = image.get('tankLeft')!
// break;
// default:
// img = image.get('tankTop')!
// break;
// }
// return img
}
}
src/model/Straw.ts、src/model/WallBrick.ts、src/model/WallSteel.ts、src/model/Water.ts修改類似
/**
* 模型
* 草地
*/
import AbstractModel from "./abstract/AbstractModel";
import {image} from "../service/image";
import config from "../config";
export default class ModelStraw extends AbstractModel implements IModel {
name: string = 'straw';
// 繼承父類抽象方法:渲染貼圖
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
super.draw()
}
// 獲取貼圖
getImage(): HTMLImageElement {
return image.get(this.name as keyof typeof config.images)!;
}
}
修改後效果一致。
畫布渲染
渲染交給畫布來做。
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'
export default {
// 畫布
canvas: {
width: 900,
height: 600,
},
// 模型
model: {
common: {
width: 30,
height: 30,
},
// 草地
// straw: {
// width: 30,
// height: 30,
// }
},
//草地
straw: {
num: 100,
},
// 磚牆
wallBrick: {
num: 100,
},
// 鋼牆
wallSteel: {
num: 30,
},
// 水域
water: {
num: 40
},
// 敵方坦克
tank: {
num: 40,// 坦克數量
speed: 25 // 坦克速度,越小越快
},
// 圖片
images: {
// 草地
straw: imgUrlStraw,
wallBrick: imgUrlWallBrick,
wallSteel: imgUrlWallSteel,
water: imgUrlWater,
tankTop: imgUrlTankTop,
tankRight: imgUrlTankRight,
tankBottom: imgUrlTankBottom,
tankLeft: imgUrlTankLeft,
}
}
src/vite-env.d.ts
/// <reference types="vite/client" />
/**
* 全局聲明
*/
/**
* 模型對象
*/
interface ConstructorModel {
new(canvas: CanvasRenderingContext2D,
x: number,
y: number): any
}
/**
* 模型實現的函數、方法
*/
interface IModel {
// 抽象屬性:模型名稱
name: string
// 抽象方法:渲染貼圖
render(): void
// 抽象方法:獲取貼圖
getImage(): HTMLImageElement
}
/**
* 畫布實現的函數、方法
*/
interface ICanvas {
// 抽象方法:渲染貼圖
render(): void
// 抽象方法,返回模型
model(): ConstructorModel
// 抽象方法:返回模型數量
num(): number
}
src/model/abstract/AbstractModel.ts
import config from "../../config";
import {EnumDirection} from "../../enum/enumPosition";
/**
* 抽象類
*/
export default abstract class AbstractModel {
// 方向
protected direction: EnumDirection = EnumDirection.top
//構造函數渲染
constructor(
protected canvas: CanvasRenderingContext2D,
protected x: number,
protected y: number
) {
// 方向隨機生成
this.randomDirection();
}
// 抽象屬性:模型名稱
abstract name: string
// 抽象方法:渲染貼圖
abstract render(): void
// 抽象方法:獲取貼圖
abstract getImage(): HTMLImageElement
// 方向隨機生成
randomDirection() {
// 隨機取一個
const index = Math.floor((Math.random() * 4))
this.direction = Object.keys(EnumDirection)[index] as EnumDirection
}
// 函數:渲染模型
protected draw() {
this.canvas.drawImage(
this.getImage(),
this.x,
this.y,
config.model.common.width,
config.model.common.height
)
}
}
src/model/Straw.ts、src/model/WallBrick.ts、src/model/WallSteel.ts、src/model/Water.ts修改類似
/**
* 模型
* 草地
*/
import AbstractModel from "./abstract/AbstractModel";
import {image} from "../service/image";
import config from "../config";
export default class ModelStraw extends AbstractModel implements IModel {
name: string = 'straw';
// 繼承父類抽象方法:渲染貼圖
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
super.draw()
}
// 獲取貼圖
getImage(): HTMLImageElement {
return image.get(this.name as keyof typeof config.images)!;
}
}
src/model/Tank.ts
/**
* 模型
* 草地
*/
import AbstractModel from "./abstract/AbstractModel";
import {image} from "../service/image";
import {EnumDirection} from "../enum/enumPosition";
import {upperFirst} from 'lodash'
import config from "../config";
export default class ModelTank extends AbstractModel implements IModel {
name: string = 'tank';
// 繼承父類抽象方法:渲染貼圖
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
// 讓坦克動起來:循環定時器
// setInterval(() => {
// this.move()
// }, 50)
// 讓坦克動
this.move()
// 渲染坦克模型
super.draw()
}
// 坦克行動
protected move(): void {
// 畫布清空
// 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 --
break;
case EnumDirection.right:
x ++
break;
case EnumDirection.bottom:
y ++
break;
case EnumDirection.left:
x --
break;
}
//x最大的座標邊界
let maxX = config.canvas.width - config.model.common.width;
//x最大的座標邊界
let maxY = config.canvas.height - config.model.common.height;
//座標邊界限制
(x < 0) ? this.x = 0 : (x > maxX) ? this.x = maxX : this.x = x;
(y < 0) ? this.y = 0 : (y > maxY) ? this.y = maxY : this.y = y;
// ********************* 座標更新 *********************
// 畫布重繪
// super.draw()
}
// 隨機取用其中一個圖片
getImage(): HTMLImageElement {
return image.get(`${this.name}${upperFirst(this.direction)}` as keyof typeof config.images)!
// let img: HTMLImageElement;
// switch (this.direction) {
// case EnumDirection.top:
// img = image.get('tankTop')!
// break;
// case EnumDirection.right:
// img = image.get('tankRight')!
// break;
// case EnumDirection.bottom:
// img = image.get('tankBottom')!
// break;
// case EnumDirection.left:
// img = image.get('tankLeft')!
// break;
// default:
// img = image.get('tankTop')!
// break;
// }
// return img
}
}
調整畫布。
src/canvas/Tank.ts
/**
* 畫布
* 坦克
*/
import AbstractCanvas from "./abstract/AbstractCanvas";
import ModelTank from "../model/Tank";
import config from "../config";
import position from "../service/position";
class Tank extends AbstractCanvas implements ICanvas {
render(): void {
// super:調用父類的方法
this.createModels()
// 調用渲染模型,防止每次重新渲染時,又生成新的模型實例
super.renderModels();
// 讓坦克畫布實時刷新,每config.tank.speed毫秒擦寫一次,等於速度。
setInterval(() => {
this.renderModels()
}, config.tank.speed)
}
// 抽象方法,返回模型
model(): ConstructorModel {
return ModelTank;
}
// 抽象方法:返回模型數量
num(): number {
return config.tank.num
}
// 重寫父類方法
// 繪製模型,生成模型實例,只負責創建實例
createModels() {
for (let i = 0; i < this.num(); i++) {
const pos = position.position()
const model = this.model()
//Y軸永遠從0開始
const instance = new model(this.canvas, pos.x, 0)
this.models.push(instance)
}
}
// 畫布渲染模型(將模型渲染到畫布上)
protected renderModels() {
// 先擦除
this.canvas.clearRect(0, 0, config.canvas.width, config.canvas.height);
// 調用渲染模型,防止每次重新渲染時,又生成新的模型實例
super.renderModels();
}
}
// 坦克在一個圖層,所以只需要new一個實例即可。
export default new Tank()
效果一致:
坦克碰撞檢測
設定坦克遇到碰撞隨機轉向。
AbstractModel.ts中增加2個屬性值。
......
/**
* 抽象類
*/
export default abstract class AbstractModel {
......
// 寬度
protected width = config.model.common.width;
// 高度
protected height = config.model.common.height;
......
}
......
src/model/Tank.ts
/**
* 模型
* 草地
*/
import AbstractModel from "./abstract/AbstractModel";
import {image} from "../service/image";
import {EnumDirection} from "../enum/enumPosition";
import {upperFirst} from 'lodash'
import config from "../config";
export default class ModelTank extends AbstractModel implements IModel {
name: string = 'tank';
// 繼承父類抽象方法:渲染貼圖
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
// 讓坦克動起來:循環定時器
// setInterval(() => {
// this.move()
// }, 50)
// 讓坦克動
this.move()
// 渲染坦克模型
super.draw()
}
// 坦克行動
protected move(): void {
// 畫布清空
// 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--
break;
case EnumDirection.right:
x++
break;
case EnumDirection.bottom:
y++
break;
case EnumDirection.left:
x--
break;
}
if (this.isTouch(x, y)) {
// 隨機獲取方向
this.randomDirection()
}else{
this.x=x;
this.y=y
}
// ********************* 座標更新 *********************
// 畫布重繪
// super.draw()
}
// 判斷是否觸碰
protected isTouch(x: number, y: number) {
let result = false;
//x最大的座標邊界
let maxX = config.canvas.width - this.width;
//x最大的座標邊界
let maxY = config.canvas.height - this.height;
if (x < 0 || x > maxX || y < 0 || y > maxY) {
result = true
}
return result;
}
// 隨機取用其中一個圖片
getImage(): HTMLImageElement {
return image.get(`${this.name}${upperFirst(this.direction)}` as keyof typeof config.images)!
// let img: HTMLImageElement;
// switch (this.direction) {
// case EnumDirection.top:
// img = image.get('tankTop')!
// break;
// case EnumDirection.right:
// img = image.get('tankRight')!
// break;
// case EnumDirection.bottom:
// img = image.get('tankBottom')!
// break;
// case EnumDirection.left:
// img = image.get('tankLeft')!
// break;
// default:
// img = image.get('tankTop')!
// break;
// }
// return img
}
}
運行效果
運動優化
- 除了草地能穿越,別的不能穿越。
- 這個遊戲需要最後敵方坦克打掉我方底部的老巢,那麼就需要增加敵方坦克往下的運動概率。
其他物體碰撞
src/canvas/abstract/AbstractCanvas.ts
import config from "../../config";
import position from "../../service/position";
/**
* 抽象類
*/
export default abstract class AbstractCanvas {
// 元素實例:模型,元素碰撞需要取用,所以爲public
public models: IModel[] = []
//構造函數渲染
constructor(
protected app = document.querySelector('#app') as HTMLDivElement,
// @ts-ignore
protected el = document.createElement<HTMLCanvasElement>('canvas')!,
// @ts-ignore
protected canvas = el.getContext('2d')!
) {
this.createCanvas()
}
// 抽象方法:渲染貼圖
abstract render(): void
// 抽象方法,返回模型
abstract model(): ConstructorModel
// 抽象方法:返回模型數量
abstract num(): number
// 初始化canvas
protected createCanvas() {
// 元素的寬高就是全局canvas得到寬高
// @ts-ignore
this.el.width = config.canvas.width
// @ts-ignore
this.el.height = config.canvas.height
// 測試畫布
// 定義填充顏色
// this.canvas.fillStyle = '#16a085'
// 繪製矩形
// this.canvas.fillRect(0, 0, config.canvas.width, config.canvas.height)
// 最終元素要放到我們的app的div中
// @ts-ignore
this.app.insertAdjacentElement('afterbegin', this.el)
}
// 繪製模型,生成模型實例,只負責創建實例
// protected:子類可以調用,外部不能調用
//num: 渲染多少個數量
//model: 模型
protected createModels() {
position.getPositionCollection(this.num()).forEach((position) => {
const model = this.model()
const instance = new model(this.canvas, position.x, position.y)
this.models.push(instance)
// this.canvas.drawImage(
// image.get('straw')!,
// position.x,
// position.y,
// config.model.common.width,
// config.model.common.height
// );
})
// Array(num).fill('').forEach(() => {
// const position = this.position()
// this.canvas.drawImage(
// image.get('straw')!,
// position.x,
// position.y,
// config.model.common.width,
// config.model.common.height
// );
// })
// const img = document.createElement('img')
// img.src = imgUrl;
// //圖片是異步加載,所以需要將圖片加載完畢後,才進行渲染繪製
// img.onload = () => {
// const position = this.position()
// this.canvas.drawImage(img, position.x, position.y, config.model.common.width, config.model.common.height);
// }
}
// 畫布渲染模型(將模型渲染到畫布上)
protected renderModels() {
this.models.forEach(model => model.render())
}
}
src/model/abstract/AbstractModel.ts
import config from "../../config";
import {EnumDirection} from "../../enum/enumPosition";
/**
* 抽象類
*/
export default abstract class AbstractModel {
// 方向
protected direction: EnumDirection = EnumDirection.top
// 寬度,碰撞判斷需要跨模型調用,所以爲public
public width = config.model.common.width;
// 高度,碰撞判斷需要跨模型調用,所以爲public
public height = config.model.common.height;
//構造函數渲染
// 碰撞判斷需要跨模型調用模型的座標位置,所以爲public
constructor(
protected canvas: CanvasRenderingContext2D,
public x: number,
public y: number
) {
// 方向隨機生成
this.randomDirection();
}
// 抽象屬性:模型名稱
abstract name: string
// 抽象方法:渲染貼圖
abstract render(): void
// 抽象方法:獲取貼圖
abstract getImage(): HTMLImageElement
// 方向隨機生成
randomDirection() {
// 隨機取一個
const index = Math.floor((Math.random() * 4))
this.direction = Object.keys(EnumDirection)[index] as EnumDirection
}
// 函數:渲染模型
protected draw() {
this.canvas.drawImage(
this.getImage(),
this.x,
this.y,
config.model.common.width,
config.model.common.height
)
}
}
src/model/Tank.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";
export default class ModelTank extends AbstractModel implements IModel {
name: string = 'tank';
// 繼承父類抽象方法:渲染貼圖
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
// 讓坦克動起來:循環定時器
// setInterval(() => {
// this.move()
// }, 50)
// 讓坦克動
this.move()
// 渲染坦克模型
super.draw()
}
// 坦克行動
protected move(): void {
// 畫布清空
// 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--
break;
case EnumDirection.right:
x++
break;
case EnumDirection.bottom:
y++
break;
case EnumDirection.left:
x--
break;
}
if (this.isTouch(x, y)) {
// 隨機獲取方向
this.randomDirection()
} else {
this.x = x;
this.y = y
}
// ********************* 座標更新 *********************
// 畫布重繪
// super.draw()
}
// 判斷是否觸碰
protected isTouch(x: number, y: number): boolean {
// ********************* 座標邊界判斷 *********************
//x最大的座標邊界
let maxX = config.canvas.width - this.width;
//x最大的座標邊界
let maxY = config.canvas.height - this.height;
if (x < 0 || x > maxX || y < 0 || y > maxY) {
return true
}
// ********************* 座標邊界判斷 *********************
// ********************* 其他模型碰撞判斷 *********************
const models = [
...water.models,// 水域
...wallBrick.models,// 磚牆
...wallSteel.models,// 鋼牆
]
return models.some(model => {
let leftX = model.x - this.width // 物體模型 左側邊碰撞判斷
let rightX = model.x + model.width// 物體模型 右側邊碰撞判斷
let topY = model.y - this.height// 物體模型 上側邊碰撞判斷
let bottomY = model.y + model.height// 物體模型 下側邊碰撞判斷
const state = x <= leftX || x >= rightX || y <= topY || y >= bottomY
return !state
})
// ********************* 其他模型碰撞判斷 *********************
}
// 隨機取用其中一個圖片
getImage(): HTMLImageElement {
return image.get(`${this.name}${upperFirst(this.direction)}` as keyof typeof config.images)!
// let img: HTMLImageElement;
// switch (this.direction) {
// case EnumDirection.top:
// img = image.get('tankTop')!
// break;
// case EnumDirection.right:
// img = image.get('tankRight')!
// break;
// case EnumDirection.bottom:
// img = image.get('tankBottom')!
// break;
// case EnumDirection.left:
// img = image.get('tankLeft')!
// break;
// default:
// img = image.get('tankTop')!
// break;
// }
// return img
}
}
運行效果如下:
增加往下運動概率
很簡單,修改src/model/Tank.ts
......
// 繼承父類抽象方法:渲染貼圖
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
// 讓坦克動起來:循環定時器
// setInterval(() => {
// this.move()
// }, 50)
// 讓坦克動
this.move()
// 增加敵方坦克向下移動的概率
// if (Math.floor(Math.random() * 5) == 1) {
if (random(20) == 1) {
this.direction = EnumDirection.bottom
}
// 渲染坦克模型
super.draw()
}
......
代碼優化:優化模型代碼
主要優化方向爲將畫布抽象成屬性實例。
// 抽象屬性:畫布實例
abstract canvas: ICanvas
src/vite-env.d.ts
/// <reference types="vite/client" />
/**
* 全局聲明
*/
/**
* 模型對象
*/
interface ConstructorModel {
new(x: number,
y: number): any
}
/**
* 模型實現的函數、方法
*/
interface IModel {
// 抽象屬性:模型名稱
name: string
// 座標,x軸
x: number
// 座標,y軸
y: number
// 寬度,碰撞判斷需要跨模型調用,所以爲public
width: number;
// 高度,碰撞判斷需要跨模型調用,所以爲public
height: number;
// 抽象方法:渲染貼圖
render(): void
// 抽象方法:獲取貼圖
getImage(): HTMLImageElement
}
/**
* 畫布實現的函數、方法
*/
interface ICanvas {
// 抽象屬性:畫布實例
ctx: CanvasRenderingContext2D
// 抽象方法:渲染貼圖
render(): void
// 抽象方法,返回模型
model(): ConstructorModel
// 抽象方法:返回模型數量
num(): number
}
src/model/abstract/AbstractModel.ts
import config from "../../config";
import {EnumDirection} from "../../enum/enumPosition";
/**
* 抽象類
*/
export default abstract class AbstractModel {
// 寬度,碰撞判斷需要跨模型調用,所以爲public
public width = config.model.common.width;
// 高度,碰撞判斷需要跨模型調用,所以爲public
public height = config.model.common.height;
// 抽象屬性:模型名稱
abstract name: string
// 抽象屬性:畫布實例
abstract canvas: ICanvas
// 方向
protected direction: EnumDirection = EnumDirection.top
//構造函數渲染
// 碰撞判斷需要跨模型調用模型的座標位置,所以爲public
constructor(
public x: number,
public y: number
) {
// 方向隨機生成
this.randomDirection();
}
// 抽象方法:渲染貼圖
abstract render(): void
// 抽象方法:獲取貼圖
abstract getImage(): HTMLImageElement
// 方向隨機生成
randomDirection() {
// 隨機取一個
const index = Math.floor((Math.random() * 4))
// 存儲方向
this.direction = Object.keys(EnumDirection)[index] as EnumDirection
}
// 函數:渲染模型
protected draw() {
this.canvas.ctx.drawImage(
this.getImage(),
this.x,
this.y,
config.model.common.width,
config.model.common.height
)
}
}
src/model/Tank.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 tank from "../canvas/Tank";
export default class ModelTank extends AbstractModel implements IModel {
name: string = 'tank';
// 畫布實例
canvas: ICanvas = tank;
// 繼承父類抽象方法:渲染貼圖
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
// 讓坦克動起來:循環定時器
// setInterval(() => {
// this.move()
// }, 50)
// 讓坦克動
this.move()
// 增加敵方坦克向下移動的概率
// if (Math.floor(Math.random() * 5) == 1) {
if (random(20) == 1) {
this.direction = EnumDirection.bottom
}
}
// 隨機取用其中一個圖片
getImage(): HTMLImageElement {
return image.get(`${this.name}${upperFirst(this.direction)}` as keyof typeof config.images)!
// let img: HTMLImageElement;
// switch (this.direction) {
// case EnumDirection.top:
// img = image.get('tankTop')!
// break;
// case EnumDirection.right:
// img = image.get('tankRight')!
// break;
// case EnumDirection.bottom:
// img = image.get('tankBottom')!
// break;
// case EnumDirection.left:
// img = image.get('tankLeft')!
// break;
// default:
// img = image.get('tankTop')!
// break;
// }
// return img
}
// 坦克行動
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--
break;
case EnumDirection.right:
x++
break;
case EnumDirection.bottom:
y++
break;
case EnumDirection.left:
x--
break;
}
if (this.isTouch(x, y)) {
// 隨機獲取方向
this.randomDirection()
} else {
this.x = x;
this.y = y;
// 跳出while死循環
break;
}
}
// ********************* 座標更新 *********************
// 畫布重繪, 渲染坦克模型,在這裏調用減少重繪次數
super.draw()
}
// 判斷是否觸碰
protected isTouch(x: number, y: number): boolean {
// ********************* 座標邊界判斷 *********************
//x最大的座標邊界
let maxX = config.canvas.width - this.width;
//x最大的座標邊界
let maxY = config.canvas.height - this.height;
if (x < 0 || x > maxX || y < 0 || y > maxY) {
return true
}
// ********************* 座標邊界判斷 *********************
// ********************* 其他模型碰撞判斷 *********************
const models = [
...water.models,// 水域
...wallBrick.models,// 磚牆
...wallSteel.models,// 鋼牆
]
return models.some(model => {
let leftX = model.x - this.width // 物體模型 左側邊碰撞判斷
let rightX = model.x + model.width// 物體模型 右側邊碰撞判斷
let topY = model.y - this.height// 物體模型 上側邊碰撞判斷
let bottomY = model.y + model.height// 物體模型 下側邊碰撞判斷
const state = x <= leftX || x >= rightX || y <= topY || y >= bottomY
return !state
})
// ********************* 其他模型碰撞判斷 *********************
}
}
src/model/Straw.ts、src/model/WallBrick.ts、src/model/WallSteel.ts、src/model/Water.ts修改類似:
/**
* 模型
* 草地
*/
import AbstractModel from "./abstract/AbstractModel";
import {image} from "../service/image";
import config from "../config";
import straw from "../canvas/Straw";
export default class ModelStraw extends AbstractModel implements IModel {
name: string = 'straw';
// 畫布實例
canvas: ICanvas = straw;
// 繼承父類抽象方法:渲染貼圖
// 一些初始化自定義的動作、行爲,都在這裏進行
render(): void {
super.draw()
}
// 獲取貼圖
getImage(): HTMLImageElement {
return image.get(this.name as keyof typeof config.images)!;
}
}
src/canvas/Tank.ts
/**
* 畫布
* 坦克
*/
import AbstractCanvas from "./abstract/AbstractCanvas";
import ModelTank from "../model/Tank";
import config from "../config";
import position from "../service/position";
class Tank extends AbstractCanvas implements ICanvas {
render(): void {
// super:調用父類的方法
this.createModels()
// 調用渲染模型,防止每次重新渲染時,又生成新的模型實例
super.renderModels();
// 讓坦克畫布實時刷新,每config.tank.speed毫秒擦寫一次,等於速度。
setInterval(() => {
this.renderModels()
}, config.tank.speed)
}
// 抽象方法,返回模型
model(): ConstructorModel {
return ModelTank;
}
// 抽象方法:返回模型數量
num(): number {
return config.tank.num
}
// 重寫父類方法
// 繪製模型,生成模型實例,只負責創建實例
createModels() {
for (let i = 0; i < this.num(); i++) {
const pos = position.position()
const model = this.model()
//Y軸永遠從0開始
const instance = new model(pos.x, 0)
this.models.push(instance)
}
}
// 畫布渲染模型(將模型渲染到畫布上)
protected renderModels() {
// 先擦除
this.ctx.clearRect(0, 0, config.canvas.width, config.canvas.height);
// 調用渲染模型,防止每次重新渲染時,又生成新的模型實例
super.renderModels();
}
}
// 坦克在一個圖層,所以只需要new一個實例即可。
export default new Tank()
src/canvas/abstract/AbstractCanvas.ts
import config from "../../config";
import position from "../../service/position";
/**
* 抽象類
*/
export default abstract class AbstractCanvas {
// 元素實例:模型,元素碰撞需要取用,所以爲public
public models: IModel[] = []
//構造函數渲染
constructor(
protected app = document.querySelector('#app') as HTMLDivElement,
// @ts-ignore
protected el = document.createElement<HTMLCanvasElement>('canvas')!,
// @ts-ignore
public ctx = el.getContext('2d')!
) {
this.createCanvas()
}
// 抽象方法:渲染貼圖
abstract render(): void
// 抽象方法,返回模型
abstract model(): ConstructorModel
// 抽象方法:返回模型數量
abstract num(): number
// 初始化canvas
protected createCanvas() {
// 最終元素要放到我們的app的div中
// @ts-ignore
this.app.insertAdjacentElement('afterbegin', this.el)
}
// 繪製模型,生成模型實例,只負責創建實例
// protected:子類可以調用,外部不能調用
//num: 渲染多少個數量
//model: 模型
protected createModels() {
position.getPositionCollection(this.num()).forEach((position) => {
const model = this.model()
const instance = new model(position.x, position.y)
this.models.push(instance)
}
// 畫布渲染模型(將模型渲染到畫布上)
protected renderModels() {
this.models.forEach(model => model.render())
}
}
效果一致: