ゴマちゃんフロンティア

アザラシが大好きなエンジニアの開発日記です

【Unity】3Dのカメラ視点移動とロックオン機能について

time 2017/09/28

というわけで、グラボのDisplayPort端子にHDMIケーブルを刺そうと頑張ってしまった今日この頃です。パッと見でHDMIと似ていたので「このグラボ2つHDMIあるじゃん!」とか悟ったのが軽率でした。デュアルディスプレイをすべくHDMI対応のモニタを買ったのにこの有様です。
ひとまず某密林で変換コネクタを注文し試しているところです。

…と、どうでもいい前置きを挟んだところで、本日のお題は「3Dカメラ視点移動」です。

実は今までのUnity歴で「3D空間を移動・探索する」というゲームをほとんど構築したことがなく、3Dにおけるカメラ移動やロックオンの実装方法はあやふやだったりします。
本ブログを閲覧いただいている方には常識レベルかもしれませんが、必要となったパッと書けるようにしたいので、気まぐれと勢いで作ってみました!

カメラ移動用オブジェクトの作成

3D空間でのカメラ移動と言えば、あるオブジェクトに対して回り込むようにカメラを移動させる必要があるため、メインカメラのGameObjectをそのまま動かすのは面倒です。
ということで、1つ空オブジェクトを作り、その子としてメインカメラを設定します。カメラの位置はゲームや場面に合わせて調整しましょう。

あとは親となる移動用オブジェクトをくるくる回せば、子となっているメインカメラも相対的に移動します。

当然ながら、作成した空オブジェクトは「視点移動のベースとなるオブジェクト」の位置に移動させる必要があります。大抵はプレイヤーキャラクターが基点になるので、今回もそれに合わせてみます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraController : MonoBehaviour {

    public GameObject player;
    public GameObject mainCamera;
    public float rotate_speed;

    void Start() {
        mainCamera = Camera.main.gameObject;
        player = GameObject.FindGameObjectWithTag("Player");
    }

    void Update() {
        transform.position = player.transform.position;
    }
}

ここまでは何も難しいことはないですね。

カメラの視点移動

先程のカメラ移動用オブジェクトに対して回転を掛けます。
「難しいこと考えずにカメラ移動用オブジェクトを回すだけでしょ?」とTransform.Rotateを使いましたが、見事に軸がブレで上手くいきませんでした。これだからクォータニオンは苦手だとあれほど…!
詰まってしまったのでGoogle先生に聞いたところ、Transform.RotateAround()なんてものがあるみたいですよ!

private void rotateCmaeraAngle() {
    Vector3 angle = new Vector3(
        Input.GetAxis("Mouse X") * rotate_speed,
        Input.GetAxis("Mouse Y") * rotate_speed,
        0
    );

    transform.RotateAround(player.transform.position, Vector3.up, angle.x);
    transform.RotateAround(player.transform.position, transform.right, angle.y);
}

うわ、便利!
最近できた関数かと思いきや、かなり前からあるようです。やはり無知は罪であると思う。

【2017/10/07 追記】
本記事の内容にTransform.RotateAround()は不適切だったので、transform.eulerAnglesに加減算する形に修正しました。
以下のような感じです。

private void rotateCmaeraAngle() {
    Vector3 angle = new Vector3(
        Input.GetAxis("Mouse X") * rotate_speed,
        Input.GetAxis("Mouse Y") * rotate_speed,
        0
    );

    transform.eulerAngles += new Vector3(angle.y, angle.x);
}

あとはこれを呼び出す記述をUpdate()内に追加すれば、最低限のものはできます。

視点移動の制限

このままの場合、左右は問題ありませんが、上下に移動させた際に視点が1回転してしまいます。3Dゲームの多くは上下に対する角度制限がある印象なので、それに習ってみます。
ここで面倒になるのは下方向に向けた場合で、X軸の角度的には0→360度になるため、Transform.eulerAnglesの値そのままで制限するのは厳しいです。なのでX軸だけちょっと細工を入れます。

private const int ROTATE_BUTTON = 1;
private const float ANGLE_LIMIT_UP = 60f;
private const float ANGLE_LIMIT_DOWN = -60f;

void Update() {
    transform.position = player.transform.position;

    if (Input.GetMouseButton(ROTATE_BUTTON)) {
        rotateCmaeraAngle();
    }

    float angle_x = 180f <= transform.eulerAngles.x ? transform.eulerAngles.x - 360 : transform.eulerAngles.x;
    transform.eulerAngles = new Vector3(
        Mathf.Clamp(angle_x, ANGLE_LIMIT_DOWN, ANGLE_LIMIT_UP),
        transform.eulerAngles.y,
        transform.eulerAngles.z
    );
}

三項演算子で「Transform.eulerAngles.xが180度以上であれば360度減算する」処理を入れます。超ダイレクトな方法ですが、これでX軸の角度が-180~180度の間になり、Mathf.Clamp()での制限が容易になります。

ついでにカメラ視点移動を「マウス右クリック中」のみにしてみました。定数ROTATE_BUTTONで1を指定しておき、Update()内のrotateCmaeraAngle()呼び出し前にifで判定します。

ロックオン機能

今日の3Dゲームでは必ずと言っていいほど備わっている「ロックオン」機能です。
深く考えると「ロックオン可能な距離」や「複数のロックオン対象がいる場合の選別」などあれこれあるので、シンプルに「特定の判定に入った敵をロックオン」だけ考えてみます。

下準備として、ロックオン対象を検知するためのオブジェクトを作成し、プレイヤーキャラクターの子オブジェクトとして設定します。

検知用オブジェクトにはShpereColliderを設定し、IsTriggerにチェックを入れておきます。
合わせて以下のスクリプトを作成し、こちらも設定します。

using UnityEngine;

public class LockOnTargetDetector : MonoBehaviour {

    [SerializeField]
    private GameObject target;

    protected void OnTriggerEnter(Collider c) {
        if (c.gameObject.tag == "Enemy") {
            target = c.gameObject;
        }
    }

    protected void OnTriggerExit(Collider c) {
        if (c.gameObject.tag == "Enemy") {
            target = null;
        }
    }

    public GameObject getTarget() {
        return this.target;
    }
}

SphereColliderに入った敵を取っておくだけのスクリプトです。1オブジェクトしか保持しないため、敵が単体ならまだしも、複数入ってくると使い物になりません。
上にも書きましたが深く考えるとややこしいので、こんなものでいいでしょう。

そしてCameraControllerクラスを修正します。

void Start() {
    mainCamera = Camera.main.gameObject;
    player = GameObject.FindGameObjectWithTag("Player");
    lockOnTargetDetector = player.GetComponentInChildren<LockOnTargetDetector>();
}

void Update() {
    transform.position = player.transform.position;

    if (Input.GetKeyDown(KeyCode.R)) {
        GameObject target = lockOnTargetDetector.getTarget();

        if (target != null) {
            lockOnTarget = target;
        } else {
            lockOnTarget = null;
        }
    }

    if (lockOnTarget) {
        lockOnTargetObject(lockOnTarget);
    } else {
        if (Input.GetMouseButton(ROTATE_BUTTON)) {
            rotateCmaeraAngle();
        }
    }

    float angle_x = 180f <= transform.eulerAngles.x ? transform.eulerAngles.x - 360 : transform.eulerAngles.x;
    transform.eulerAngles = new Vector3(
        Mathf.Clamp(angle_x, ANGLE_LIMIT_DOWN, ANGLE_LIMIT_UP),
        transform.eulerAngles.y,
        transform.eulerAngles.z
    );
}

private void lockOnTargetObject(GameObject target) {
    transform.LookAt(target.transform, Vector3.up);
}

先述の視点移動制限があるため、至近距離でジャンプしてもそこそこ見やすい角度に収まります。

視点のリセット

せっかくなので視点をリセットする機能も付けます。深いことは考えずにプレイヤーキャラクターのtransform.rotation.yに合わせて移動用オブジェクトに回転をかけましょう。
リセット用のキーはロックオンと同じ「R」でいきます。

if (Input.GetKeyDown(KeyCode.R)) {
    GameObject target = lockOnTargetDetector.getTarget();

    if (target != null) {
        lockOnTarget = target;
    } else {
        iTween.RotateTo(gameObject, iTween.Hash(
            "rotation", player.transform.eulerAngles,
            "time", 0.5f
        ));

        lockOnTarget = null;
    }
}

単に値を設定するだけでは瞬時に回転して味気ないので、iTween.RotateTo()を使用して徐々に変化するようにします。ハッシュのrotationにはtransform.eulerAnglesを指定しましょう。

ちなみにtransform.forwardiTween.ValueTo()DOTween.To()等でプレイヤーの値に合わせることでも実現できますが、現在の向きと真反対を向いた際にイージングが上手くいかないので見送りました。

あとがき

そんなわけで、今更3Dカメラの動かし方についておさらいしてみました!
あっさりできそうで意外と苦戦する部分が多かったです。今後が不安になるため、Unityユーザー間では常識っぽい処理も定期的に取り上げて書いていこうかと思います。

現在開発中(?)のゲームは3Dっぽい2Dですが、今になって「3Dもいいなー」なんて思い始めたりしています。
CharacterMotorを強引に2D向けに使用している上、そのCharacterMotor自体が古いスクリプトというのもあり、このまま開発するのは無理があるかもしれません。一応2Dっぽい動きにはなっている上、場面によって3D移動ができるようにもしたので、限界が来るまで粘りつつ、次の手を模索してみます。

コメント

  • こんにちは、通りすがりで失礼します。
    Transform.Rotateで軸がブレたとのことですが、y軸回転させると斜めになっちゃう感じですかね?
    だとすると、Rotateメソッドの引数でSpace.Worldを指定すると、ワールド空間での回転になるので、たぶん期待する回転になると思います。デフォルトだとローカル空間です。
    Transform.RotateAround()は指定した点を中心に回転させる機能なので、用途が違うかなと(太陽の周りを回る地球のような動きをさせたいときに使う)

    motoyama  2017年9月30日 9:29 PM

    • motoyamaさん
      コメントありがとうございます。

      > Transform.Rotateで軸がブレたとのことですが、y軸回転させると斜めになっちゃう感じですかね?
      X軸とY軸を同時に回転させた際にZ軸まで回転し、傾いた視点になってしまいます。

      > だとすると、Rotateメソッドの引数でSpace.Worldを指定すると、ワールド空間での回転になるので、たぶん期待する回転になると思います。
      指定が可能なことは知っていたのでWorldでの指定も試みましたが、改善しませんでした。

      > Transform.RotateAround()は指定した点を中心に回転させる機能なので、用途が違うかなと
      仰る通り、本投稿のやり方にRotateAround()は不適当かと思います。
      どうしてもRotate()での実現ができなかったため、違和感を抱きつつも使用しております。

      素直にtransform.eulerAnglesに回転量を加算するのが確実な気がしてきたので、その方向で考え直してみます。

      riberunn  2017年10月1日 10:22 AM

  • 初めまして、いつも参考にさせて頂いてます!
    視点移動の制限についてのことなのですが、
    無理やり上下にマウスを動かすと画面が反転してしまいます!

    chite  2019年12月25日 1:19 AM

    • chiteさん

      ご指摘ありがとうございます。確かに稀に逆さまになってしまいますね…。
      根本解決にはなりませんが、処理を`FixedUpdate()`内で行うように変えると少しはマシになるかもしれません。
      手元で軽く試した感じでは簡単に解決できそうにないので、対策を思いついたら追記します。

      riberunn  2019年12月25日 11:30 PM

    • 記事のスクリプトでは「回転実行後に制限に合わせて角度を補正する」形になっていますが、これを「回転実行前に回転角度を制限内に収まる値に補正する」形に直すとどうでしょうか?
      具体的には`rotateCmaeraAngle()`を以下のように修正します。

      private void rotateCmaeraAngle()
      {
          Vector3 angle = new Vector3(
              Input.GetAxis("Mouse X") * rotate_speed,
              Input.GetAxis("Mouse Y") * rotate_speed,
              0
          );
          var rotateEulerAngles = transform.eulerAngles + new Vector3(angle.y, angle.x);
          float angle_x = 180f <= rotateEulerAngles.x ? rotateEulerAngles.x - 360 : rotateEulerAngles.x;
          rotateEulerAngles = new Vector3(
              Mathf.Clamp(angle_x, ANGLE_LIMIT_DOWN, ANGLE_LIMIT_UP),
              rotateEulerAngles.y,
              rotateEulerAngles.z
          );
          transform.eulerAngles = rotateEulerAngles;
      }
      

      riberunn  2019年12月26日 12:10 AM

  • ありがとうございます!

    Chite  2020年3月27日 10:31 PM

  • はじめまして、いつも参考にさせていただいております。
    このプログラムの全文を教えていだだけませんか?

    guatema  2021年6月11日 5:56 PM

    • 当時のデータが残っていないので掲載できません。
      残っていたとしても他の関係ない処理が多数含まれていたと思うので、お見せできるコードではないです。

      riberunn  2021年6月12日 1:41 AM

down

コメントする