微信小游戏体验之打飞机改造计划

枫之叶2018-12-06 12:33:56

微信小游戏推出已有几天了,这个功能对小程序和小游戏的推动影响不用多说,大家赶紧摩拳擦掌往上撸就可以了。关于如何开发官方文档已经说明了,这篇则是对官方的 打飞机demo一些小改造。

开发预备式

  1. 下载最新版本的 微信开发者工具(v1.02.1712280)

  2. 根据官方文档说明,目前不提供公开注册。因此目前只能使用无AppID模式进行体验

  3. 为了让 HTML5游戏轻松接入,官方提供了 Adapter。这个的作用就是提供 HTML5写法和 wx写法的全局转换层。

非开发人员的话看到这就可以大概了解小游戏这个开发形态了,以下部分就全部是码代码部分了。

打飞机小游戏

使用 AppID模式创建一个微信小游戏后可以看到官方demo,其中入口文件和配置文件: game.jsgame.jsongame.js引入并初始化包含整个 打飞机的游戏场景、参与者(玩家飞机和敌方飞机)、游戏逻辑的主函数的 main.js。在 main.js中我们可以发现由于 Adapter的存在,这里的代码和我们平常的代码写法没什么差异了。游戏的主逻辑如下图:

在loop中,玩家每隔20帧射一次,每隔60帧生成新的敌机。每帧检查玩家和敌机是否死亡,玩家死亡游戏结束,敌机死亡分数+1。只有玩家可以射击,且射击方式固定,通过躲避敌机生存。接下来我们针对这些进行改造,提升游戏的可玩性和挑战性。

玩家升级计划

  1. 玩家初始等级为1,玩家可通过击杀敌机升级,每击落30敌机升级一次

  2. 玩家每升级一次,增加一个射击口

  3. 玩家最多升级两次

首先用编辑器打开 player/index.js,将等级逻辑加入到玩家的类中。

  1. export default class Player extends Sprite {

  2.  constructor() {

  3.    super(PLAYER_IMG_SRC, PLAYER_WIDTH, PLAYER_HEIGHT)

  4.    // 玩家默认处于屏幕底部居中位置

  5.    this.x = screenWidth / 2 - this.width / 2

  6.    this.y = screenHeight - this.height - 30

  7.    // 用于在手指移动的时候标识手指是否已经在飞机上了

  8.    this.touched = false

  9.    this.bullets = []

  10.    // 初始化事件监听

  11.    this.initEvent()

  12.    this.playerLevel = 1;

  13.  }

  14.  get level () {

  15.    return this.playerLevel;

  16.  }

  17.  set level (level) {

  18.    this.playerLevel = Math.min(level, 3);

  19.  }

接下来在 main.jsupdate函数加入升级逻辑。

  1. // 其他代码...

  2.    update() {

  3.        this.bg.update();

  4.        databus.bullets.concat(databus.enemys).forEach(item => {

  5.            item.update();

  6.        });

  7.        this.enemyGenerate();

  8.        this.player.level = Math.max(1, Math.ceil(databus.score / 30));

  9.        this.collisionDetection();

  10.    }

  11. // 其他代码...

好的,到此玩家已经可以正常升级了。那么该给予玩家奖励品了。在 player/index.jsshoot函数中我们修改射击的逻辑。玩家1级时只有中间的射击口,2级有左边和中间的射击口,3级有左中右三个射击口。

  1. // ...其他代码

  2.    /**

  3.     * 玩家射击操作

  4.     * 射击时机由外部决定

  5.     */

  6.    shoot() {

  7.      for(let i = 0; i < this.level; i++) {

  8.        const bullet = databus.pool.getItemByClass('bullet', Bullet);

  9.        const middle = this.x + this.width / 2 - bullet.width / 2;

  10.        const x = !i ? middle : (i % 2 === 0 ? middle + 30 : middle - 30);

  11.        bullet.init(

  12.          x,

  13.          this.y - 10,

  14.          10

  15.        )

  16.        databus.bullets.push(bullet)

  17.      }

  18.    }

  19. // ...其他代码

武器的最终形态如图, 这时候的玩家已经可以为所欲为了<_<,实际上都不需要躲避了。。。

敌人的反击号角

为了对抗愚昧的玩家,不让他们为所欲为,最后没兴趣玩下去~~,敌机装备武器,反击开始。

首先敌机的子弹是向下,所以复制一份 images/bullet.png,并颠倒保存为 images/bullet-down.png, 然后我们重用 js/player/bullet.js,在构造函数处增加敌机的子弹配置项,并修改敌人子弹更新逻辑。

  1. const BULLET_IMG_SRC = 'images/bullet.png'

  2. const BULLET_DOWN_IMG_SRC = 'images/bullet-down.png'

  3. const BULLET_WIDTH   = 16

  4. const BULLET_HEIGHT  = 30

  5. const __ = {

  6.    speed: Symbol('speed')

  7. }

  8. let databus = new DataBus()

  9. export default class Bullet extends Sprite {

  10.    constructor({ direction } = { direction: 'up' }) {

  11.        super(direction === 'up' ? BULLET_IMG_SRC : BULLET_DOWN_IMG_SRC, BULLET_WIDTH, BULLET_HEIGHT)

  12.        this.direction = direction;

  13. // 其他代码...

  14.    // 每一帧更新子弹位置

  15.    update() {

  16.        if (this.direction === 'up') {

  17.            this.y -= this[__.speed]

  18.            // 超出屏幕外回收自身

  19.            if ( this.y < -this.height )

  20.                databus.removeBullets(this)

  21.        } else {

  22.            this.y += this[__.speed]

  23.            // 超出屏幕外回收自身

  24.            if ( this.y > window.innerHeight + this.height )

  25.                databus.removeBullets(this)

  26.        }

  27.    }

  28. }

接着在 js/npc/enemy.js结尾部分为敌人装备武器, 子弹速度为敌人自身速度 +5

  1. import Animation from '../base/animation'

  2. import DataBus   from '../databus'

  3. import Bullet from '../player/bullet';

  4. const ENEMY_IMG_SRC = 'images/enemy.png'

  5. // 其他代码...

  6.  update() {

  7.    this.y += this[__.speed]

  8.    // 对象回收

  9.    if ( this.y > window.innerHeight + this.height )

  10.      databus.removeEnemey(this)

  11.  }

  12.  /**

  13.   * 敌机射击操作

  14.   * 射击时机由外部决定

  15.   */

  16.  shoot() {

  17.      const bullet = databus.pool.getItemByClass('bullet', Bullet);

  18.      bullet.init(

  19.          this.x + this.width / 2 - bullet.width / 2,

  20.          this.y + 10,

  21.          this[__.speed] + 5

  22.      );

  23.      databus.bullets.push(bullet);

  24.  }

  25. }

接下来,在 js/main.js中加入敌机的射击逻辑,敌机移动5次、60次时设计。

  1. // 其他代码...

  2. let ctx = canvas.getContext("2d");

  3. let databus = new DataBus();

  4. const ENEMY_SPEED = 6;

  5. // 其他代码...

  6.    /**

  7.     * 随着帧数变化的敌机生成逻辑

  8.     * 帧数取模定义成生成的频率

  9.     */

  10.    enemyGenerate(playerLevel) {

  11.        if (databus.frame % 60 === 0) {

  12.            let enemy = databus.pool.getItemByClass("enemy", Enemy);

  13.            enemy.init(ENEMY_SPEED);

  14.            databus.enemys.push(enemy);

  15.        }

  16.    }

  17. // 其他代码...

  18.    // 实现游戏帧循环

  19.    loop() {

  20.        databus.frame++;

  21.        this.update();

  22.        this.render();

  23.        if (databus.frame % 20 === 0) {

  24.            this.player.shoot();

  25.            this.music.playShoot();

  26.        }

  27.        databus.enemys.forEach(enemy => {

  28.            const enemyShootPositions = [

  29.                -enemy.height + ENEMY_SPEED * 5,

  30.                -enemy.height + ENEMY_SPEED * 60

  31.            ];

  32.            if (enemyShootPositions.indexOf(enemy.y) !== -1) {

  33.                enemy.shoot();

  34.                this.music.playShoot();

  35.            }

  36.        });

  37.        // 游戏结束停止帧循环

  38.        if (databus.gameOver) {

  39.            this.touchHandler = this.touchEventHandler.bind(this);

  40.          canvas.addEventListener("touchstart", this.touchHandler);

  41.            this.gameinfo.renderGameOver(ctx, databus.score);

  42.            return;

  43.        }

  44.        window.requestAnimationFrame(this.loop.bind(this), canvas);

  45.    }

这时候我们发现,由于不明宇宙的干扰射线的影响,玩家和敌机的子弹不受控制的乱飞。接下来我们就来恢复世界的秩序吧 ;

经侦测发现是对象池 pool的获取逻辑问题导致子弹不受控问题,我们需要区分获取玩家、每个敌机的子弹

首先,对象获取我们加入对象属性的判断,当有传入对象属性时,我们获取所有属性值一致的已回收对象,若没有找到或者对象池为空时,则用属性创建新对象

  1.  /**

  2.   * 根据传入的对象标识符,查询对象池

  3.   * 对象池为空创建新的类,否则从对象池中取

  4.   */

  5.  getItemByClass(name, className, properties) {

  6.    let pool = this.getPoolBySign(name)

  7.    if (pool.length === 0) return new className(properties);

  8.    if (!properties) return pool.shift();

  9.    const index = pool.findIndex(item => {

  10.        return Object.keys(properties).every(property => {

  11.            return item[property] === properties[property];

  12.        });

  13.    });

  14.    return index !== -1 ? pool.splice(index, 1)[0] : new className(properties)

  15.  }

相应的我们需要给每个子弹设置归属,在 js/player/bullet.jsBullet类修改 constructor

  1. export default class Bullet extends Sprite {

  2.    constructor({ direction, owner } = { direction: 'up' }) {

  3.        super(direction === 'up' ? BULLET_IMG_SRC : BULLET_DOWN_IMG_SRC, BULLET_WIDTH, BULLET_HEIGHT)

  4.        this.direction = direction;

  5.        this.owner = owner;

  6.    }

接着修改 js/player/index.jsshoot,为其中创建的 bullets提供归属

  1.    /**

  2.     * 玩家射击操作

  3.     * 射击时机由外部决定

  4.     */

  5.    shoot() {

  6.      for(let i = 0; i < this.level; i++) {

  7.        const bullet = databus.pool.getItemByClass('bullet', Bullet, { direction: 'up', owner: this });

同样处理 js/npc/enemy.jsshoot

  1.  /**

  2.   * 敌机射击操作

  3.   * 射击时机由外部决定

  4.   */

  5.  shoot() {

  6.      const bullet = databus.pool.getItemByClass('bullet', Bullet, { direction: 'down', owner: this });

最后处理 js/databus.jsremoveBullets的回收逻辑

  1.  /**

  2.   * 回收子弹,进入对象池

  3.   * 此后不进入帧循环

  4.   */

  5.  removeBullets(bullet) {

  6.    const index = this.bullets.findIndex(b => b === bullet);

  7.    bullet.visible = false

  8.    this.bullets.splice(index, 1);

  9.    this.pool.recover('bullet', bullet)

  10.  }

  11. }

这时候敌我的子弹就恢复正常了。不过这时候玩家中弹并不会死亡,现在来让玩家 GoDie吧。在 js/main.jscollisionDetection我们判断增加每一颗子弹如果是敌方的,就判断其是否打中玩家,是则游戏结束。玩家的子弹判断保持不变。

  1.    // 全局碰撞检测

  2.    collisionDetection() {

  3.        let that = this;

  4.        databus.bullets.forEach(bullet => {

  5.            for (let i = 0, il = databus.enemys.length; i < il; i++) {

  6.                let enemy = databus.enemys[i];

  7.                if (bullet.owner instanceof Enemy) {

  8.                    databus.gameOver = this.player.isCollideWith(bullet);

  9.                } else if (!enemy.isPlaying && enemy.isCollideWith(bullet)) {

  10.                    enemy.playAnimation();

  11.                    that.music.playExplosion();

  12.                    bullet.visible = false;

  13.                    databus.score += 1;

  14.                    break;

  15.                }

  16.            }

  17.        });

到此整个简单改造计划就结束了,以后还可以添加武器系统,boss战等等。下面是改造后的游戏动图录屏