15. 経験値ジェム

はじめに

ヴァンサバのように敵を倒すと経験値ジェムを落とすようにしました。以下を実装しました。

  • 敵を倒すと経験値ジェムを落とす
  • プレイヤーの一定範囲のジェムはプレイヤーに引き寄せる(マグネット)
  • 経験値を集めると経験値バーが上昇する(Tween)
  • バーがMAXになるとレベルアップする

方針

経験値ジェムを新しいEntityとして作成します。ExpGemComponentを追加します。内容はexp(もらえる経験値)です。

敵が死んだイベント(onEnemyDied)で経験値ジェムを生成します。経験値の値は敵ごとに設定しておき、それを参照します。

実装

マグネット

ヴァンサバのように、プレイヤーの一定範囲内のジェムはプレイヤーに向かって飛んでくるようにします。 (今後、マグネット範囲をゲーム中に増加できるようにする予定です。)

これはExpGemSystemのupdateでプレイヤーと各ジェムの距離が一定範囲以内なら、プレイヤーに向けて移動するという処理を入れます。

update(_delta: number) {
  for (const [entity] of ComponentManager.entries("expGem")) {
    if (ComponentManager.has("expGainedFlag", entity)) continue; // 既に獲得している場合はスキップ
    // プレイヤーに近い場合は引き寄せる(マグネット)
    const { sprite } = ComponentManager.get("sprite", entity);
    const playerSprite = ComponentManager.get("sprite", ComponentManager.player).sprite;
    const { magnetRange, magnetSpeed } = ComponentManager.get("magnet", ComponentManager.player);
    const distance = getDistance(sprite, playerSprite);
    if (distance > magnetRange) continue;
    this.scene.physics.moveTo(sprite, playerSprite.x, playerSprite.y, magnetSpeed);
  }
}

同時に多数の経験値ジェムを取得したら

当初は、経験値ジェムとプレイヤーが接触したらGemCollectedイベントを発生させて、プレイヤーに経験値を追加する、という処理を考えていました。 しかし、この方法だと、同時に多数の経験値ジェムを取得した場合に、イベントが大量発生します。

そこで、以下のような流れにしました。これで100msごとにまとめて処理できます。

  • プレイヤーとジェムが接触したら、ジェムEntityにExpGainedFlagコンポーネントを追加
  • ExpGainedSystemのupdateで、100msに1回チェック
    • ExpGainedFlagフラグのついたジェムのExpを合計
    • フラグの付いたジェムはEntity削除
    • ExpGainedイベントを発行
  • LevelUpSystemExpGainedイベントハンドラ
    • レベルアップの計算を行う
    • プレイヤーの持つLevelExpコンポーネントを更新
    • LevelExpUpdatedイベントを発行
  • UISceneLevelExpUpdatedイベントハンドラ
    • UIのEXPバーを更新

UIシーンのExpバー

Expを取得したら、ExpバーがTweenしながら増加します。 ヴァンサバのように、大量のExpをまとめて取得すると、ExpバーがMaxまでいって次のレベルに上がってバーがリセットされ、またMaxまで上がって、一度に複数のレベルアップを行う演出を入れました。

このとき、Tweenのアニメーションは300msかけているのですが、Expの取得は100msごとに起きます。そうすると、アニメーション途中で、次のアニメーションが始まってしまい、見た目が破綻します。 そこで、アニメーションはQueueに入れて順番に行うようにしました。

  • LevelExpUpdatedイベントハンドラでは、ExpBarのsetProgressを呼ぶ
  • setProgressではQueueに追加してplayNextInQueueを呼び出す
  • playNextInQueueではアニメーション中ならreturnし、そうでなければQueueがなくなるまでアニメーションを再生
async setProgress(progress: number, level: number, exp: number) {
  this.animationQueue.push({ progress, oldLevel: this.currentLevel, level, exp });
  this.currentLevel = level;
  this.playNextInQueue();
}

private async playNextInQueue() {
  if (this.isAnimating || this.animationQueue.length === 0) return;

  this.isAnimating = true;
  const { oldLevel, level, exp, progress } = this.animationQueue.shift()!;

  for (let i = oldLevel; i < level; i++) {
    await this.animateTo(1, i, exp, 300);
    await this.animateTo(0, i + 1, exp, 100);
  }
  await this.animateTo(progress, level, exp, 300);

  this.isAnimating = false;
  this.playNextInQueue(); // 次のアニメーションへ
}
private animateTo(progress: number, level: number, exp: number, duration: number): Promise<void> {
  return new Promise((resolve) => {
    const clamped = Phaser.Math.Clamp(progress, 0, 1);
    const newWidth = clamped * this.barWidth;
    this.text.setText(`Level: ${level}, Exp: ${exp}`);
    this.scene.tweens.add({
      targets: this.frontBar,
      width: newWidth,
      duration,
      ease: "Sine.easeOut",
      onComplete: () => {
        resolve();
      },
    });
  });
}

まとめ

経験値ジェムを集めてレベルアップできるようにしました。 今はレベルの数字が変わるだけですが、次は、レベルアップごとにアップグレードできるようにします。