Phaser3 遠近の表現 depth(z-index)

phaser

Phaser3 遠近の表現 depth(z-index)

前回の記事
https://mijinc0.github.io/blog/post/20200820_pahser3_scene/

基本とDisplayList

Phaser3で描写されるオブジェクトは基本、順番を決める(重なり順を決める)depthと呼ばれるプロパティを持つ(一般的にはz-indexなどと呼ばれたりもする)。このプロパティを一切触らなかった場合、オブジェクトの描写はSceneクラス内でthis.add.XXXした順番に描写される。つまり、後にシーンに追加されたオブジェクトが手前に描写される。

先にSceneクラス内でthis.add.XXXしたオブジェクトを後に追加したオブジェクトよりも手前に表示したければ、手前に表示したいオブジェクトのdepthの値を後方に表示したいオブジェクトよりも大きな値に設定してあげることで達成できる。では実際のところ、depthの値を設定した時に何が起きているのかという話。

実際に描写の順番を決めているのはDisplayListと呼ばれる配列構造を持つオブジェクトで、そのシーンに描写されるオブジェクトは全てこの配列の中に入っている。depthを設定するとDisplayListdepthの値順にソートされるように作られているので、depth値を設定すると描写順が変わるという仕組みになっている。

DisplayList

https://github.com/photonstorm/phaser/blob/b46776a2c0a623bf2078b533cf29d02d24663fc0/src/gameobjects/DisplayList.js

Depth.depth

https://github.com/photonstorm/phaser/blob/9bb3a4ba19e2b0c70ac95237b227c387dc742f56/src/gameobjects/components/Depth.js#L43

ちなみに、各オブジェクトのdepthをセットする度にDisplayListソートするのではなく、オブジェクトのdepthをセットすると “このフレームでソートする” というフラグが立ち、つぎの描写時にソートされるようになっているので、一つのフレーム内で何度depthの値を設定しても一回の描写で一度しかソートされない。

DisplayListは各シーン毎に管理される。シーン管理の記事で複数シーンが稼働ている時はそれぞれのシーンの描写がされることを書いたが、DisplayListは各シーン毎に管理されているのであるシーン内のオブジェクトのdepthは他のシーンのオブジェクトのdepthに影響を与えない。例えば、先に描写されるシーンAのオブジェクトObjectA.depth = 100と後に描写されるシーンBのオブジェクトObjectB.depth = 0がある時、手前に表示されるのは後から描写されるシーン内にあるObjectBということになる。

遠近の表現

depthの調整で2Dゲームの遠近を表現するのは"おきまり"の話。

平面的なタイルマップの上を歩くオブジェクトは画面の上にあるオブジェクトはそれよりも下にあるオブジェクトよりも “奥に” あると思うのが普通だ。 奥にあるということは、重なった時に後ろに隠れるということ。

draw1

キャラクターがマップ上を移動する場合、そのy座標の位置によってdepthが変化する(マップの上に行けば行くほどdepthが小さくなる)ようにすれば、自然な遠近が付いているように見える。より具体的に言えば、キャラクターの下部のy座標とdepthが連動するようにすれば良いということ。

キャラクターの下部のy座標とdepthを連動させるには、いくつも方法があるが自然なのはy(座標)のセッターにdepth値も変更する処理を加える方法だと思う。処理を加えるにはそのゲームオブジェクトのクラスを継承したカスタムクラスを作るか、Object.definePropertyでその場にあるインスタンスを作り変えてしまうかになると思うが、正直好きな方で良いと思う。(ダックタイピングは用法用量を守って正しく使ってください)

import * as Phaser from 'phaser';

// PhysicsにArcadeを使っている場合
export class CustomSprite extends Phaser.Physics.Arcade.Sprite {
  private _y: number;

  constructor(
    scene: Phaser.Scene,
    x: number,
    y: number,
    texture: string,
    frame?: string | number,
  ) {
    super(scene, x, y, texture, frame);

    this.y = y;
  }

  get y(): number {
    return _y;
  }

  set y(v: number) {
    this._y = v;
    this.depth = v + this.height;
  }
}

こんな感じとか…

import * as Phaser from 'phaser';

function syncBottomAndDepth(sprite: Phaser.Sprite): void {
  let _y = sprite.y;

  Object.defineProperty(sprite, 'y', {
    get: () => {
      return _y;
    },
    set: (newY: number) => {
      _y = newY;
      const bottom = newY + sprite.height;
      sprite.depth = bottom;
    },
  });
}

こんな感じにすれば良い。

タイルマップについて

タイルマップを扱っていると、このタイルはキャラクラーの上に表示したい、このタイルはキャラクターのしたに表示したい などの要求がでることもあると思う。結論から言うと、タイルマップの各タイルのdepthを設定することは出来ない。それらが所属するレイヤーオブジェクトにdepthを設定する。つまり、

と分けて描写を操作する必要がある。もし、それ以上の要求が出る場合は、スプライトなどのオブジェクトとしてシーンに追加し、タイルマップとは別に管理する必要がある。