【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() // 画布渲染:玩家坦克
}
......
image.png

玩家坦克控制

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)!
    }
}
image.png

这里可以看到,键盘按键区别类别有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)!
    }
}

这里切换方向,会出现卡死。


image.png

修复时间无限绑定

在我们的模型代码中,调用this.canvas.renderModels(),会执行模型的render()方法,随着越按越多,模型会无限增加事件的绑定,最终导致卡死。

image.png

解决方法:增加一个事件是否绑定的标记位。
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;
            }
        }
......
image.png

玩家坦克子弹发射

/**
 * 模型
 * 敌方坦克
 */
......
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]))
        }
......
image.png

坦克互战

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

......

玩家坦克被打掉了:


image.png

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):