表示順を制御する

このページのサンプルコードをダウンロード

エンティティの入れ子

シーンにエンティティを append() できるのと同様、エンティティにもまた別のエンティティを append() することができます。 すなわちエンティティは入れ子構造にできます。

エンティティ pc があり、 p.append(c) された時、c を「 p の子エンティティ」と呼びます。 p を「 c の親エンティティ」と呼びます。

エンティティを入れ子構造にすることには、二つの意義があります。

  • 複数のエンティティをまとめて操作する
  • 表示順を制御する

第一に、複数のエンティティを一度に操作したい時に利用できます。 子エンティティは親エンティティに追従して動くためです。

特に E エンティティは、それ自体は何も描画しないエンティティで、複数のエンティティをグループ化する場合に便利です。

例えばエンティティ E に子エンティティとして FilledRect を追加する場合は、次のようなコードになります。

var group = new g.E({ scene: scene });
var child = new g.FilledRect({
  scene: scene,
  width: 30,
  height: 30,
  cssColor: "red"
});
group.append(child); // group の子エンティティに child を追加

以下のプログラムでは、angle を設定した E オブジェクトに 3 つの矩形を追加しています。矩形の座標系は E を基準にして変わるので、3 つの矩形は斜めに配置されます。

function main() {
  var scene = new g.Scene({ game: g.game });
  scene.onLoad.add(function() {
    var group = new g.E({ scene: scene, x: 50, y: 50, angle: 30 });
    var rect1 = createRect(scene, 0, 0, "darkgreen");
    group.append(rect1);
    var rect2 = createRect(scene, 30, 0, "darkorange");
    group.append(rect2);
    var rect3 = createRect(scene, 60, 0, "darkred");
    group.append(rect3);
    scene.append(group);
  });
  g.game.pushScene(scene);
}

function createRect(scene, x, y, color) {
  return new g.FilledRect({
    scene: scene,
    width: 30,
    height: 30,
    x: x,
    y: y,
    cssColor: color
  });
}

module.exports = main;

Tips: new する時に親が決まっているなら、次のように parent プロパティで親を指定してしまうこともできます。

var group = new g.E({ scene: scene });
var child = new g.FilledRect({
  scene: scene,
  width: 30,
  height: 30,
  cssColor: "red",
  parent: group // parent プロパティで生成時に親を指定
});

描画順

エンティティを入れ子にする二つ目の意義は、描画順の制御です。

  • エンティティは、自分のどの子エンティティよりも先に (奥側に) 描画されます。
  • 同じ親を持つエンティティは、親に append() で追加された順に描画されます。

言い換えると、エンティティのツリーは行きがけ順 (pre-order) で描画されます。 これを利用することでエンティティの前後関係を制御できます。

子エンティティの変更

上では子エンティティの描画順を「 append() で追加された順」としましたが、子エンティティの順序 (=描画順) は、追加後でも自由に制御することもできます。

まず append() されたエンティティは、 remove() メソッドで親エンティティから切り離すことができます。

group.remove(child); // group の子エンティティから child を削除

エンティティの remove() を引数なしで呼び出すと、自分を親から切り離します。つまり上のコードは次のように書くこともできます。

child.remove(); // child の親の子エンティティから child を削除

切り離されたエンティティは、また別のエンティティにの子になることができます。 append() された時親エンティティが既にある場合は、切り離してから新たな親の子エンティティになります。

また append() の代わりに insertBefore() を利用することもできます。 append() は常に「最後の子要素として」追加する関数ですが、 insertBefore() は子エンティティの中での順番を指定できます。次のように記述した場合、

parent.insertBefore(child, target);

parent の子エンティティ target の直前の子要素として child を追加します。

前述のとおり、子エンティティ間の順序を制御することで、描画順を制御できることに注意してください。 たとえばゲーム内に "地面" と "キャラクタ" と "宝箱" があり、「宝箱の Sprite はいつ生成してもキャラクタの "奥" に表示したい」というような場合に、 insertBefore() を使うことができます。

NOTE: もっとも、 "地面" と "地表のアイテム" と "キャラクタ" のように、描画順序の定まったカテゴリ分けができる場合、最初にそれらのカテゴリをまとめるエンティティを生成してしまう方が、単純になることもあるでしょう。

// 表示順別ごとにエンティティをグループ化するための E を作って、
var groundLayer = new g.E({ scene: scene });
var itemLayer = new g.E({ scene: scene });
var characterLayer = new g.E({ scene: scene });

// シーンに表示順で追加しておく。
scene.append(groundLayer);
scene.append(itemLayer);
scene.append(characterLayer);

// 地面画像は groundLayer に追加。
const groundSprite = new g.Sprite({
  scene: scene,
  src: scene.asset.getImageById("ground"),
  x: 0,
  y: 0,
  parent: groundLayer // 親を groundLayer に
});

// キャラ画像は characterLayer に追加。
const playerSprite = new g.Sprite({
  scene: scene,
  src: scene.asset.getImageById("player"),
  x: 100,
  y: 100,
  parent: characterLayer // 親を characterLayer に
});

このように生成しておけば、 "アイテム" は常に itemLayer の子にするだけで適切な順序で描画されます。 insertBefore() での細かい順序制御は、 "アイテム" 間の表示順を制御する必要がなければ、必要ありません。