阅读 3398

HTML5游戏开发(五):飞机大战之让所有元素动起来

《HTML5游戏开发》系列文章的目的有:一、以最小的成本去入门egret小项目开发,官方的教程一直都是面向中重型;二、egret可以非常轻量;三、egret相比PIXI.js和spritejs文档更成熟、友好;四、学习从0打造高效的开发工作流。

本文我们将会让游戏的所有元素动起来。这包括:

  • 让背景从上到下循环移动,实现飞机向上飞行的效果。通过使两张相同的背景图上下交替的方式就可以实现。
  • 让敌机不断地从上方出现,并持续地向友机移动。

游戏完整源码:github.com/wildfirecod…

在线展示:wildfirecode.com/egret-plane…

动画实现方式:按帧刷新

游戏画面每刷新一次,即为一帧。如果我们能在每一帧都对场景元素的属性进行变更,这样我们就获得动态效果。

为了实现帧循环,我们向egret.startTick注册并启动一个计时器onTick,它通常会以60FPS的速率触发回调方法。这里的FPS即多少帧每秒。另外,我们会提前创建一个数组用来存放所有需要进行按帧刷新的对象,那么前提条件是必须实现IOnTick接口的方法onTick。这些对象的类包括处理背景循环移动的Background以及控制敌机AI的EnemyAI

_IOnTicks: IOnTick[];//用来存放所有需要进行按帧刷新的对象
async onAddToStage() {
    this._IOnTicks = [];//提前创建这个数组
    ...
    this.createGame();
    ...
    egret.startTick(this.onTick, this);
    ...
}

createGame() {
    ...
    const background = new Background();//利用帧循环实现背景循环移动
    this._IOnTicks.push(background);//保存到数组
    this.addEnemy();//添加一个敌机
}

addEnemy() {
    ...
    const enemy = new Enemy();//我们在Enemy类中实例化了EnemyAI
    this._IOnTicks.push(enemy.AI);//保存到数组
}

onTick() {
    this._IOnTicks.forEach(val => val.onTick());//执行所有需要按帧刷新的对象的onTick方法
    return false;
}
复制代码

EnemyAIBackground都要实现接口IOnTick

class EnemyAI extends egret.EventDispatcher implements IOnTick {
    onTick() {
        //这里每帧都会执行
    }
}
class Background implements IOnTick {
    onTick() {
        //这里每帧都会执行
    }
}
复制代码

让背景动起来

为了实现背景循环移动的效果,我们需要创建两个同样背景图像的位图。 我们创建了工具方法cloneImage来克隆一个位图。对这个方法传入一个egret.Bitmap,你将会获得一个一模一样的egret.Bitmap

// cloneImage API
const cloneImage: (bitmap: egret.Bitmap) => egret.Bitmap
复制代码
 createGame() {
    const [bg, hero, enemy] = this._bitmaps;
    this.addChild(bg);
    const bg2 = cloneBitmap(bg);
    this.addChild(bg2);//将克隆的背景图也添加到舞台
    ...
    const background = new Background(bg, bg2);
    ...
}
复制代码

最后,我们来看看Background类是如何实现背景循环的功能。

import IOnTick from "./IOnTick";
export default class Background implements IOnTick {
    _bg: egret.Bitmap;
    _bg2: egret.Bitmap;
    constructor(bg: egret.Bitmap, bg2: egret.Bitmap) {
        this._bg = bg;
        this._bg2 = bg2;
        this._bg2.y = -bg2.height;
    }
    
    onTick() {
        const SPEED = 8;//每帧里背景都会向下移动8px
        this._bg.y += SPEED;
        this._bg2.y += SPEED;
        const height = this._bg.height;//背景交替移动
        if (this._bg.y > height) {
            this._bg.y = this._bg2.y - height;
        }
        if (this._bg2.y > height) {
            this._bg2.y = this._bg.y - height;
        }
    }
}
复制代码

让敌机动起来

为了每秒生成一架敌机,我们必须要有一个敌机的模板位图。 另外,当敌机移动到屏幕之外时,我们要彻底的销毁它。我们做以下设计:

  • EnemyAI类负责敌机移至屏幕之外的算法以及在移至屏幕之外时广播onEnemyDisappear事件。
  • Enemy类负责从显示层销毁敌机。
  • Main.removeEnemy方法负责将EnemyAI对象移除帧刷新列表,避免不必要的计算。
createGame() {
    const [bg, hero, enemy] = this._bitmaps;
    ...
    this._enemyTemplate = enemy;//将敌机模板保存起来
    setInterval(() => this.addEnemy(), 1000);//每1000ms生成一架敌机
}

addEnemy() {
    const enemyImage = cloneImage(this._enemyTemplate);//克隆敌机图片
    this.addChild(enemyImage);
    this.centerAnchor(enemyImage);
    const enemy = new Enemy(enemyImage);
    enemy.AI.once('onEnemyDisappear', this.onEnemyDisappear, this);//监听敌机移出屏幕的广播事件
    this._IOnTicks.push(enemy.AI);
}

onEnemyDisappear(e: egret.Event) {
    const AI = e.currentTarget as EnemyAI;
    AI.enemy.destroy();//将敌机从显示层销毁
    this.removeEnemy(AI);//将EnemyAI对象移除帧刷新列表,避免不必要的计算。
}

removeEnemy(enemyAI: EnemyAI) {
    const index = this._IOnTicks.indexOf(enemyAI);
    this._IOnTicks.splice(index, 1);//移除帧刷新对象列表
}
复制代码

Enemy类负责从显示层销毁敌机

import EnemyAI from "./EnemyAI";
export default class Enemy {
    image: egret.Bitmap;
    AI: EnemyAI;
    constructor(image: egret.Bitmap) {
        this.image = image;
        this.AI = new EnemyAI(this);//实例化敌机AI
    }

    removeImage() {
        this.image.parent.removeChild(this.image);//从显示层移除
    }

    destroy() {
        this.removeImage();
    }
}
复制代码

EnemyAI

import Enemy from "./Enemy";
import IOnTick from "../IOnTick";

class EnemyAI extends egret.EventDispatcher implements IOnTick {
    enemy: Enemy;
    _image: egret.Bitmap;
    initialX: number;
    initialY: number;
    constructor(enemy: Enemy) {
        super();
        this._image = enemy.image;
        this.enemy = enemy;

        this.initialX = this._image.stage.stageWidth / 2;
        this.initialY = -100;

        this.setInitialPosition();//设置敌机的初始位置
    }

    private setInitialPosition() {
        this._image.x = this.initialX;
        this._image.y = this.initialY;
    }

    onTick() {
        this._image.y += 5; //每帧里敌机都会向下移动5px
        if (this._image.y > this._image.stage.stageHeight) { //判断是否移至屏幕之外
            //广播移至屏幕之外的事件
            this.dispatchEvent(new egret.Event('onEnemyDisappear'));
        }
    }
}
复制代码

运行效果