🐡

[Unity][C# Script] ゲームらしく移動させよう

2021/02/19に公開約11,800字

はじめに

UnityでGameObjectを移動させたり、相手を向かせたりするには、transformを操作しますが、それにはいろいろな操作方法があります。場面に応じて最適な方法を選べるよう、まとめてみました。地面との接地判定を含めているので、わりと汎用性があると思います。
 あとめっちゃ簡単に3Dのオブジェクトをぽよんぽよんさせます。

1.transformコンポーネントについて

GameObjectには必ずtransformコンポーネントがついています。そして下の3つのパラメータを持っています。

・Position Vector3型 GameObjectの座標
・Rotation Quaternion型 GameObjectの向き
・Scale Vector3型 GameObjectの相対的な大きさ(標準 1,1,1)

GameObjectを移動させるには、Positionを変更すればいいことになります。ただし、その際、地形や他のGameObjectなどの状況に応じてふるまうとなるとそう単純にはいかないので、それらについてひとつずつ調べていきます。
 なお、子オブジェクトを操作したい場合は「2.子オブジェクトの操作」にいくつか注意がありますので、そちらも確認ください。

1) Position の操作
 インスペクターから値を入力するか、スクリプトから以下の方法で操作できます。
 Position はワールド座標となります。単位はメートル(m)と考えてよさそうです。

(1) transform.position に Vectore3型を直接代入する
 初期設定などに使います。

float x=0f;
float y=3f;
float z=-1f;

transform.position = new Vector3(x,y,z);

なお、transform.position.xなどは参照のみで、値を直接代入することはできません。

Unityでは左手座標系を採用しています。
ここでは、座標は以下のように表現されます。
・ (-)左 右(+) = x
・ (+)上 下(-) = y
・ (+)奥手前(-) = z

(2) 地面にあわせてGameObjectの高さを調整
 Raycastにて地面の高さを検出し、そこに位置をあわせます。

Ray ray; // Ray
ray.origin = transform.position;
ray.direction = new Vector3(0f,1f,0f); // キャラクタの向きと無関係の場合

RaycastHit hit; // hitしたオブジェクトを収納する、RaycastHit型の変数を用意する。

// Ray判定
Physics.Raycast(ray, out hit, Mathf.Infinity);

if (hit.collider != null)
{
  // y軸をhit座標にあわせ補正
  transform.position = new Vector3(transform.position.x, hit.point.y , transform.position.z)
}

(3) 地面にGameObjectをフィットさせる
 Raycastにて地面の高さにあわせて位置を変更しますが、傾斜にあわせて回転軸もフィットさせてみます。

Ray ray; // Ray
ray.origin = transform.position;
ray.direction = -transform.up; // キャラクタの下にむける

RaycastHit hit; // hitしたオブジェクトを収納する、RaycastHit型の変数を用意する。

// Ray判定
Physics.Raycast(ray, out hit, Mathf.Infinity);

if (hit.collider != null)
{
  // y軸をhit座標にあわせ補正
  transform.position = new Vector3(transform.position.x, hit.point.y , transform.position.z)
  // Y軸のみを取得
  var defaultRotation = Quaternion.Euler( 0f, referenceTarget.transform.eulerAngles.y, 0f);
  // 接地の傾き回転を取得
  var groundRotation = Quaternion.FromToRotation(Vector3.up, hit.normal); // 常に真上を基準
  rotateTarget.transform.rotation = groundRotation * defaultRotation;
}

(4) ゲームオブジェクトが向いている方向にそって座標を補正する

向いている方向に対して上下左右に行う座標操作は、定義を使うとよいようです。
射出する弾丸の出現位置(初期位置)の補正などに使います。

transform.position = transform.position + transform.forward * 3.0f; // 前に3.0単位移動
transform.position = transform.position + transform.right; // 右に1.0単位移動
transform.position = transform.position + transform.up; // 上に1.0単位移動
// back, left, downは以下のようにすることができます。
transform.position = transform.position - transform.forward; // 後ろに1.0単位移
transform.position = transform.position - transform.right; // 左に1.0単位移動
transform.position = transform.position - transform.up; // 下に1.0単位移動

(5) transform.Translateをつかう
 transform.positionに値を加算します。
 弾や自動的に動く地形などに使うのがよさそうです。よくカクカクするのでキャラクタには CharactorController コンポーネントか Rigidbody をつかい、この操作は行わないことにしています。(マイルール)

transform.Translate(1f * Time.deltaTime,0f,0f) // x方向に1単位/秒の速度で移動
// movementSpeed は別途定義

//コントローラ入力からの入力 横軸 を取得
float horizontalInput = Input.GetAxis("Horizontal");

//コントローラ入力からの入力 縦軸 を取得
float verticalInput = Input.GetAxis("Vertical");

//コントローラ入力があった場合のみ
if (horizontalInput != 0f || verticalInput != 0f)
{
  // GameObject の方向を移動方向に向ける。
  transform.rotation = Quaternion.LookRotation(new Vector3(horizontalInput, 0, verticalInput));

  // GameObject の向いている方向に進む。
  transform.Translate(transform.forward * movementSpeed * Time.deltaTime);
}

(6) CharactorControllerコンポーネントを使う
 CharactorControllerコンポーネントの Moveを使ってみます(推奨)
このコンポーネントは衝突判定・処置と移動が組み込まれていて、簡易に使うことができます。
 ・地形などとの衝突回避処理
 ・接地処理(スロープ移動含む)
 ・ジャンプ処理をしない場合、地形の凹凸に追従する。
 ・(NG)移動床の子になれない
 ・(NG)接地判定フラグisGroundedメソッドはいまいちあてにできない。
  よって、接地判定はRayを使い、判定することになります。
 なおCharactorCotrollerコンポーネントの各種設定は今回は取り扱いません。

// float stoppingPower、addSpeed と Vector3 moveDirection は別途定義

//コントローラ入力からの入力 横軸 を取得
float horizontalInput = Input.GetAxis("Horizontal");

//コントローラ入力からの入力 縦軸 を取得
float verticalInput = Input.GetAxis("Vertical");

// 入力をカメラ補正して Vector3型に代入
Vector3 inputDirection = cameraForward * inputVertical + Camera.main.transform.right * inputHorizontal;

// 加速・減速操作
moveDirection = moveDirection + (inputDirection * addSpeed * Time.deltaTime);
moveDirection = moveDirection * stoppingPower; //  ここは改良の余地あり

// controller はCharactorControllerコンポーネントとする(別途定義)
controller.Move(moveDirection * Time.deltaTime);

(7) RigidBody コンポーネントを使う
 rigidbody の AddForce ( ForceMode.VelocityChange )を使ってみます。
 相手との物理挙動をそれらしくしたり、移動床などとそれらしく整合性をとるには rigidbody のほうが都合がよい場合があります。
 rigidbody を使うには、rigidbodyコンポーネントの設定をしたり、Cupsle Collider を組み込んだりする必要がありますが、今回は取り扱いません。

// float stoppingPower、addSpeed と Vector3 moveDirection は別途定義

//コントローラ入力からの入力 横軸 を取得
float horizontalInput = Input.GetAxis("Horizontal");

//コントローラ入力からの入力 縦軸 を取得
float verticalInput = Input.GetAxis("Vertical");

// 入力をカメラ補正して Vector3型に代入
Vector3 inputDirection = cameraForward * inputVertical + Camera.main.transform.right * inputHorizontal;

// 加速・減速操作
moveDirection = moveDirection + (inputDirection * addSpeed * Time.deltaTime);
moveDirection = moveDirection * stoppingPower; //  ここは改良の余地あり

// rbController は Rigitbody コンポーネントとする
rbControlle.Move(moveDirection * Time.deltaTime,  ForceMode.VelocityChange);

なお、プレイヤーキャラクタの制御についてはいったんnoteなどにまとめました。
https://note.com/k1togami/n/n75b6c1659654
https://zenn.dev/k1togami/articles/eea2cd01d4199c

(8) 敵キャラなどをNavMeshで動かす
敵キャラやNPCなどは、NacMeshで動かすことが多いでしょう。
移動先をランダムに指定したり、WayPointとよばれる巡回点を設定して移動させたりすることが多いと思います。

NavMeshに関しては以下のzennにまとめてあります。
https://zenn.dev/k1togami/articles/71519622146168

2) Rotation の操作
 インスペクターから値を入力するか、スクリプトから以下の方法で操作します。ちなみに、Vector3型は角度の場合と座標の場合がありますが、ここでは角度です。
 また Rotation は、親がいる場合は親に対しての回転、そうでない場合はワールド座標に対しての回転となります。
 より詳細な回転の扱いについては以下を参照ください。

https://zenn.dev/k1togami/articles/18f04da51d4d05

Unity の rotationは内部でクォータ二オンを採用していますが、インスペクターでは編集しやすいように同等のオイラー角で表しています。

(1) transform.rotationに Vector3型(角度)を代入する。
 transform.rotationはクォータニオン型なので、Vector型を直接代入はできません。Quaternion.EulerでVector3型にするか、かわりにtransform.eulerAnglesに代入します。

// a)
transform.rotation = Quaternion.Euler( 0f, 0f, 10f); // Z軸を10°にする

// b)
transform.eulerAngles = new Vector3( 0f, 0f, 10f); // 道場

(2) ターゲットの位置(Vector3型(座標))を向かせる。
 ターゲットの方向を向かせるだけならば、LookAtが便利です。

transform.LookAt ( target.transform.position ); // target は GameObject型

(3) Rotate を使って度数(0~360°)を指定して回転する。
 Rotateを使って、何度回転させるか指定します。 
 サンプルは、1秒ごとに120度回転します。

transform.Rotate( 0f, 120.0f * Time.deltaTime ,0f );

(4) ゆっくりとターゲットの位置(Vector3型(座標))を向かせる。
 (2)のバリエーションです。なめらかに回転するので、ホーミングミサイルなどゲームキャラクタの旋回に使うと便利です。

transform.rotation = Quaternion.RotateTowards ( transform.rotation, 
  Quaternion.LookRotation( target.transform.position - transform.position ),
  120.0f * Time.deltaTime );

(少し解説)
・Vector3 direction = target.transform.position - transform.position;
 Vector3型(方向)を算出
・Quaternion rotation = Quaternion.LookRotation( direction );
 Vector3型(方向)を Quaternion型に変換。
・Quaternion. RotateTowards (Quaternion from, Quaternion to, float maxDegreesDelta);
 from から to への回転を得る。
 maxDegreesDeltaの角度ステップ分だけtoに向かって回転する。
 ここでは1秒間に120度の速度で回転。

(5) ゆっくりと進んでいる方向を向かせる。
 ゲームでは、キャラクターが急な方向転換をするものがよくありますが、あまり急に回転すると不自然な場合があります。なめらかに回転するので、プレイヤーキャラクタの旋回に使うと便利です。

// lookingDirection は向かせる方向。通常は移動方向。
transform.rotation = Quaternion.RotateTowards(transform.rotation,
  Quaternion.LookRotation(lookingDirection),
  120.0f * Time.deltaTime);

(少し解説)
public static Quaternion LookRotation (Vector3 forward, Vector3 upwards= Vector3.up);
・forward 向かせたい方向
・upwards 上方向を定義するベクトル。 Vector3.up は Vector3(0, 1, 0) と同意。

3) Scaleの操作
 Scaleはインスペクターから値を入力するか、スクリプトから操作します。
scaleだけは親の場合でも、localscaleを使うようですので注意が必要です。
スケールを操作することはあまりなさそうに思えますが、複雑なアニメーションを仕込まなくても、ぷるぷるさせることができるので、ゲームの演出に向いているのではないでしょうか。積極的に使っていきたいと思います。

// StartCoroutine("PuruDo"); で呼びます

// 2秒間だけオブジェクトを ぷるぷる させるコルーチン
IEnumerator PuruDo()
   {
       // 2秒間
       float time = 0f;

       while (time < 2f)
       {
           // ぷるぷるにSinを使います。
           float puruW = (2f - time) * 0.2f * (Mathf.Sin(time * 20f)) + 1f;
           float puruH = (2f - time) * -0.12f * (Mathf.Sin(time * 20f)) + 1f;

           transform.localScale = new Vector3( puruW , puruH , puruW );
           time += Time.deltaTime;
           yield return null;
       }
   }

2.子オブジェクトの操作

子オブジェクト以外から子オブジェクトを操作する場合は、いくつか注意が必要なようですです。
 (1) 子オブジェクトのposition、rotationはワールド座標に変換されるようです。
  ・インスペクタにあるposition、rotationとは差異が出ます。
・localPosition、localEurarAngleは差異がありません。
(2) スクリプトでtransform.positionなどを他オブジェクトから操作した場合、親の変更に追従しなくなる場合があります。
 localPosition、LocalRotation、LocalEurarAnglesを使った場合は大丈夫でした。
また、子オブジェクト本体からtransform.positionを操作した場合、親の変更に追従するようになるようです。

**1) position(ローカル座標)の操作

transform.localPosition に Vectore3型を直接代入する

transform.localPosition = new Vector3();

**2) rotation(ローカル座標)の操作

transform.localRotationはクォータニオン型なので、Vector型を直接代入はできません。Quaternion.EulerでVector3型にするか、かわりにtransform.localEulerAnglesに代入します。

// a)
transform.localRotation = Quaternion.Euler( 0f, 0f, 10f); // Z軸を10°にする

// b)
transform.localEulerAngles = new Vector3( 0f, 0f, 10f); // 道場

localRotate()などの関数は、使用できないようです。

3.3Dベクトルの操作

以下にtransformを扱うのときに必要そうな、基本的な移動と回転をまとめます。

1) 単純な移動(元座標 → 指定座標)
特定の座標に等速で近づけます。

//maxDistanceDeltaぶんフレーム毎に移動します。
transform.position = Vector3.MoveTowards (Vector3 transform.position, 
                                          Vector3 targetPosition,
                                          float maxDistanceDelta);

2) 標的の方向を向く(現在向き → 向ける方向)
 標的の方向に向けて、等速で回転させるときに使います。
 車両のドリフト状態などは、進む方向と GameObject の向きが異なりますが、これを表現するためには内部的に進行方向を持つ必要があります。そのときに使うとよさそうでしょうか。

// target は標的となる GameObjectです。
// currentLookAt は Vector3 
currentLookAt = Vector3.RotateTowards(currentLookAt,
                                      target.position,
                                      lookAtSmoothing * Time.deltaTime);
// その方向を向かせるときは、以下の行のコメント記号を削除
// transform.LookAt(currentLookAt); 

3) 標的までの角度差を得る(2本の向き → 角度)
 首だけを標的を向かせたりする場合、回転できる角度を制限しないと首だけ真後ろを向いたりしますよね。そんなことを防ぐためには、自分が向いている角度と、相手の方向の角度の差を得る必要があります。とりあえずそれらしい角度を得るには、Vector3.Angleが簡単で便利です。

// ベクトルの差分で出力されるわけでないので、
// 返した値を利用して物体を回転させる事は出来ない
Vector3 targetDir= target.transform.position - transform.position;
float angle = Vector3.Angle(targetDir, transform.forward);

if (angle < 5.0f)
           print("close(近い)");

おそらくこの角度は、targetDirまでの直線と自身の向きという直線の2本で定まる平面上での角度になると思われます。
双方のxz平面が同一であれば、得られる回転はy軸にそったものになるはずです。

4) 標的までの角度差(Y軸に沿った角度差)を得る
 Unityでは角度差をあまり意識せずにプログラムが組めるよう配慮されているのですが、やっぱりちょっと泥臭いことをしなければならないときもありそうです。
 もっといい方法があったら知りたいものです。

// a)
float angleY = Mathf.Atan2( target.transform.position.x
                            - transform.position.x
                            , target.transform.position.z
                            - transform.position.z
                            ) * Mathf.Rad2Deg;

// b)
Vector3 targetDir= target.transform.position - transform.position;
targetDir.y = 0f;
float angleY = Vector3.Angle(targetDir, transform.forward);

>-> Radian の変換は deg * Mathf.Deg2Rad (意味は(PI * 2) / 360と同一)
>Radian → 度 の変換は rad * Mathf.Rad2Deg

5) 最大速度を制限する
 移動の速度を増減させるときのうち、特に加速制御するとき、一定の速度内にクリッピングしたいときがあります。そのときは一旦正規化するとよいでしょう。

// float maxSpeed、vector3 moveDirection は別途定義 

if ( moveDirection.magnitude >= maxSpeed )
{
     moveDirection = moveDirection.normalized * maxSpeed;
}   

ex.関係ありそうな記事

https://zenn.dev/k1togami/articles/18f04da51d4d05
https://zenn.dev/k1togami/articles/eea2cd01d4199c
https://note.com/k1togami/n/n42389783cc98
https://note.com/k1togami/n/nadc12cef7700

2021年2月19日 noteより転記
2022年10月16日 子オブジェクトの操作について追記

Discussion

ログインするとコメントできます