Unity 用エンドレス ランナー チュートリアル
ビデオゲームでは、世界がどんなに広くても、必ず終わりがあります。ただし、無限の世界をエミュレートしようとするゲームもあり、そのようなゲームは Endless Runner と呼ばれるカテゴリに分類されます。
エンドレス ランナーは、プレイヤーがポイントを集めて障害物を避けながら常に前進するタイプのゲームです。主な目的は、障害物に落ちたり衝突したりすることなくレベルの最後に到達することですが、多くの場合、プレイヤーが障害物に衝突するまでレベルが無限に繰り返され、徐々に難易度が上がります。
現代のコンピューターやゲーム デバイスですら処理能力が限られていることを考えると、真に無限の世界を作ることは不可能です。
では、一部のゲームはどのようにして無限の世界のような錯覚を生み出すのでしょうか?答えは、ビルディング ブロック (別名オブジェクト プーリング) を再利用することです。つまり、ブロックがカメラ ビューの後ろまたは外に出るとすぐに前面に移動します。
Unity でエンドレス ランナー ゲームを作成するには、障害物とプレイヤー コントローラーを備えたプラットフォームを作成する必要があります。
ステップ 1: プラットフォームを作成する
まず、後で Prefab に保存されるタイル化されたプラットフォームを作成します。
- 新しいゲームオブジェクトを作成して呼び出します "TilePrefab"
- 新しいキューブを作成します (ゲームオブジェクト -> 3D オブジェクト -> キューブ)
- Cube を "TilePrefab" オブジェクト内に移動し、位置を (0, 0, 0) に変更し、スケールを (8, 0.4, 20) に変更します。
- 必要に応じて、次のように追加のキューブを作成して、側面にレールを追加できます。
障害物については 3 つの障害物のバリエーションを用意しますが、必要に応じていくつでも作成できます。
- "TilePrefab" オブジェクト内に 3 つのゲームオブジェクトを作成し、それらに "Obstacle1"、"Obstacle2"、および "Obstacle3"
- 最初の障害物として、新しい Cube を作成し、"Obstacle1" オブジェクト内に移動します。
- 新しい Cube をプラットフォームとほぼ同じ幅に拡大縮小し、高さを縮小します (プレーヤーはこの障害物を避けるためにジャンプする必要があります)
- 新しいマテリアルを作成し、"RedMaterial" という名前を付け、その色を赤に変更してから、それを Cube に割り当てます (これは、障害物をメイン プラットフォームから区別するためです)
- "Obstacle2" の場合は、いくつかの立方体を作成し、三角形の形で配置し、下部に 1 つの空きスペースを残します (プレイヤーはこの障害物を避けるためにしゃがむ必要があります)。
- そして最後に、"Obstacle3" は、"Obstacle1" と "Obstacle2" を組み合わせた複製になります。
- 次に、障害物内のすべてのオブジェクトを選択し、そのタグを "Finish" に変更します。これは、後でプレーヤーと障害物の間の衝突を検出するために必要になります。
無限のプラットフォームを生成するには、オブジェクト プーリングと障害物のアクティブ化を処理するいくつかのスクリプトが必要です。
- という新しいスクリプトを作成し、"SC_PlatformTile" という名前を付けて、その中に以下のコードを貼り付けます。
SC_PlatformTile.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SC_PlatformTile : MonoBehaviour
{
public Transform startPoint;
public Transform endPoint;
public GameObject[] obstacles; //Objects that contains different obstacle types which will be randomly activated
public void ActivateRandomObstacle()
{
DeactivateAllObstacles();
System.Random random = new System.Random();
int randomNumber = random.Next(0, obstacles.Length);
obstacles[randomNumber].SetActive(true);
}
public void DeactivateAllObstacles()
{
for (int i = 0; i < obstacles.Length; i++)
{
obstacles[i].SetActive(false);
}
}
}
- という新しいスクリプトを作成し、"SC_GroundGenerator" という名前を付けて、その中に以下のコードを貼り付けます。
SC_GroundGenerator.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SC_GroundGenerator : MonoBehaviour
{
public Camera mainCamera;
public Transform startPoint; //Point from where ground tiles will start
public SC_PlatformTile tilePrefab;
public float movingSpeed = 12;
public int tilesToPreSpawn = 15; //How many tiles should be pre-spawned
public int tilesWithoutObstacles = 3; //How many tiles at the beginning should not have obstacles, good for warm-up
List<SC_PlatformTile> spawnedTiles = new List<SC_PlatformTile>();
int nextTileToActivate = -1;
[HideInInspector]
public bool gameOver = false;
static bool gameStarted = false;
float score = 0;
public static SC_GroundGenerator instance;
// Start is called before the first frame update
void Start()
{
instance = this;
Vector3 spawnPosition = startPoint.position;
int tilesWithNoObstaclesTmp = tilesWithoutObstacles;
for (int i = 0; i < tilesToPreSpawn; i++)
{
spawnPosition -= tilePrefab.startPoint.localPosition;
SC_PlatformTile spawnedTile = Instantiate(tilePrefab, spawnPosition, Quaternion.identity) as SC_PlatformTile;
if(tilesWithNoObstaclesTmp > 0)
{
spawnedTile.DeactivateAllObstacles();
tilesWithNoObstaclesTmp--;
}
else
{
spawnedTile.ActivateRandomObstacle();
}
spawnPosition = spawnedTile.endPoint.position;
spawnedTile.transform.SetParent(transform);
spawnedTiles.Add(spawnedTile);
}
}
// Update is called once per frame
void Update()
{
// Move the object upward in world space x unit/second.
//Increase speed the higher score we get
if (!gameOver && gameStarted)
{
transform.Translate(-spawnedTiles[0].transform.forward * Time.deltaTime * (movingSpeed + (score/500)), Space.World);
score += Time.deltaTime * movingSpeed;
}
if (mainCamera.WorldToViewportPoint(spawnedTiles[0].endPoint.position).z < 0)
{
//Move the tile to the front if it's behind the Camera
SC_PlatformTile tileTmp = spawnedTiles[0];
spawnedTiles.RemoveAt(0);
tileTmp.transform.position = spawnedTiles[spawnedTiles.Count - 1].endPoint.position - tileTmp.startPoint.localPosition;
tileTmp.ActivateRandomObstacle();
spawnedTiles.Add(tileTmp);
}
if (gameOver || !gameStarted)
{
if (Input.GetKeyDown(KeyCode.Space))
{
if (gameOver)
{
//Restart current scene
Scene scene = SceneManager.GetActiveScene();
SceneManager.LoadScene(scene.name);
}
else
{
//Start the game
gameStarted = true;
}
}
}
}
void OnGUI()
{
if (gameOver)
{
GUI.color = Color.red;
GUI.Label(new Rect(Screen.width / 2 - 100, Screen.height / 2 - 100, 200, 200), "Game Over\nYour score is: " + ((int)score) + "\nPress 'Space' to restart");
}
else
{
if (!gameStarted)
{
GUI.color = Color.red;
GUI.Label(new Rect(Screen.width / 2 - 100, Screen.height / 2 - 100, 200, 200), "Press 'Space' to start");
}
}
GUI.color = Color.green;
GUI.Label(new Rect(5, 5, 200, 25), "Score: " + ((int)score));
}
}
- SC_PlatformTile スクリプトを "TilePrefab" オブジェクトにアタッチします
- "Obstacle1"、"Obstacle2"、および "Obstacle3" オブジェクトを Obstacles 配列に割り当てます
開始点と終了点については、プラットフォームの開始点と終了点にそれぞれ配置される 2 つのゲームオブジェクトを作成する必要があります。
- SC_PlatformTile で開始点と終了点の変数を割り当てる
- "TilePrefab" オブジェクトをプレハブに保存し、シーンから削除します
- 新しいゲームオブジェクトを作成して呼び出します "_GroundGenerator"
- SC_GroundGenerator スクリプトを "_GroundGenerator" オブジェクトにアタッチします
- メインカメラの位置を (10, 1, -9) に変更し、その回転を (0, -55, 0) に変更します。
- 新しいゲームオブジェクトを作成し、"StartPoint" という名前を付け、位置を (0, -2, -15) に変更します。
- "_GroundGenerator" オブジェクトを選択し、SC_GroundGenerator で Main Camera、Start Point、および Tile Prefab 変数を割り当てます。
ここで [再生] を押して、プラットフォームがどのように動くかを観察してください。プラットフォーム タイルがカメラ ビューから消えるとすぐに、ランダムな障害物がアクティブになって最後まで戻り、無限レベルの錯覚を作り出します (0:11 までスキップ)。
カメラはビデオと同じように配置する必要があり、プラットフォームがカメラに向かってカメラの後ろに移動します。そうしないと、プラットフォームが繰り返されません。
ステップ 2: プレーヤーを作成する
プレイヤーのインスタンスは、ジャンプやしゃがむ機能を持つコントローラーを使用する単純な球体になります。
- 新しい球体 (ゲームオブジェクト -> 3D オブジェクト -> 球体) を作成し、その球体コライダー コンポーネントを削除します。
- 以前に作成した "RedMaterial" をそれに割り当てます
- 新しいゲームオブジェクトを作成して呼び出します "Player"
- "Player" オブジェクト内で球を移動し、その位置を (0, 0, 0) に変更します。
- 新しいスクリプトを作成し、"SC_IRPlayer" という名前を付け、その中に以下のコードを貼り付けます。
SC_IRPlayer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class SC_IRPlayer : MonoBehaviour
{
public float gravity = 20.0f;
public float jumpHeight = 2.5f;
Rigidbody r;
bool grounded = false;
Vector3 defaultScale;
bool crouch = false;
// Start is called before the first frame update
void Start()
{
r = GetComponent<Rigidbody>();
r.constraints = RigidbodyConstraints.FreezePositionX | RigidbodyConstraints.FreezePositionZ;
r.freezeRotation = true;
r.useGravity = false;
defaultScale = transform.localScale;
}
void Update()
{
// Jump
if (Input.GetKeyDown(KeyCode.W) && grounded)
{
r.velocity = new Vector3(r.velocity.x, CalculateJumpVerticalSpeed(), r.velocity.z);
}
//Crouch
crouch = Input.GetKey(KeyCode.S);
if (crouch)
{
transform.localScale = Vector3.Lerp(transform.localScale, new Vector3(defaultScale.x, defaultScale.y * 0.4f, defaultScale.z), Time.deltaTime * 7);
}
else
{
transform.localScale = Vector3.Lerp(transform.localScale, defaultScale, Time.deltaTime * 7);
}
}
// Update is called once per frame
void FixedUpdate()
{
// We apply gravity manually for more tuning control
r.AddForce(new Vector3(0, -gravity * r.mass, 0));
grounded = false;
}
void OnCollisionStay()
{
grounded = true;
}
float CalculateJumpVerticalSpeed()
{
// From the jump height and gravity we deduce the upwards speed
// for the character to reach at the apex.
return Mathf.Sqrt(2 * jumpHeight * gravity);
}
void OnCollisionEnter(Collision collision)
{
if(collision.gameObject.tag == "Finish")
{
//print("GameOver!");
SC_GroundGenerator.instance.gameOver = true;
}
}
}
- SC_IRPlayer スクリプトを "Player" オブジェクトにアタッチします (Rigidbody と呼ばれる別のコンポーネントが追加されていることがわかります)
- BoxCollider コンポーネントを "Player" オブジェクトに追加します
- "Player" オブジェクトを "StartPoint" オブジェクトの少し上、カメラの正面に配置します。
Play を押し、W キーを使用してジャンプし、S キーを使用してしゃがみます。目的は、赤い障害物を避けることです。
この Horizon Bending Shader を確認してください。