Unity 无尽跑者教程

在电子游戏中,无论世界有多大,它总有尽头。但有些游戏试图模拟无限世界,此类游戏属于Endless Runner 类别。

Endless Runner 是一种玩家在不断前进的同时收集积分并躲避障碍的游戏。主要目标是在不落入或碰撞障碍物的情况下到达关卡终点,但通常情况下,关卡会无限重复,逐渐增加难度,直到玩家与障碍物碰撞。

地铁跑酷游戏玩法

考虑到即使是现代计算机/游戏设备的处理能力也有限,不可能创造一个真正无限的世界。

那么有些游戏是如何营造出无限世界的幻觉的呢?答案是通过重用构建块(也称为对象池),换句话说,一旦块移到相机视图的后面或之外,它就会移到前面。

要在 Unity 中制作无尽奔跑游戏,我们需要制作一个带有障碍物和玩家控制器的平台。

第 1 步:创建平台

我们首先创建一个平铺平台,稍后将其存储在 Prefab 中:

  • 创建一个新的 GameObject 并调用它 "TilePrefab"
  • 创建新的立方体(游戏对象 -> 3D 对象 -> 立方体)
  • 将立方体移动到 "TilePrefab" 对象内,将其位置更改为 (0, 0, 0),并缩放到 (8, 0.4, 20)

  • 您可以选择通过创建额外的立方体来将 Rails 添加到侧面,如下所示:

对于障碍物,我将提供 3 种障碍物变体,但您可以根据需要制作任意数量的障碍物:

  • 在 "TilePrefab" 对象内创建 3 个游戏对象,并将它们命名为 "Obstacle1"、"Obstacle2" 和 "Obstacle3"
  • 对于第一个障碍物,创建一个新的立方体并将其移动到 "Obstacle1" 对象内
  • 将新立方体缩放到与平台相同的宽度并降低其高度(玩家需要跳跃才能避开这个障碍)
  • 创建一个新的Material,命名为"RedMaterial",并将其颜色更改为Red,然后将其分配给Cube(这只是为了将障碍物与主平台区分开来)

  • 对于 "Obstacle2" 创建几个立方体并将它们放置为三角形,在底部留下一个开放空间(玩家需要蹲下以避免这个障碍)

  • 最后,"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" 对象保存到 Prefab 并将其从场景中删除
  • 创建一个新的 GameObject 并调用它 "_GroundGenerator"
  • 将 SC_GroundGenerator 脚本附加到 "_GroundGenerator" 对象
  • 将主摄像机位置更改为 (10, 1, -9) 并将其旋转更改为 (0, -55, 0)
  • 创建一个新的 GameObject,将其命名为 "StartPoint" 并将其位置更改为 (0, -2, -15)
  • 选择 "_GroundGenerator" 对象并在 SC_GroundGenerator 中分​​配主摄像机、起始点和平铺预制件变量

现在按“播放”并观察平台如何移动。一旦平台图块离开摄像机视野,它就会移回末端,并激活随机障碍物,从而产生无限关卡的错觉(跳至 0:11)。

相机的放置方式必须与视频类似,因此平台要朝向相机并位于相机后面,否则平台将不会重复。

Sharp Coder 视频播放器

第 2 步:创建播放器

玩家实例将是一个简单的球体,使用具有跳跃和蹲伏能力的控制器。

  • 创建一个新的球体(GameObject -> 3D Object -> Sphere)并删除其 Sphere Collider 组件
  • 将之前创建的 "RedMaterial" 分配给它
  • 创建一个新的 GameObject 并调用它 "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键蹲伏。目标是避免红色障碍:

Sharp Coder 视频播放器

检查这个地平线弯曲着色器

来源
📁EndlessRunner.unitypackage26.68 KB
推荐文章
在 Unity 中创建 2D 打砖块游戏
在 Unity 中创建滑动益智游戏
如何在 Unity 中制作一款受 Flappy Bird 启发的游戏
Unity 中的迷你游戏 | 立方体避免
Unity 中的三消益智游戏教程
农场僵尸 | 在 Unity 中制作 2D 平台游戏
Unity 中的迷你游戏 | 飞扬的立方体