今回の記事はUnityで3Dエンドレスランゲームを制作する講座の第3回目です。
前回は無限ランで走る3Dモデルの作成、走るモーションとジャンプモーションのアニメーションの設定を行いました。
前回の記事:
今回はランゲームのコースを作成し、キャラクターの移動やジャンプ処理の実装をUnity C#スクリプトを用いて行います。また、障害物の設置や当たり判定の設定を行い本格的なランゲームの開発を行っていきます。
3Dランゲームのフィールドマップを作成しよう
さあ、いよいよ3Dランゲームで走るためのマップを制作していきます。
まずは地面を作って簡単に装飾を行います。続いてゲーム内で使いやすくするためにいくつかの変更を加え、複製して無限にランできる土台を用意します。
まずはアセットフォルダでAssets > Forest Pack > Prefabsフォルダを探し、Island002を現在開いているシーンにドラッグ&ドロップします。
このオブジェクトを選択した状態で、Forest PackのMaterialsフォルダをクリックし、インスペクター画面の「Island002」のMeshRendererのMaterialの項目に緑色のMaterialオブジェクト「Color009」をドラッグ&ドロップします。
これで草原のフィールドができました。このあとは木々のアセットを配置して森林を作成していく流れになります。
森林を作成する前に一度キャラクターもしくはIsland002のTransformの位置を再度変更し、キャラクターがフィールドの上に位置するようにします。
サンプルでは、CharacterのTransform Position = (0, 0.4, 0)、Island002のTransform Position = (0, -1, 6.32)としています。
Main CameraのTranfsform位置を再修正してちょうどいい位置を探してみましょう。このあたりの数値はある程度各自の自由な値を設定してもらってOKです。
ここでヒエラルキー画面で一度作った島である「island002」を選択します。Ctrl+Dを押して複製してみましょう。さらにもう一度複製します。そして作った島のTransformの値を変更し、位置を移動させ、左に1つ、右に1つ配置します。
ここでは、Island002(1)のTransform Position = (-10.5, -1, 6.32)、Island002(2)のTransform Position = (10.5, -1, 6.32)としてみました(フィールド3つがすき間なく並べば異なる値で作成してもOKです)。
両脇のislandオブジェクトに木々や植物や石を自由に配置していきます。
アセットのプレハブを島の子オブジェクトとしてドラッグ&ドロップして作っていってください(参考図は以下)。
真ん中の島はランゲームのメインフィールドになります。プレイヤーとのインタラクションを組みこむことになります。こちらも飾り付けしておきましょう。
また、装飾に用いる木々や草や岩のプレハブの色は、インスペクタのMaterialの要素で変更できます。アセットのMaterialフォルダから他のMaterial素材をドラッグするか、独自のMaterialを作成して追加してください。
Projectフォルダで右クリック→Create→MaterialでMaterialを作成して使えば自分好みに色をカスタマイズすることができます。
また、真ん中の島には、プレイヤーが避けたり飛び越えたりできるような障害物をいくつか配置しておきましょう。
Ctrl+Sでシーンを定期的に保存することをお忘れなく。
Unityの物理エンジンを活用して3Dランの移動可能・不可能エリアを作る
ここではキャラクターが走るためのフィールド領域を設定していきます。
今の段階では地面は単なる飾りに過ぎません。3Dランゲームをプレイするには物理演算が組み込まれた地面が必要になります。そのためにBox Colliderというコンポーネントを使います。
もしアセットの地面に Box Collider(ボックスコライダー)が付いていれば何もする必要はありません。
今回はBox Colliderを設定する必要があります。ヒエラルキー画面で右クリック > 3D Object > Cubeを選択し、立方体オブジェクトを作成します。
このCubeオブジェクトを中央のisland002の子オブジェクトとしてドラッグ&ドロップし、Transformコンポーネントの右メニューを選択し、Resetを選択。位置を初期化します。
そして私たちが作る3D障害物ランゲームのプレイ領域に合うようにCubeのTransformのshape、sizeそしてpositionを変えます。
ここで、のちの実行時の当たり判定計算の都合上、Transformの値は本来は小数を使わず整数にしておいたほうがいいでしょうが、サンプルでは小数も使ってしまっています。数値は以下の画像のように設定しました。
続いて、立方体のMaterialをisland002のMaterialの色に合わせておきましょう。
次にislandのオブジェクトを選択し、Mesh Colliderを削除します。必要なのはMesh Rendererだけです。
Colliderを使って当たり判定を作りますが、今回はMesh Colliderの代わりにCubeオブジェクトのBox Colliderをゲーム内で使用します。
Cubeオブジェクトを追加した後、数値の設定次第では障害物とキャラクターの高さを変更する必要があるかもしれません。
3Dランゲームのキャラクターを動かす処理をUnity C#スクリプトで実装する
ここからはランゲームで走るキャラクターの動作をC#スクリプトで実装していきます。
プロジェクトフォルダに新しいC#スクリプトを作成します。Scriptsフォルダを右クリック → Create → C# Scriptを選択しましょう。
名前にはスペースを入れずわかりやすい名前を付けます。ここではPlayerMovementと名付けます(PlayerMovement.csというC#スクリプトが生成されます)。
スクリプトをダブルクリックして開いてみましょう。Visual Studioまたはお好みのエディタを使って表示しましょう。
Visual Studioがインストールされているにもかかわらず、スクリプトがVisual Studioで開かない場合は、メニューのEdit → Preferences → External Tools → External Script Editorから、環境設定にVisual Studioを割り当ててください。
PlayerMovement.csを以下のコードに書き換えてください:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerMovement : MonoBehaviour { public float moveSpeed = 3f; public float acceleration = 0.01f; public float leftRightSpeed = 4f; public float limit = 5f; public Animator animator; // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { transform.Translate(Vector3.forward * Time.deltaTime * moveSpeed, Space.World); moveSpeed += Time.deltaTime * acceleration; if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow)) { if (transform.position.x > -limit) { transform.Translate(Vector3.left * Time.deltaTime * leftRightSpeed); } } if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow)) { if (transform.position.x < limit) { transform.Translate(Vector3.left * Time.deltaTime * leftRightSpeed * -1); } } if (Input.GetKeyDown(KeyCode.Space)) { animator.SetTrigger("a_Jump"); } } } |
スクリプトの基本の構造の解説は今回割愛しますが、スクリプトの基本構造を理解したい方は「Unity C#スクリプトの構造 スクリプトの作成と実行方法」を読んでみてください。
今回は3Dランゲームを開発するために元のスクリプトに追加した部分を中心に解説していきます。
Start()メソッドの前で4つの変数を宣言:
1 2 3 4 5 |
public float moveSpeed = 3f; public float acceleration = 0.01f; public float leftRightSpeed = 4f; public float limit = 5f; public Animator animator; |
1つ目の変数はキャラクターが走る速度で、2つ目はランゲームを進めるにつれてその速度を増加させる加速度。3つ目は横方向への移動スピード。4つ目は横方向に進むことが許される限界距離です。5番目はアニメーターの変数宣言をしています。アニメーターの中身はInspector画面で後ほど設定します。
Update()メソッドに追加したコードについて:
1 2 |
transform.Translate(Vector3.forward * Time.deltaTime * moveSpeed, Space.World); moveSpeed += Time.deltaTime * acceleration; |
1行目はUnityの原点から見た座標空間において、キャラクターの位置を、時間×速度分だけ前方に移動させる処理になります。
2行目では時間に対して速度を増加させ、加速度を乗算します。
1 2 3 4 5 6 7 |
if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow)) { if (transform.position.x > -limit) { transform.Translate(Vector3.left * Time.deltaTime * leftRightSpeed); } } |
この命令では「A」または左矢印ボタンを入力すると、プレーヤーは左に進むことを意味しています。また、その移動距離の制限がlimitで指定されています。
1 2 3 4 5 6 7 |
if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow)) { if (transform.position.x < limit) { transform.Translate(Vector3.left * Time.deltaTime * leftRightSpeed * -1); } } |
「D」または左矢印ボタンを入力すると、プレーヤーは右に進みます。左移動と同じでlimitが移動距離の制限を与えています。
1 2 3 4 |
if (Input.GetKeyDown(KeyCode.Space)) { animator.SetTrigger("a_Jump"); } |
スペースキーを押すとキャラクターがジャンプします。
Ctrl+Sでスクリプトの変更内容を保存してエディタに戻りましょう。
Unityエディタに戻ったらCharacterプレハブを開き、今作成したスクリプトPlayerMovement.csをインスペクタ上にドラッグ&ドロップします。
そしてCharacterのAnimator コンポーネントをPlayerMovementのanimator フィールドにドラッグ&ドロップします。
Ctrl+SでUnityのシーンを保存し、エディターでPlayを押してキャラクターが動くかどうかをテストしてください。
これだと移動速度が速すぎるかもしれませんね。
移動が速すぎる場合は移動速度であるMoveSpeedを下げてみましょう。1に下げてテストしてみてください。
以下のことが確認できるはずです:
- キャラクターが障害物を通過する。
- カメラは後ろにとどまり、プレイヤーだけが進み続けます。
- ジャンプしても、ジャンプアニメーションをするだけで高さは変わらず、実際にはジャンプしていない。
これらの問題を一つずつ解決していきましょう。
キャラクターに障害物との当たり判定を実装する
まずは障害物との当たり判定実装していきましょう。Characterプレハブを開きます。
インスペクタ画面でAdd componentをクリックし、Capsule Colliderを追加します。
「Edit Collider」ボタンをクリックし、小さな緑色の四角を使って、モデルに合うようにコライダのサイズを調整します。
サイズ調整が終了したらもう一度Edit Colliderをクリックしましょう。
コライダーはY軸上で少し上に移動させる必要があるかもしれません。
次に、CharacterプレハブにRigidbodyコンポーネントを追加します。Add Component → 「Rigidbody」を選択してください。
インスペクタ画面の「Constraints」の欄で、全方向の「Rotation(回転)」とY方向の「Position(座標)」を「Freeze」にチェックを入れて固定します。
ヒエラルキー画面で戻る向きの矢印をクリックしてプレハブ編集画面を終了し、変更を保存しましょう。
また、障害物オブジェクトにもコライダーが必要です。ただし、今回使用するアセットには既にコライダーが設定されています。
もしコライダーのコンポーネントがなかったら、すべての障害物にBoxColliderかCapsuleColliderのコライダを追加しなければなりません。RigidBodyは不要です。
Ctrl+Sを押してシーンを保存したのち、Unityエディタで再生モードに入りましょう。
これで、プレイヤーが障害物にぶつかると(アニメーションではなく実際に進む)動きが止まることがわかります。障害物やプレイヤーのコライダーの位置によっては異なる動きになるので各自CharacterオブジェクトのColliderのy座標や障害物の配置調整を行ってください。
また、衝突してもプレイヤーが滑らないようにするには、障害物にBoxコライダーを追加する必要があります。
3Dランゲームの動きに合わせてカメラをプレイヤーに追随させる
ヒエラルキー画面でMain CameraオブジェクトをCharacterオブジェクトにドラッグ&ドロップして子オブジェクトに設定します。
カメラの見え方が気に入らない場合は、TransformのPositionの値を変更するかシーン画面で直接操作して位置を調節してください。
Unityエディタの再生ボタンを押してプレイヤーを動かすと、カメラがプレイヤーを追いかけるようになったのがわかります。障害物の高さなどは当たり判定に引っかかるように微調整しました。
Main CameraをCharacterオブジェクトに追加して動きを追うようにはなりました。次はそれをプレイヤーのオブジェクトから分離します。
まずCharacterプレハブを選択し、プレハブ編集画面に入ります。ヒエラルキー内で右クリックし、「Create Empty(空のオブジェクトを作成)」します。名前をPlayerにします。
他のすべてのオブジェクトをこの新しいオブジェクトの子オブジェクトにするためドラッグ&ドロップします。
親オブジェクトであるCharacterオブジェクトからAnimator Componentを右クリックし、Copy Componentを選択します。
子オブジェクトであるPlayerオブジェクトを選択し、Transformコンポーネントを右クリック → Paste(貼り付け) → Component As New(新しいコンポーネントとして作成)を選択します。
再度、親オブジェクトのコンポーネントで右クリックし、Animatorのコンポーネントを削除します。
CharacterオブジェクトのPlayerMovement(Script)コンポーネント上で、Player(プレイヤー)オブジェクトを現在NoneになっているAnimator(アニメーター)フィールドにドラッグします。
PlayerオブジェクトのTransformをリセットします。
ただし、basic_rigの子の位置やRotation(回転)は、x=-90、z=90のままにしておきましょう。
障害物を避けるジャンプ処理を実装しよう
さて、3つ目のジャンプの動きの実装は少し厄介です。
PlayerMovementスクリプトにコードを追加していきます。C#スクリプトを開いてください。
修正したスクリプト全体は以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerMovement : MonoBehaviour { public float moveSpeed = 2f; public float acceleration = 0.01f; public float leftRightSpeed = 4f; public float limit = 5f; public Animator animator; public Vector3 jump; public float jumpForce = 2.0f; public bool isGrounded; Rigidbody rb; // Start is called before the first frame update void Start() { rb = GetComponent<Rigidbody>(); jump = new Vector3(0.0f, 2.0f, 0.0f); } void OnCollisionStay() { isGrounded = true; } void OnCollisionExit() { isGrounded = false; } // Update is called once per frame void Update() { transform.Translate(Vector3.forward * Time.deltaTime * moveSpeed, Space.World); moveSpeed += Time.deltaTime * acceleration; if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow)) { if (transform.position.x > -limit) { transform.Translate(Vector3.left * Time.deltaTime * leftRightSpeed); } } if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow)) { if (transform.position.x < limit) { transform.Translate(Vector3.left * Time.deltaTime * leftRightSpeed * -1); } } if (Input.GetKeyDown(KeyCode.Space)) { animator.SetTrigger("a_Jump"); } if (Input.GetKeyDown(KeyCode.Space) && isGrounded) { animator.SetTrigger("a_Jump"); rb.AddForce(jump * jumpForce, ForceMode.Impulse); isGrounded = false; } } } |
まず4つの変数を新しく宣言します。
1 2 3 4 |
public Vector3 jump; public float jumpForce = 2.0f; public bool isGrounded; Rigidbody rb; |
Vector3の型を持つjumpは空間ベクトル(x,y,z)の3成分の値を格納する変数です。ここではジャンプしている場所を判断するために使っています。
また、ジャンプ可能かどうかの判定のため地面にいるかどうかを判断する必要があります。この処理をBool型変数isGroundedで扱います。Bool型変数はTrue(真)とFalse(偽)の2種類の値を設定できます。
Rigidbody rbは物理演算のために使用します。
次に、Start関数で変数rbとjumpの初期化処理を行います。
1 2 3 4 5 |
void Start() { rb = GetComponent<Rigidbody>(); jump = new Vector3(0.0f, 2.0f, 0.0f); } |
CharacterオブジェクトのRigidBodyコンポーネントがRigidbody型変数rbに格納されます。
次にCharacterオブジェクトが地面に接しているかどうかを判定するためのメソッドOnCollisionStay()を追加します。
1 2 3 4 |
void OnCollisionStay() { isGrounded = true; } |
地面との接している状態ではなくなったことを検知するためにその下にメソッドOnCollisionExit()を追加します。
1 2 3 4 |
void OnCollisionExit() { isGrounded = false; } |
最後に、Update()の中で、Spaceキーを押すかどうかのチェック処理で内容を次のように変更します:
1 2 3 4 5 6 |
if (Input.GetKeyDown(KeyCode.Space) && isGrounded) { animator.SetTrigger("a_Jump"); rb.AddForce(jump * jumpForce, ForceMode.Impulse); isGrounded = false; } |
つまり、スペースキーを押したときにプレイヤーが地面に接地していれば、animatorに設定したアニメーションが起動し、Characterオブジェクトの持つリジッドボディにjumpベクトルの方向にjumpForceの力が撃力(短時間での衝撃)として加わります。
そしてジャンプしている間、isGrounded boolはfalseになります。
ここまでできたらCtrl+Sでスクリプトを保存し、Unityエディタに戻ります。
Characterプレハブを開き、Rigidbodyのy方向のFreeze Positionを解除します(ジャンプできるように)。
プレハブ編集画面を終了し、再生ボタンを押して変更した箇所の動作をテストしてみましょう。
もし意図したように動作しないものがあれば、コライダの形状、アニメーションの遷移時間、スピード、その他私たちが変更したものをあなたのゲームに合わせて変更してみてください。
また、今回は地面との当たり判定を平らなcubeオブジェクトのColliderで代用しているので草原フィールドに埋もれたように見えるタイミングも出てきます。
曲面対応した実装もできなくはないですが、3Dの無限ランゲームでは走る地面自体は平坦な地形で作るのが通常なのでこのような形としました。
まとめ
今回は3Dアセットを用いてランゲームで使うフィールドの土台を構築しました。
また、プレイヤーを走って移動させること、カメラを追随させること、そしてジャンプを行い障害物を避ける動きを実装しました。
次回はBGMや効果音の設定から始めていきます。また、今の段階では地形が切れているところでゲームが終了してしまいますが、地形を無限生成して無限3D障害物ランゲームの流れを仕上げていきます。
次の記事:
コメント