御託はいらねえ!とりあえずイイカンジのカメラ構造


グレンジ Advent Calendar 2018 2日目担当の、flankids と申します。
グレンジでUnityによる新規ゲームアプリ開発のクライアントエンジニアをしております。

今回は、Unityで3Dゲームを開発する上でのカメラワークについてのお話です。よろしくお願い致します!

ゲームのカメラ

「カメラがなんとなくイケてねぇ!」

3Dゲーム開発歴3年、とにかくここにぶつかりまくり。
そしてレビューした結果
「もっと寄る感じにしたい」
「プレイヤーがもうちょい下に映るといいよね」
「上からもっとあおる感じがいいんじゃない?」
といろいろ出るものの、設計がイマイチ※でどうもすぐ反映し辛い…

※カメラの位置取りの基本仕様はエンジニアが持つことが多く、映したい「画」の仕様は出してもらうものの、実現可能なのか、どう繋げるべきなのか、カメラ素人なりに考えないといけません

その背景として以下のようなことがありました。

"ゲーム向け"のカメラワークの情報が少ない

  • イラストや写真向けの、カメラアングルが与える効果、技法
  • 映像向けの、カメラの移動や回転が与える効果、技法

などについての情報は非常に豊富で、参考になるのですが、アルゴリズムに落とし込みづらいという問題があります。
そもそもどういう位置関係にしたら被写体が画面中央に来るのか?
回り込みや追いかけるような動きはどう作ればいいのか?
「プログラマーはアルゴリズムが欲しいんだ!!!!(心の叫び)」

体系化された情報がディープすぎる

ゲーム向けのカメラワークの技術書もあるにはあるんですが、広いジャンルのカメラワークを網羅してそれぞれが持つ効果を解説していたり、具体的なアルゴリズム(ソースコード)が少なかったりします。
身勝手な話、「とりあえずサクッと簡単にできること教えてくれや」という人もいるはず(僕です)。

処理負荷やリソース量を抑えるために背景を特定の角度からしか映せない!という事情もあるかもですが、やっぱりスマホでもいろんな視点からモノが見えるビューを実現したい!

ということで

本記事では

  • 動的に動くゲームのカメラを前提にする!
  • とにかくすぐ実践できる!
  • 専門知識がなくても調整できる!

をテーマに、エンジニア向けのカメラ実装の入門書を意識して情報をまとめました。
レッツトライ。

前提

3Dアクションゲームに多い、TPSのカメラ挙動を作るための構造を解説します。
FPSやレースゲームなどでも同じ構造は使えると思うので、カメラワークづくりの第一歩として参考になれば幸いです。

△こういう移動するときのカメラや

△こういう敵と対峙したときのカメラとかが実装できるようになる(はず)

実装

カメラの操作ロジックを実装するために、
カメラを制御するためのパラメータを用意します。

オブジェクト構成

まず、下記のようにCameraを入れ子構造の孫にしてください。

完成です。楽ちんですね。

この構成の中で、それぞれ下記の要素が、記載してあるパラメータに対応しています。

パラメータ 要素
注視点 CameraParentの座標
注視点からの距離 CameraChildのローカルZ座標(負数)
注視点への回り込み角度 CameraParentの角度(X,Y)
視界オフセット座標 Main Cameraのローカル座標(X,Y)

例1

要素
CameraParentの座標 (0, 0, 0)
CameraChildのローカルZ座標 -10
CameraParentの角度(X,Y) (30, 0)
Main Cameraのローカル座標(X,Y) (0, 1)

※キャラクターのモデルの座標、角度共に(0, 0, 0)のとき
※Main CameraのFieldOfView(画角)が60のとき
※アスペクト比は9:16(≒iPhone6)

このように値を入れると、下記のようなビューになるはずです。


△白い箱はサイズ比較用。1㎥の立方体です

△CameraParentのX角度を小さくすると、カメラが低いアングルから映したり、CameraChildのローカルZ座標を0に近くするとカメラが寄ったりするのがわかると思います。

例2

Main Cameraのローカル座標は、注視点(キャラクター)の画面上の位置をズラすような目的で利用します。

要素
CameraParentの座標 (0, 0, 0)
CameraChildのローカルZ座標 -7
CameraParentの角度(X,Y) (5, 0)
Main Cameraのローカル座標(X,Y) (1, 1.75)

このように値を入れると、下記のようなビューになるはずです。

注視点であるキャラクターは必ずしも画面中央付近に表示することがベストではありません。
エイムをするようなシーンがある場合は、このようにキャラクターを脇にずらして、画面中央に照準を持ってくるのが一般的だと思います。

これでオブジェクトの入れ子関係を使って、複雑な計算無しに大まかなカメラ制御が出来るようになりました。

スクリプト

いちいち各オブジェクトの要素を触るのは面倒なので変数で動かせるようにしておきます。

CameraManager.cs
using UnityEngine;

public class CameraManager : MonoBehaviour {
    private Transform _cameraParent;
    private Transform _cameraChild;
    private Transform _camera;

    /// <summary>
    /// 注視点(CameraParentの座標)
    /// </summary>
    public Vector3 LookPosition;

    /// <summary>
    /// 注視点からの距離(CameraChildのローカルZ座標)
    /// </summary>
    public float Distance;

    /// <summary>
    /// 注視点への回り込み角度(CameraParentの角度)
    /// </summary>
    public Vector2 LookAngles;

    /// <summary>
    /// 視界オフセット座標(Main Cameraのローカル座標)
    /// </summary>
    public Vector2 OffsetPosition;

    void Start () {
        _cameraParent = transform;
        _cameraChild = _cameraParent.GetChild(0);
        _camera = _cameraChild.GetChild(0);
    }

    void Update () {
        _cameraParent.position = LookPosition;
        _cameraChild.localPosition = new Vector3(0, 0, -Distance); // 負数にする
        _cameraParent.eulerAngles = LookAngles;
        _camera.localPosition = OffsetPosition;
    }
}

Inspector上の表示はこんな感じになります。

実行中に各値を変更すると、カメラ距離や回り込み位置を変えることが出来ます。

注視対象を追う機能

このスクリプトだと注視座標を直接指定する形になっていますが、TPS視点のカメラの場合、基本的には特定のオブジェクトの移動を追いかけるはずです。

CameraManagerにTransformオブジェクトを登録して、注視対象を設定できるように改修しましょう。

CameraManager.cs
using UnityEngine;

public class CameraManager : MonoBehaviour {
    private Transform _cameraParent;
    private Transform _cameraChild;
    private Transform _camera;

    /// <summary>
    /// 注視対象
    /// 座標をCameraParentの座標に代入
    /// </summary>
    public Transform LookTarget;

    /// <summary>
    /// 注視点からの距離(CameraChildのローカルZ座標)
    /// </summary>
    public float Distance;

    /// <summary>
    /// 注視点への回り込み角度(CameraParentの角度)
    /// </summary>
    public Vector2 LookAngles;

    /// <summary>
    /// 視界オフセット座標(Main Cameraのローカル座標)
    /// </summary>
    public Vector2 OffsetPosition;

    void Start () {
        _cameraParent = transform;
        _cameraChild = _cameraParent.GetChild(0);
        _camera = _cameraChild.GetChild(0);
    }

    void Update () {
        _cameraParent.position = LookTarget.position;
        _cameraChild.localPosition = new Vector3(0, 0, -Distance); // 負数にする
        _cameraParent.eulerAngles = LookAngles;
        _camera.localPosition = OffsetPosition;
    }
}

こんな感じです。Inspector上で"LookTarget"に注視対象とするキャラクターのTransformを登録するのを忘れずに。

これで実行中に注視対象のオブジェクトの座標を動かしても、カメラがついてくるようになったはずです。

△こんな感じです

TIPS

LookTargetの座標を直接代入するとカメラがピッタリ付いてきすぎて硬い感じがするので、座標更新に補間をかけるとイイ感じになったりします。

CameraManager.cs
_cameraParent.position = Vector3.Lerp(_cameraParent.position, LookTarget.position, 3f * Time.deltaTime);

この実装はやや雑ですが、どんな感じになるか試してみると雰囲気の変化がわかると思います。

△こんな感じです

この考え方は注視点の更新のみではなく、他のパラメータについても同じことが言えます。適宜補間をかけるといいでしょう。

※ただし、エイム操作など、入力に対してきっちりカメラがついてくることが重要なゲームや操作モードに関しては補間をかけない方がベターだったりします。ケースバイケース。

おわりに

この仕組みを使って、

  • 見るもの
  • 見る距離
  • 見る角度
  • 画面上の表示位置(ズラし)

さえ決められれば、そうそうおかしなビューにはならないはずです。
冒頭の敵との対峙ビューは、注視点(_cameraParent.position)を2オブジェクトの中点~プレイヤーよりの位置にして、角度計算で2オブジェクトが視界に入るようにすれば実現できたりと、いろいろと応用が利きます。
モーションやエフェクトだけでなく、カメラでカッコよくゲームを演出しましょう!

長くなりましたが、ここまでご覧いただきありがとうございました。

もしよければ、いいねを押して頂けると嬉しいです!

敵との対峙のビューについてはこのアカウントの他記事で考え方をまとめていたりするので、興味がある方は是非!