🐡

[Unity][C# Script] ゲームにtransfotmを使いこなそう

2021/02/19に公開

はじめに

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

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

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

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

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

1) 移動のあれこれ(Positionの操作)

GameObjectの位置は、インスペクターから値を入力するなどエディタで操作するか、スクリプトから以下の方法で設定します。
 Position は基本的にワールド座標となります。単位はメートル(m)と考えてよさそうですが、使用するアセットなどにもよります。

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

// a)
float x = 0f;
float y = 3f;
float z = -1f;
transform.position = new Vector3(x,y,z);

// b)
Vector3 position;
position.x = 0f;
position.y = 3f;
position.z = -1f;
transform.position = position;

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

(2) transformの座標の参照
 transform.positionはVector3型なので、x,y,zそれぞれの座標を確認できます。なお、参照のみで代入はできません。

// 各座標を取得
Vector3 myPosition = transform.position;
float x = myPosition.x;
float y = myPosition.y;
float z = myPosition.z;

// 各座標を直接取得
float x = transform.position.x;
float y = transform.position.y;
float z = transform.position.z;

(3) GameObjectからみた前方、上下・左右
GameObjectが向いている方向(Rotation)からみた前方は、以下で取得できます。

// GameObjectの前方(進行方向)をVector3で取得
vector3 forward = transform.forward;

前・上・右の取得はそのまま取得できますが、後・下・左は負の記号を使います。

// GameObjectの前方・上方、右方向をVector3で取得
vector3 forward = transform.forward;
vector3 up = transform.up;
vector3 right = transform.right;

// GameObjectの後方・下方、左方向をVector3で取得
vector3 back = -transform.forward;
vector3 down = -transform.up;
vector3 left = -transform.right;

(4) transform.positionにVector3を加減算
transform.positionはVector3型なので、移動するときなどはVector3を加減算することができます。射出する弾丸の出現位置(初期位置)の補正などにも使います。

// x方向に1.0単位/秒の速度で移動
transform.position += new Vector3(1.0f * Time.deltaTime,0f,0f)

// GameObjectの向きに3.0単位/秒の速度で移動
transform.position += transform.forward * 3.0f *Time.deltaTime;

// 弾丸の射出位置の補正など
transform.position += transform.right * 0.3f; // 右に0.3単位アジャスト
transform.position += transform.up * 0.1f; // 上に0.1単位アジャスト

// back, left, downは以下のように負の演算子にて表現することもできます。
transform.position -= transform.forward; // 後ろに1.0単位移
transform.position -= transform.right; // 左に1.0単位移動
transform.position -= transform.up; // 下に1.0単位移動

(5) transform.Translateで加算・減算
 transform.positionにxyz値かVector3値を加算します。transform.positionへの直接操作よりはシンプルに表現できるかもしれません。
弾や自動的に動く地形などに使うのがよさそうです。
よくカクカクするのでキャラクタには CharactorController コンポーネントか Rigidbody をつかい、この操作は行わないことにしています。(マイルール)

// x方向に1.0単位/秒の速度で移動
transform.Translate(1.0f * Time.deltaTime,0f,0f)

// GameObjectの向きに3.0単位/秒の速度で移動
transform.Translate(transform.forward * 3.0f *Time.deltaTime)

// ローカル座標でX方向に10.0単位/秒の速度で移動
transform.Translate(10.0f *Time.deltaTime, 0f, 0f);

// ワールド座標でX方向に10.0単位/秒の速度で移動
transform.Translate(10.0f *Time.deltaTime, 0f, 0f, Space.World);

(6) GameObject間の距離を得る
 targetのGameObjectとの距離は以下で取得します。

// a) xとzのそれぞれの距離
float distanceX = target.transform.x - transform.position.x;
float distanceZ = target.transform.z - transform.position.z;

// b) xとzのそれぞれの距離
Vector3 distanceVector = target.transform - transform;
float distanceX = distanceVector.x;
float distanceZ = distanceVector.z;

// c) 直線距離
Vector3 distanceVector = target.transform.position - transform.position;
float distanceLength = distanceVector.magnitude;
// 直線距離の2乗(2乗のまま扱えるなら、速度的に優位であると考える)
float distanceLength = distanceVector.sqrtMagnitude;

// d) 直線距離のうち、高さ(Y)を無視する
Vector3 distanceVector = target.transform - transform;
distanceVector = new Vector3(distanceVector.x, 0f, distanceVector.z);
float distanceLength = distanceVector.magnitude;

2) Ray の操作

unityでの物体の判定はコライダーかRayを使うことが多いようです。ここではtransformの障害物や地面との接地など、キャラクターの座標操作と組わせることが多いRayの使い方をいくつかまとめてみました。

(1) GameObjectの前方の障害物を検出
RayをGameObjectの前方に飛ばし、障害物を検出します。

Ray ray; // Ray
ray.origin = transform.position;
ray.direction = Vector3.forward; // 前向きを検出

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

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

if (hit.collider != null)
{
  // レイがあたった障害物の情報
  GameObject = hit.collider.gameObject; // 命中したGameObject
  Vector3 hitLocation = hit.position; // レイがコライダーにヒットした位置
  Vector3 distance = hit.distance; // レイの原点から衝突点までの距離
  Vector3 nomal = hit.normal; // レイがヒットしたサーフェスの法線
}

(2) 対象のGameObjectとの間の障害物を検出
Rayを対象GameObjectとの間に飛ばし、壁などの障害物で遮られているかを検出します。
これはLinecastを使うと簡単に表記できます。

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

// Ray判定
Physics.Linecast(transform.position, target.transform.position, out hit)
if (hit.collider != null)
{
  // レイがあたった障害物の情報
  GameObject = hit.collider.gameObject; // 命中したGameObject
}

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

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

// GameObject原点から下に向けてRayを飛ばす
Ray ray = new Ray(transform.position, -transform.up);

// 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)
}

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

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

// GameObject原点から下に向けてRayを飛ばす
Ray ray = new Ray(transform.position, -transform.up);

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

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

3) Unityの機能によるGameObjectの移動

GameObjectを移動する際、地形や他のGameObjectなどの状況に応じてふるまうためにいくつかの基本的なムーブメントシステムが用意されています。

(1) 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);

(2) 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

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

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

4) Rotation の操作

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) と同意。

(6) ターゲットのGameObjectが視界にはいっているかの確認
 ゲームでは、敵キャラがプレイヤーを発見する、といったチェックをしたいことがよくあると思います。そんなときは、対象キャラの向きと、自分が向いている角度との差をチェックします。

// 視界の広さを別途定義
float angle = 15.0f; // 倍の前方30度が視界となる。

// 視界に入っているかのチェック
// 対象のGameObjectとの位置関係
Vector3 targetVector = target.transform.position - transform.position;
// Y座標を無視する処置
targetVector = new Vector3(targetVector.x , 0f, targetVector.z);
// 自分の向いている方向と対象のGameObjectとの差を得る
float targetAngle = Vector3.Angle( targetVector, transform.forward );

if(targetAngle <= angle)
{
  // 視界内の処理
}

(7) ターゲットのGameObjectへの角度と、向いている方向との角度差
ターゲットをロックオンしているときなど、向いている方向を固定したまま移動方向に応じてアニメーションを変更する場合は、360°の範囲(この場合は-180°~180°)の値をとる角度差を求める必要があります。

// 視界の広さを別途定義
float angle = 15.0f; // 倍の前方30度が視界となる。

// 視界に入っているかのチェック
// 対象のGameObjectとの位置関係
Vector3 targetVector = target.transform.position - transform.position;
// Y座標を無視する処置
targetVector.y=0f;
// 自分の向いている方向と対象のGameObjectとの差を得る。高低差は考慮しない
float targetAngle = Vector3.SignedAngle( targetVector, transform.forward, Vector3.up);

5) 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) localPosition(子)の操作

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

childObject.transform.localPosition = new Vector3();

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

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

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

// b)
childObject.transform.localEulerAngles = new Vector3( 0f, 0f, 10f); // 同上

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

3.3Dベクトルの操作

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

1) 単純な移動(元座標 → 指定座標)

特定の座標に等速で近づけます。一定の場所を往復する地形ブロックなどに使うと、イージング処置をベクトルではなく、より単純なfloatで扱うことができそうです。

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

2) 最大速度を制限する

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

// 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日 子オブジェクトの操作について追記
2023年4月7日 トピック追加。構成の見直しと誤記の訂正。

Discussion