[Akashic Engine] カメラを使うと画面固定のエンティティを配置出来ない問題

## カメラについて

カメラの公式による解説ページ
https://akashic-games.github.io/tutorial/v3/multiplay/camera.html

また、ここでは２次元のカメラ（Camera2Dクラス）を対象に話します

カメラはシーンに配置された全てのエンティティの見え方と当たり判定を変更します
例えばカメラをX:10,Y:10にした場合、X:10,Y:10にあるエンティティがちょうど画面の左上角に表示されます
見た目だけでなく当たり判定（タッチ判定）も一緒に動きます
アクションゲームで自機を中心に画面を動かしたり、将棋で画面を回転させる時にとても便利です

しかし、カメラはシーンに配置された全てのエンティティに対して影響を与えてしまうため、ボタンやスコアなど、画面に固定したいエンティティもカメラの影響を受けて動いてしまします

これを解決するための方法は２つ
・[逆変換](#解決策１-カメラの位置を逆変換する)　カメラの操作とちょうど逆の操作を固定したいエンティティに行う
・[カメラを使わない](#解決策２-カメラを使わない)　カメラの問題を解決しつつ、カメラと同じ操作を行う方法を考えます



## 解決策１ カメラの位置を逆変換する

（[解決策２](#解決策２-カメラを使わない)の方がおすすめです）

カメラの操作の逆の値を掛けることで、カメラの変更を打ち消します
カメラを動かすと固定したいエンティティはその逆に動かすと、カメラの操作を打ち消すことができます
（しかし角度や拡大率は純粋に逆の値を掛けるだけでは解決しない）


[公式の解説](https://akashic-games.github.io/tutorial/v3/multiplay/camera.html)では、この方法を使って解決しています
※この方法で解決出来るのはエンティティの表示のみ。当たり判定はカメラの影響を受けたままなのでズレた位置になっている
※カメラのアンカーを１以外にした場合、表示もズレてしまう

<details>
  <summary>公式の解決策の解説</summary>

公式の解決策で触れられている[`DefaultLoadingScene`](https://github.com/akashic-games/akashic-engine/blob/main/src/DefaultLoadingScene.ts)を見てみると、`_handleLoad`メソッド内で[`CameraCancellingE`](https://github.com/akashic-games/akashic-engine/blob/main/src/entities/CameraCancellingE.ts)というクラスが見つかります

このクラスは描画を行う`renderSelf`メソッド内でレンダラーに対してカメラの逆の値を掛けることで、カメラが行ったレンダラーに対する操作を打ち消しています
</details>
  

しかしこの方法では問題を完全に解決出来ていません
カメラのアンカーが１以外の場合、表示がズレてしまいます
(この問題を解決するにはアンカーもカメラの逆を掛けてあげれば良いのだが、アンカーは割合で指定するためエンティティとカメラのサイズの比を元に計算する必要がある)
また、`CameraCancellingE`は見た目はカメラの影響を受けていないように見えますが、当たり判定はカメラの影響を受けたままになっています
そのため、ボタンなど当たり判定を計算する必要があるエンティティがある場合この方法は使えません


## 解決策２ カメラを使わない

カメラを使うと全てのエンティティが影響を受けるのは避けられません
ということで、カメラを使わないようにします

代わりにカメラと同じ操作を子要素にのみ行うエンティティクラスを作成します
作りました
g.Eクラスを継承した`CameraEffect`クラスです (命名はこれで良い？)

カメラの影響を受けるエンティティを絞ることが出来るため、画面に固定するエンティティに付いて考える必要がなくなります

<details>
  <summary>コードは折りたたみの中</summary>
  
```typescript:CameraEffect.ts
/**
 * `g.Camera2D`と同じ効果を子要素に対して行うエンティティ
 */
export class CameraEffect extends g.E {
  constructor(param: g.EParameterObject) {
    super(param);
  }

  render(renderer: g.Renderer, camera?: g.Camera): void {
    // このメソッドはほぼg.Eとg.Cameraの実装まま
    // https://github.com/akashic-games/akashic-engine/blob/4fd5eb2b8d2b658c7ce8167ae95b3e77c47ab5e7/src/entities/E.ts#L376
    // https://github.com/akashic-games/akashic-engine/blob/4fd5eb2b8d2b658c7ce8167ae95b3e77c47ab5e7/src/Camera2D.ts#LL116C13-L116C13

    if (!this.children) return;

    this.state &= ~g.EntityStateFlags.Modified;
    if (this.state & g.EntityStateFlags.Hidden) return;
    if (this.state & g.EntityStateFlags.ContextLess) {
      // カメラの angle, x, y はエンティティと逆方向に作用することに注意。
      renderer.translate(-this.x, -this.y);
      const goDown = this.renderSelf(renderer, camera);
      if (goDown && this.children) {
        const children = this.children;
        const len = children.length;
        for (let i = 0; i < len; ++i) children[i].render(renderer, camera);
      }
      renderer.translate(this.x, this.y);
      return;
    }

    renderer.save();

    if (
      this.angle ||
      this.scaleX !== 1 ||
      this.scaleY !== 1 ||
      this.anchorX !== 0 ||
      this.anchorY !== 0
    ) {
      // Note: this.scaleX/scaleYが0の場合描画した結果何も表示されない事になるが、特殊扱いはしない
      renderer.transform(this.getMatrix()._matrix);
    } else {
      // Note: 変形なしのオブジェクトはキャッシュもとらずtranslateのみで処理
      renderer.translate(-this.x, -this.y);
    }

    if (this.opacity !== 1) renderer.opacity(this.opacity);

    const op = this.compositeOperation;
    if (op !== undefined) {
      renderer.setCompositeOperation(
        typeof op === "string" ? op : g.Util.compositeOperationStringTable[op]
      );
    }

    if (this.shaderProgram !== undefined && renderer.isSupportedShaderProgram())
      renderer.setShaderProgram(this.shaderProgram);

    // Note: concatしていないのでunsafeだが、render中に配列の中身が変わる事はない前提とする
    const children = this.children;
    for (let i = 0; i < children.length; ++i) children[i].render(renderer, camera);

    renderer.restore();
  }

  _updateMatrix(): void {
    // g.Cameraの実装まま https://github.com/akashic-games/akashic-engine/blob/4fd5eb2b8d2b658c7ce8167ae95b3e77c47ab5e7/src/Camera2D.ts#LL129C1-L129C1

    if (!this._matrix) return;
    // カメラの angle, x, y はエンティティと逆方向に作用することに注意。
    if (
      this.angle ||
      this.scaleX !== 1 ||
      this.scaleY !== 1 ||
      this.anchorX !== 0 ||
      this.anchorY !== 0
    ) {
      this._matrix.updateByInverse(
        this.width,
        this.height,
        this.scaleX,
        this.scaleY,
        this.angle,
        this.x,
        this.y,
        this.anchorX,
        this.anchorY
      );
    } else {
      this._matrix.reset(-this.x, -this.y);
    }
  }
}
```
</details>


### 使い方と使用例
`CameraEffect`を生成して、カメラの影響を受けさせるエンティティを子に入れるだけです

**注意**
CameraEffectのanchorを1以外にするとCameraEffect自身の当たり判定がズレてしまいます
そのためCameraEffectではタッチイベントを受け付けない方が良いです
（子要素の当たり判定は問題ないです。問題ない？）

以下がサンプルです
10つの灰色の矩形はカメラの影響を受けます
赤色の矩形はカメラの影響を受けません

それぞれの矩形をクリックで色を変更
それ以外の場所をクリックしたまま動かすことで、カメラを動かすことができます
キーボードでカメラを操作出来ます (動作未保証です)
a:回転 s:拡大 d:anchor+0.1 q:初期化

```typescript:サンプル

    const scene = g.game.env.scene;

    // カメラ (カメラではないが今はカメラと呼ぶ)
    const cameraE = new CameraEffect({
      scene,
      parent: scene,
      width: g.game.width,
      height: g.game.height,
      anchorX: 0.5,
      anchorY: 0.5
    });
    scene.onPointMoveCapture.add(e => {
      if (e.target != null) return;
      cameraE.moveBy(-e.prevDelta.x, -e.prevDelta.y);
      cameraE.modified();
    });

    // カメラの中に入れるエンティティ. カメラに合わせて動く
    // カメラが動いても当たり判定が正しい事を確認する
    for (let i = 0; i < 10; i++) {
      const inE = new g.FilledRect({
        scene,
        parent: cameraE,
        cssColor: "#888",
        x: -250 + i * 50,
        y: -250 + i * 50,
        width: 50,
        height: 50,
        touchable: true,
        anchorX: 0.5,
        anchorY: 0.5
      });
      inE.onPointDown.add(() => {
        inE.cssColor = inE.cssColor === "#888" ? "#000" : "#888";
        inE.modified();
      });
    }

    // カメラの外のエンティティ. カメラの影響を受けないことを確認する
    const outE = new g.FilledRect({
      scene,
      parent: scene,
      cssColor: "red",
      x: 10,
      y: 10,
      width: 100,
      height: 100,
      touchable: true
    });
    outE.onPointDown.add(() => {
      outE.cssColor = outE.cssColor === "red" ? "blue" : "red";
      outE.modified();
    });

    // キーボード操作 (動作未保証です)  a:回転 s:拡大 d:anchor+0.1 q:初期化
    if (typeof document !== "undefined") {
      document.onkeydown = ev => {
        if (ev.key === "a") cameraE.angle += 10;
        else if (ev.key === "s") cameraE.scale(cameraE.scaleX + 0.1);
        else if (ev.key === "d") {
          let anchor = cameraE.anchorX! + 0.1;
          if (anchor > 1) anchor = 0;
          cameraE.anchor(anchor, anchor);
        } else if (ev.key === "q") {
          cameraE.moveTo(0, 0);
          cameraE.angle = 0;
          cameraE.scale(1);
          cameraE.anchor(0.5, 0.5);
        } else return;

        cameraE.modified();
      };
    }
```