美文网首页让前端飞
【Tank】6.0 坦克动起来、渲染次数优化、坦克碰撞检测

【Tank】6.0 坦克动起来、渲染次数优化、坦克碰撞检测

作者: bobokaka | 来源:发表于2022-05-14 00:17 被阅读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()
    

    坦克最终会移动到边界停下。


    image.png

    重构模型渲染机制

    这里如果有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()
    

    效果一致:


    image.png

    坦克碰撞检测

    设定坦克遇到碰撞随机转向。
    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
        }
    }
    

    运行效果


    image.png

    运动优化

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

    运行效果如下:


    image.png
    增加往下运动概率

    很简单,修改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()
        }
    ......
    
    GIF.gif

    代码优化:优化模型代码

    主要优化方向为将画布抽象成属性实例。

        // 抽象属性:画布实例
         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())
        }
    }
    

    效果一致:


    image.png

    相关文章

      网友评论

        本文标题:【Tank】6.0 坦克动起来、渲染次数优化、坦克碰撞检测

        本文链接:https://www.haomeiwen.com/subject/wuxsurtx.html