Help us understand the problem. What is going on with this article?

【Unity】オブジェクトをカメラから見た前後左右に向かせる

More than 1 year has passed since last update.

前置き

先日、UnityでCharacterControllerを使う場合、オブジェクトを動かすためのMove()とSimpleMove()では基本的にはMove()を使おうという記事を書きました。

そちらのサンプルスクリプトではキャラクターのオブジェクトが前後左右に平行移動するだけでした。
折角だからちゃんと3Dゲームで使える感じの動きにしてみようと思って回転処理を実装したらまたしても沼にハマったため、最終的に出来上がったスクリプトと解説をまとめることにしました。

新年度からの目標は、戦いの記録を積極的にアップしていくことですね。

要件

  • 今回は私がこれまでの人生で一番長く遊んだ3DACTのスーパーマリオ64(DS)に従うことにします。
  • 十字キーが入力されると、カメラから見て前後左右の方向にキャラクターが回転します。
  • キャラクターの移動は別スクリプトのCharacterController.Move()で実装していますが、今回のスクリプトはtransform.rotationを弄っているだけなので、他にも応用は効くと思います。
  • 向いている方向を分かりやすくする為、ユニティちゃんには人柱になってもらいます。
  • 最終的には移動に合わせてカメラも追従させないとゲームになりませんが、本記事ではとりあえず回転するだけで。
  • 入力に合わせたアニメーションの設定もまた別の機会にしましょう。

スクリプト

PlayerRotateBaseCamera.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerRotateBaseCamera : MonoBehaviour{

    public float rotateSpeed = 10.0F;

    private Transform player;

    // Use this for initialization
    void Start () {
        //Playerタグのつけ忘れに注意!
        player = GameObject.FindGameObjectWithTag("Player").transform;
        //見つからない場合は自身を設定
        if(player == null){
            player = transform;
        }
    }

    // Update is called once per frame
    void Update () {
        float vertical = Input.GetAxis("Vertical");
        float horizontal = Input.GetAxis("Horizontal");
        //アナログスティックのグラつきを想定して±0.01以下をはじく
        if(Mathf.Abs(horizontal) + Mathf.Abs(vertical) > 0.1F){
            //カメラからみたプレイヤーの方向ベクトル
            Vector3 camToPlayer = player.position - Camera.main.transform.position;
            // π/2 - atan2(x,y) == atan2(y,x)
            float inputAngle = Mathf.Atan2(horizontal,vertical) * Mathf.Rad2Deg;
            float cameraAngle = Mathf.Atan2(camToPlayer.x,camToPlayer.z) * Mathf.Rad2Deg;
            Quaternion targetRotation = Quaternion.Euler(0, inputAngle + cameraAngle, 0);
            //deltaTimeを用いることで常に一定の速度になる
            player.rotation = Quaternion.Slerp(player.rotation, targetRotation, Time.deltaTime * rotateSpeed);
        }
    }
}

結果

こうなりました
rota.gif
© UTJ/UCL

瞬間的に方向が変わるのではなく、ぐるっと回転しているのがポイントです。
また、カメラを違う位置と角度に向けても正しく動く事は確認しています(流石に真上とか真下にあると誤動作します)。

Hierarchyビューはこんな感じになっています。
無題.png

PlayerRotateBaseCamera.csはPlayerオブジェクトのInspectorに入っています。CharacterControllerオブジェクトにまとめてもよいのでは? という気もしますが、移動と回転は分けておいた方が後々ややこしいことにならないかなぁと考えました。

ちなみにModelオブジェクトはユニティちゃんの親オブジェクトにしてあるだけで、Transform以外のコンポーネントは入っていません。モデルに対して何かスクリプト処理を入れたい場合はここに追加するつもりです。
というのも、原則として外部から持ってきたプレハブには手を加えない方がいいですからね。

スクリプト内で用いているPlayerタグは、同名のPlayerオブジェクトに設定しています。そのため、入力に合わせて回転するのはPlayerオブジェクトから下の階層のみで、CharacterControllerオブジェクトは一切回転しないことになります。
MainCameraをPlayerより下の階層に置いてしまうと大変賑やかなことになるので、興味があればやってみてください。失敗もまた経験です。

以下は細かい解説となります。スクリプトにもコメントが入っていますし、それで十分だぜって方はここでお別れとなります。


スクリプト解説

スペースが空いたのでもう一度貼りますね。

PlayerRotateBaseCamera.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerRotateBaseCamera : MonoBehaviour{

    public float rotateSpeed = 10.0F;

    private Transform player;

    // Use this for initialization
    void Start () {
        //Playerタグのつけ忘れに注意!
        player = GameObject.FindGameObjectWithTag("Player").transform;
        //見つからない場合は自身を設定
        if(player == null){
            player = transform;
        }
    }

    // Update is called once per frame
    void Update () {
        float vertical = Input.GetAxis("Vertical");
        float horizontal = Input.GetAxis("Horizontal");
        //アナログスティックのグラつきを想定して±0.01以下をはじく
        if(Mathf.Abs(horizontal) + Mathf.Abs(vertical) > 0.1F){
            //カメラからみたプレイヤーの方向ベクトル
            Vector3 camToPlayer = player.position - Camera.main.transform.position;
            // π/2 - atan2(x,y) == atan2(y,x)
            float inputAngle = Mathf.Atan2(horizontal,vertical) * Mathf.Rad2Deg;
            float cameraAngle = Mathf.Atan2(camToPlayer.x,camToPlayer.z) * Mathf.Rad2Deg;
            Quaternion targetRotation = Quaternion.Euler(0, inputAngle + cameraAngle, 0);
            //deltaTimeを用いることで常に一定の速度になる
            player.rotation = Quaternion.Slerp(player.rotation, targetRotation, Time.deltaTime * rotateSpeed);
        }
    }
}

privateなplayerについて、こちらはpublicにしてエディタから該当のオブジェクトを設定しても問題ありません。その場合はStart()での処理を消去しておくべきでしょう。
今回はオブジェクトのtransformしか使わないので、メンバ変数の型もTransformにしています。エディタから設定する場合は普通にオブジェクトをドラッグしてくることで、そのオブジェクト内のTransformコンポーネントが勝手に入ってくれます。

GetAxis()は十字キーやアナログスティックの入力値から-1~1の値を出力します。
Mathf.Abs()は絶対値を出力する関数なので、horizontalverticalの入力の絶対値を足しても0.1に満たない場合は処理が行われないことになります。
使いこまれたアナログスティックではニュートラルポジションにあっても若干の傾きが検出されることがあるので、ある程度の閾値を設けておくことで「操作してないのにジリジリ動く」事態を防ぐことができます。
※GetAxis()の内部でも同じ処理がされており、閾値もエディタから変更することができます(Edit>ProjectSetting>Input>Axes>Vertical/Horizontal>Dead)。しかしUnity以外の環境でも全て同様の親切設計とは限りませんし、float型を用いた条件分岐で「float != 0」を使うのは躊躇われることもあり、現在の仕様になっています。

if文内の冒頭で、プレイヤーとカメラの座標から、カメラの向いている方向ベクトルを算出しています。
後ほど、これのx成分とz成分を使うことになります。

inputAnglecameraAngleを定める計算はほぼ同じです。Mathf.Atan2()という関数を使って、必要な角度を算出します。
Atanはアークタンジェントの略です。これは逆三角関数といって、二次元の座標から原点までの角度を逆算してくれます。
逆三角関数は学術的には大学の分野ですが、ゲーム開発では非常によく使われています。まだ習っていない方はアークタンジェントだけでも覚えておくとよいでしょう。

そしてコメントにも書きましたが、この計算はAtan2()の引数に従うと
90-Mathf.Atan2(vertical,horizontal)*Mathf.Rad2Deg;
と書くべきなのですが、π/2-atan(x,y)atan(y,x)が等価であるため、短く書くことにしました。
こういった変換は高校数学でもありましたね。私は頭で考えるより覚えろと教えられたので、あまり楽しい思い出ではありませんでした。
なお、最後に掛けているMathf.Rad2Degはラジアンを度数に変換する定数です。ぶっちゃけると180/πです。

inputAngleではAtan2()の引数に入るのは縦横の入力値です。アナログスティックの場合、ちょうど真上から見てどの方向に入力されているのかの角度が分かります。
cameraAngleではカメラからプレイヤーを見た方向の、x成分とz成分です。Unityではzが奥行きなので、これも世界を真上から見た時の方向ベクトルとx軸のなす角を表します。

その後Quaternion.Euler()でVector3からQuaternionに変換します。
Unityのプログラミングを始めると多くの人が回転角の扱いで躓きますが、Vector3とQuaternionの変換が自在にできるようになるかがここの分水嶺だと思います。ちなみに反対の変換は[Quaternionの変数].eulerAnglesでいけます。

最後にQuaternion.Slerp()という関数を使っていますが、これは二つの回転角の中間を取得する関数です。これがゆっくり回転させる処理の正体です。
Slerpで現在の角度と目的の角度の中間を取得し、現在の角度に置き換えています。
中間の割合はTime.deltaTimeの倍数にしています。ここは0から1までの値であれば0.1とか0.5とか他の数でもいいのですが、deltaTimeにすることでフレームレートが変わった時や処理落ちが発生した時にも常に同じ速さで回転してくれます。

Slerp()はFloatクラスやVector3クラスにも実装されており、様々な場面で使うことができます。
手軽に動きのクオリティをアップさせられるありがたい関数なので、積極的に活用したいですね。

以上がスクリプトの解説となります。お付き合いいただきありがとうございました。

N_Hidano
場末のVTuberエンジニア。 趣味はXRのトレンドを追うことと、ミクさんを召喚することです。
https://twitter.com/N_Hidano
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした