シンプルなアクションゲームを作ってみようの第7回です。
今回はシーンに配置したキャラクターを動かすスクリプトを作成していきます。
前回はUnityでのスクリプトの作成の基本的な事についてやりました。
シンプルなアクションゲームを作ってみようの他の記事は
シンプルなアクションゲームを作るのを通してUnityの使い方を学ぶカテゴリです。
から参照出来ます。
キャラクター用のスクリプトを作成
まずはAssets/Scriptsフォルダ内にPlayerControllerスクリプトを作成し、Stage1シーンに配置したEthanにドラッグ&ドロップをして取り付けます。
PlayerControllerスクリプトにはキーボードの入力によってキャラクターを移動させるスクリプトを記述していきます。
Assets/Scripts/PlayerController.csをダブルクリックして、PlayerControllerスクリプトをVisual Studioで開きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerController : MonoBehaviour { // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } } |
スクリプトが長いので少しずつ記述していきます。
処理はPlayerControllerスクリプトに追加していきますが、フィールドはクラスのブロックのすぐ後、メソッドはフィールドの下に書いていく事とします(メソッドの順番は自分でわかりやすい順番にしてください)。
スクリプトの基本的な事を忘れてしまった場合は、前回の記事を見て確認してください。
また、スクリプトのエラーがコンソールに出た時は、エラー内容を確認し、修正してください。
エラーの修正方法は以下の記事も参照してください。
フィールド宣言部分
まずは使用するフィールド宣言を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // Rigidbodyコンポーネント private Rigidbody rigidBody; // キャラクターのコライダ private CapsuleCollider myCollider; // 入力値 private Vector3 input; // 移動速度 private Vector3 velocity; // 接地しているかどうか [SerializeField] private bool isGrounded; // 移動の速さ [SerializeField] private float moveSpeed = 2f; |
rigidBodyはEthanに取り付けているRigidbodyコンポーネント、myColliderにはEthanに取り付けているCapsuleColliderを入れます。
現時点では宣言のみで後で入れます。
inputはユーザーが入力したXYZの入力値を入れます。
XYZの3つの値を使う場合はVector3型を使うと便利です。
ただし今回の入力で使うのはXZだけなのでVector2型でもいいですが、velocityと合わせる為にVector3型にしました。
velocityはユーザーの入力値から速度を計算したものを入れます。
isGroundedはキャラクターが地面と接地しているかどうか?を表します。
実際に接地しているかどうかを判定する為には、自分で処理を書く必要があります。
[SerializeField]アトリビュートを取り付けているのは、Unityエディターでプレイボタンを押した時に、キャラクターが地面と接地しているかどうかの判定をインスペクタで確認する為です(確認しない場合はアトリビュートは要りません)。
moveSpeedはキャラクターの移動スピードに使います。
こちらにも[SerializeField]アトリビュートが付いていて、インスペクタで値を設定出来ます。
Startメソッド
Startメソッドに処理を追加します。
1 2 3 4 5 6 7 | // Start is called before the first frame update void Start() { rigidBody = GetComponent<Rigidbody>(); myCollider = GetComponent<CapsuleCollider>(); } |
フィールドで宣言したrigidBodyとmyColliderにスクリプトが取り付けられているゲームオブジェクトのコンポーネントから取得し代入しています。
1 2 3 | GetComponent<取得する型>(); |
で自身(スクリプトが設定されているゲームオブジェクト)に取り付けている指定した型のコンポーネントを取得出来ます。
Startメソッド内でコンポーネントを取得してその参照をフィールドに保持しておくと、後でそのコンポーネントを使いたい時に再度コンポーネントを取得する必要がないので、アクセスが速くなります。
Updateメソッド
Updateメソッドに処理を追加します。
1 2 3 4 5 6 7 8 9 | // Update is called once per frame void Update() { // 接地確認 CheckGround(); // 移動速度の計算 Move(); } |
接地確認をするCheckGroundメソッドの呼び出しと、移動速度の計算をするMoveメソッドの呼び出しをしています。
この二つのメソッドはまだ作っていないのでこの後作成していきます。
接地確認メソッドCheckGroundの作成
接地を確認するメソッドを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 地面のチェック private void CheckGround() { if (isGrounded) { return; } // GroundまたはEnemyレイヤーと球のコライダがぶつかった場合は地面に接地 if (Physics.CheckSphere(rigidBody.position, myCollider.radius - 0.1f, LayerMask.GetMask("Ground")) ) { isGrounded = true; velocity.y = 0f; } else { isGrounded = false; } } |
CheckGroundメソッドの最初でisGroundedがtrueかfalseかをif文で確認しています。
if文は()の中の条件がtrueかfalseかを判断し、trueであれば{}の中身を実行します。
isGroundedがtrueの時は接地しているということなので接地確認をする必要がないので、returnを使ってそれ以降の処理をせずに呼び出し元に帰します(今回の場合はUpdateメソッド内で呼び出しているのでそこに戻る)。
戻り値がvoidの場合でもreturnが使えますが、当然戻り値はないので戻り値は指定しません。
returnは即座に呼び出し元に戻せるので便利ですが、使いどころを間違えると本来は実行したい処理を飛ばして呼び出し元に戻ってしまうので注意が必要です。
もしreturnを使うと分かり辛いという方は使わないでif文でisGroundedがfalseの時に接地の確認をするというように処理を変更してもかまいません。
1 2 3 4 5 | if(!isGrounded) { // 接地確認の処理 } |
isGroundedがfalseの場合はそれ以降の処理に進み、またif文で条件分岐をします。
今回は床から落下した場合はisGroundedがtrueのままですが、落下時にfalseにしたい場合はこれらの条件なしに常に地面をチェックするようにします。
!(エクスクラメーションマーク)を付けるとtrueとfalseを反転させます。
Physics.CheckSphereメソッドは球の形の範囲を作成し、その範囲で何らかのレイヤーが設定されたコライダと接しているかどうかを判定するものです。
第1引数には球の中心位置、第2引数には球の半径、第3引数には接触したと判定するゲームオブジェクトに設定されたレイヤーを指定します。
レイヤーについてはこの記事で別途やります。
1 2 3 | Physics.CheckSphere(球の中心位置, 球の半径, 該当するレイヤーマスク) |
今回は中心位置にrigidBody.positionで位置を取得し指定していますが、位置を取得したい場合はtransform.positionでも取得出来ます。
ただし、Rigidbodyを介して位置を取得した方が高速になるようです。
詳細はUnityのスクリプトリファレンスに載っています。
球の半径にはキャラクターに取り付けたCapsuleColliderの半径より少し小さい半径を指定しています。これはキャラクターの横にゲームオブジェクトが来た場合にも接地したと判定されるのを避ける為、キャラクターのコライダよりも少し小さい値にしています。
該当するレイヤーマスクでは
1 2 3 | LayerMask.GetMask("Ground") |
上のようにGroundという名前のレイヤーが設定されたゲームオブジェクトのコライダと接触した場合のみに限定しています。
Physics.CheckSphereがtrueになった場合はisGroundedをtrueにし、Y方向の速度を0にしています。
Y方向の速度を0にしていますが、いずれジャンプ機能を作る時にvelocityのY方向の速度を操作して作る場合は必要になりますが、Rigidbodyを操作する場合は必要ありません。
Physics.CheckSphereのようにある形状の範囲で他のコライダと接触したかどうかや、レイ(線)を飛ばして他のコライダと接触したかどうか等の判定メソッドは他にもたくさんあります。
用途によって使い分けるといいです。
移動速度計算メソッドMoveの作成
次に移動速度を計算するMoveメソッドを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 移動値と向きの計算 private void Move() { // 接地している場合 if (isGrounded) { // 移動速度を初期化 velocity = Vector3.zero; } // 移動入力値 input = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical")); // 速度の計算 velocity = new Vector3(input.normalized.x * moveSpeed, 0f, input.normalized.z * moveSpeed); } |
接地している場合はvelocityにVector3.zeroでXYZが0のVector3の値を入れています。
その後Input.GetAxis(“Horizontal”)で横方向、Input.GetAxis(“Vertical”)で前後方向の入力値を得て、それをVector3のXとZにそれぞれ設定します。
Input.GetAxisは引数で指定した名前のキーやコントローラースティックの入力値を取得出来ます。
どの名前が、どのキーやコントローラースティックの入力値と対応するかは、UnityのメニューのEdit→Project Settings…を選択し確認や設定をします。
出てきたProject SettingsウインドウのInput Managerで設定されています。
一番上のHorizontalの設定ではNegative Buttonにleft、Positive Buttonにrightが設定されています。
これは→キーを押した時にプラス、←キーを押した時にマイナスの値が得られます。
Alt Negative ButtonとAlt Position Buttonは第2のボタンでaキーでマイナス、dキーでプラスとなります。
つまりHorizontalでは、一つの名前に二つのキーの割り当てがされています(別途スティック用のHorizontalもあります)。
入力値であるinputが決まったら、その入力値を使って移動速度を計算します。
1 2 3 | velocity = new Vector3(input.normalized.x * moveSpeed, 0f, input.normalized.z * moveSpeed); |
input.normalizedでベクトルの正規化(長さ1のベクトルにする)をしています。
これは入力の方向だけを得る為で、長さはmoveSpeed分にしたい為です。
inputはVector3の値なので、XとZに入力値のそれぞれの成分を入れてmoveSpeedを掛けています。
ギズモの表示メソッドの作成
1 2 3 4 5 6 7 8 | // ギズモの表示 void OnDrawGizmos() { Gizmos.color = Color.red; // 接地判断時の範囲表示 Gizmos.DrawLine(transform.position + Vector3.up * 0.1f, transform.position + Vector3.down * 0.2f); } |
OnDrawGizmosメソッドを作成するとシーンビューやゲームビューなどにギズモを表示することが出来ます。
ギズモは目印として使用したり、実際はどのような形状なのかを視覚的に確認したい時に使用します。
Gizmos.color = Color.redでギズモの色を赤色に設定しています。
Gizmos.DrawLineメソッドで第1引数の位置から第2引数の位置まで線を引いています。
ここでは接地を確認する時の計算を線を使って視覚的に確認出来るようにしています。
ここで、transform.positionをrigidBody.positionにした方がいいのではないか?と思われる方もいるかもしれませんが、rigidBodyはStartメソッドで取得しているので、Unityエディターでプレイボタンを押さない限りRigidbodyコンポーネントを取得出来ません。
ギズモはプレイボタンを押していない時でもシーンビュー等で表示されるので、rigidBodyがNull(設定されていない)というエラーがコンソールに表示されてしまいます。
FixedUpdateメソッドの作成
次にFixedUpdateメソッドを作成します。
FixedUpdateメソッドはUnityメニューのEdit→Project Settings…を選択し、Timeを選択した時に表示されるFixed Timestepの秒数毎に呼び出されます。
デフォルトでは0.02になっているので0.02秒毎にFixedUpdateメソッドが実行されます。
ただし処理が多い場合はズレます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // 固定フレームレートで実行される private void FixedUpdate() { // 入力がある時だけ実行 if (!Mathf.Approximately(input.x, 0f) || !Mathf.Approximately(input.z, 0f)) { // 入力方向に向ける rigidBody.MoveRotation(Quaternion.LookRotation(input.normalized)); } //// 入力がある時だけ実行 //if (input.magnitude > 0f) { // // 入力方向に向ける // rigidBody.MoveRotation(Quaternion.LookRotation(input.normalized)); //} // 現在の位置に1フレームの速度分を足して移動させる if (velocity.magnitude > 0f) { rigidBody.MovePosition(rigidBody.position + velocity * Time.fixedDeltaTime); } } |
Mathf.Approximatelyメソッドでfloat型の値を比較出来ます。
!でboolを反転しています。
!Mathf.Approximately(input.x, 0f)で入力値のXが0でない時
!Mathf.Approximately(input.z, 0f)で入力値のZが0でない時
||は左右のどちらかの条件がtrueの時にtrueになります(日本語に表すと または にあたります)。
つまり何らかの入力がされていればその下のブロックの処理を行います。
RigidbodyのMoveRotationメソッドを使用するとRigidbodyを回転出来ます。
Quaternion.LookRotationメソッドは引数で指定した方向の回転(Quaternion値)を取得出来ます。
上のスクリプトの場合はinput.normalizedで入力した方向を取得しています。
ここら辺は少し難しいですね・・・(-_-)
Mathf.Approximatelyを使わない場合はinput.magnitude(入力の大きさ)が0より大きい時だけ回転させます。
1 2 3 4 5 6 7 | // 入力がある時だけ実行 if (input.magnitude > 0f) { // 入力方向に向ける rigidBody.MoveRotation(Quaternion.LookRotation(input.normalized)); } |
次に移動させる処理です。
1 2 3 4 5 6 | // 現在の位置に1フレームの速度分を足して移動させる if (velocity.magnitude > 0f) { rigidBody.MovePosition(rigidBody.position + velocity * Time.fixedDeltaTime); } |
velocity.magnitudeで速度(ベクトル)の長さを求められます。
つまり速さ(スカラー)が0より大きければ移動処理をします。
RigidbodyのMovePositionメソッドを使用すると移動出来ます。
引数には移動先の位置を指定します。
上のスクリプトの場合は現在の位置に1フレーム分の速度を足した分の位置を指定し、移動させています。
Time.fixedDeltaTimeで前回のFixedUpdateメソッドから現在のFixedUpdateメソッドまでの時間を取得出来ます。
固定フレームレートなのでだいたい(デフォルトでは)0.02です。
Rigidbodyの位置を直接変更しない方がいい
先ほどのスクリプトでRigidbodyのMoveRotationやMovePositionを使って回転や移動処理を実行するようにしました。
ですが、
1 2 3 | transform.position = transform.position + velocity * Time.fixedDeltaTime; |
や
1 2 3 | rigidBody.position = rigidBody.position + velocity * Time.fixedDeltaTime; |
のように直接位置を変更することも出来ます。
ただしRigidbodyを使って物理的に移動や回転処理をする場合は、まずtransformを使った処理と同時に使うと動作がおかしくなる可能性があるので使わないようにします。
またrigidBody.positionを使って位置を変更する処理もなるべく使わない方がいいです。
位置や回転を直接変更すると、小刻みにワープして移動や回転をしているのと同じなので物理的に移動や回転をしているのとは違います。
MoveRotationやMovePositionの場合はRigidbodyコンポーネントのinterplateで補間の設定をしている場合に、フレーム間の補間が有効になるので、すり抜けが起こりにくくなります。
同様にRigidbodyのプロパティのvelocityを直接変更して速度を変えるのもあまりよくないようです。
Rigidbodyのvelocityを直接変更するのは、ジャンプなど直ぐに遠くに飛ばしたい時等の限定的な使用が良いようです。
ただMoveRotationやMovePositionを使う場合はRigidbodyのIsKinematicにチェックを入れておかないと補完機能が働かず小刻みにワープしているのと変わらないみたいです。
であれば直接位置を変換してるのと同じです。(^_^;)
AddForceやAddTorque
今回はRigidbodyのMoveRotationとMovePositionを使ってキャラクターの回転と移動させますが、その他にAddForceメソッドで力を加えたり、AddTorqueメソッドで回転に力を加えられます。
Rigidbodyの物理的な特性を利用する場合はAddForceやAddTorqueを使って力を加えるやり方が良さそうです。
Planeゲームオブジェクトにレイヤーを設定
これでスクリプトが出来たので、次はスクリプトの中で接地の確認に使用しているGroundレイヤーを作成し、床のゲームオブジェクトであるPlaneゲームオブジェクトにGroundレイヤーを設定したいと思います。
タグとレイヤーとは?
タグはゲームオブジェクトを識別する名前のようなものです。
レイヤーはゲームオブジェクトに設定出来るグループです。
タグとレイヤーは似たような感じですが、レイヤーの場合は所属するグループを設定します。
UnityメニューのEdit→Project Settings…を選択し、Physicsを押すと下の方にLayer Collision Matrixで衝突判定をするレイヤーの設定が出来ます。
ここでチェックを外せば該当するレイヤー同士の衝突が行われなくなります。
なので、例えば弾丸のゲームオブジェクトを作成した時に弾丸同士は衝突しないようにしたい時は、Bulletというレイヤーを作成して弾丸のゲームオブジェクトに設定し、Layer Collision Matrixに表示される縦と横のBulletが交錯する部分のチェックを外します。
Groundレイヤーを作成し、Planeに設定していきます。
ヒエラルキーでPlaneゲームオブジェクトを選択し、インスペクタでLayerの部分を押し、Add Layerを選択します。
Tags & Layersがインスペクタに表示されるので、User Layer 6にGroundと入力します。
BuiltIn Layerは最初から作られているレイヤーです。
Groundレイヤーを作成したら、再度ヒエラルキーでPlaneゲームオブジェクトを選択し、LayerからGroundを設定します。
これでPlaneゲームオブジェクトにはGroundレイヤーが設定され、スクリプトでGroundレイヤーで判定する時に、このPlaneは地面であると認識出来ます。
あくまでグループ名をGroundとしただけで、Groundという名前にすると地面になるわけではありませんので注意が必要です。
実行して確認
これでキャラクター移動のスクリプトが出来たので、Unityエディターのプレイボタンを押して実行し、矢印キーを押してEthanが移動するかどうかを確認してみましょう。
以下のようになりました。
終わりに
このカテゴリではRigidbody+コライダでキャラクターの移動処理を作っているので、CharacterControllerを使った移動処理よりも少し難しくなっています。
最初はなんじゃこれ!難しすぎ!と思うかもしれませんが、何回もスクリプトを作っていれば段々と慣れてきます。
慣れてくるだけで簡単になるわけではありませんが・・・・(´Д`)
キャラクターは移動しましたが、アニメーションが行われていないので、そこら辺を次回やりたいと思います。