【Tank】9.0 創建玩家;玩家坦克控制、碰撞檢測、子彈、坦克互戰

創建玩家

首先加載貼圖
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()
        }
    }

......

玩家坦克被打掉了:


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