【Tank】6.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'

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
    }
}

運行效果


運動優化

  1. 除了草地能穿越,別的不能穿越。
  2. 這個遊戲需要最後敵方坦克打掉我方底部的老巢,那麼就需要增加敵方坦克往下的運動概率。
其他物體碰撞

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())
    }
}

效果一致:


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章