使用Unity制作3D无尽跑酷游戏(下)

发表于2017-04-25
评论2 6.4k浏览
此前为大家分享了使用Unity创建3D无尽跑酷游戏的上半部分,今天这篇文章将继续分享下半部分。

下载示例工程:
littlehill,如果您要查看本帖隐藏内容请回复


关卡

本文将讲解如何构建关卡。先从“直线型道路”关卡开始。

Straight Paths Level

层级结构列表中的游戏对象:

 
Straight Paths Level中游戏对象的层级结构


所有的路径都是通过预制件(见上图蓝色的WideStraightPath)来复制的,直线型道路的路径要比旋转型道路更宽。场景中还有一些立方体状的3D对象(上图中的Cubes),作为路径的地板。另外还有一些简单的3D坐标,NewPathSpawn以及SpawnPoints。SpawnPoints用于指定生成新物体的位置,例如糖果、障碍物等。


为了节省内存资源,游戏不会在一开始就生成所有路径。实际上,游戏需要生成N+1条路径,N是当前Max所在的路径位置。可以利用一个简单的碰撞器BoxCollider,当Max与它发生碰撞, 则通过PathSpawnCollider脚本生成新路径。在直线型道路的关卡中,新生成的路径位于“NewPathSpawn”坐标,这个坐标就是当前路径的终点。

 
当玩家与PathSpawnCollider发生碰撞时,在NewPathSpawn位置生成新路径

 
NewPathSpawn的位置就是新路径的生成点

Character游戏对象下包含相机和Max模型,此外还有Character Controller 组件和CharacterSidewaysMovement脚本。通过该组件和脚本的控制,相机会始终保持在Max上方的位置,这是第三人称视角游戏中的常见角色控制方式。

Canvas包含两个文本对象。Directional Light就是很简单的平行光光照,ScriptHolder对象包含了本教程上半部分介绍的GameManager单例脚本。

PathSpawnCollider类——路径生成器

此脚本适用于两种关卡,主要功能是根据Max当前奔跑位置来生成下一段路径。

[C#] 纯文本查看 复制代码
public class PathSpawnCollider : MonoBehaviour {
 
    public float positionY = 0.81f;
    public Transform[] PathSpawnPoints;
    public GameObject Path;
    public GameObject DangerousBorder;
 
}


脚本首先设置了几个公共变量,可以在Unity编辑器中设置。
  • PositionY:用于将RedBorder放置到正确的Y轴位置。
  • 数组PathSpawnPoints:用于保存下一段路径及边界的生成坐标。在“直线型道路”关卡中,数组内只有一个元素。但“旋转型道路“关卡中,该数组需要3个元素,其中一个是新路径的位置,另外两个是RedBorder障碍物(Max一旦碰撞到它,则游戏失败)。
  • Path:物体包含路径预制件。
  • 数组DangerousBorder:包含了用于“旋转型道路”关卡的RedBorder预制件,在“直线型道路”关卡中为null。


[C#] 纯文本查看 复制代码
void OnTriggerEnter(Collider hit)
  {
      //player has hit the collider
      if (hit.gameObject.tag == Constants.PlayerTag)
      {
          //find whether the next path will be straight, left or right
          int randomSpawnPoint = Random.Range(0, PathSpawnPoints.Length);
          for (int i = 0; i < PathSpawnPoints.Length; i++)
          {
              //instantiate the path, on the set rotation
              if (i == randomSpawnPoint)
                  Instantiate(Path, PathSpawnPoints[i].position, PathSpawnPoints[i].rotation);
              else
              {
                  //instantiate the border, but rotate it 90 degrees first
                  Vector3 rotation = PathSpawnPoints[i].rotation.eulerAngles;
                  rotation.y += 90;
                  Vector3 position = PathSpawnPoints[i].position;
                  position.y += positionY;
                  Instantiate(SpawnBorder, position, Quaternion.Euler(rotation));
              }
          }
  
      }
  }


当Max与PathSpawnCollider对象碰撞时,游戏会随机决定下一段路径是往左、往右还是笔直前行。在“直线型道路”关卡中,PathSpawnPoints数组只需一个路径对象(对应直行位置),将randomSpawnPoint参数设置成0,则会笔直生成下一段路径。在“旋转型道路”关卡中,在选定位置生成下一段路径,并在另外2个位置生成RedBorder障碍物,将其旋转90度以正确摆放。


Stuff Spawner类——道具生成器

Stuff Spawner脚本用于生成糖果、障碍物等游戏道具,两个关卡均有用到。首先声明一些可以在Unity编辑器中设置的公共变量。

[C#] 纯文本查看 复制代码
//points where stuff will spawn :)
public Transform[] StuffSpawnPoints;
//meat gameobjects
public GameObject[] Bonus;
//obstacle gameobjects
public GameObject[] Obstacles;
  
public bool RandomX = false;
public float minX = -2f, maxX = 2f;


  • StuffSpawnPoints:包含所有要被实例化物体的位置(糖果或者障碍物)。
  • 数组Bonus:包含所有的糖果预制件。
  • 数组Obstacles:中包含所有的障碍物预制件。
  • RandomX:该变量只有在“直线型道路”关卡中才会是true。它会随机在X轴的位置([minx, maxX]之间)生成各类糖果和障碍物,以显得每段路径都不一样。


[C#] 纯文本查看 复制代码
void CreateObject(Vector3 position, GameObject prefab)
  {
      if (RandomX) //true on the straight paths level, false on the rotated one
          position += new Vector3(Random.Range(minX, maxX), 0, 0);
  
      Instantiate(prefab, position, Quaternion.identity);
  }


CreateObject方法用于在选定位置实例化一个新的预制件,并且判定是否要沿X轴方向移动一段距离(仅“直线型道路”关卡)。

[C#] 纯文本查看 复制代码
void Start()
   {
       bool placeObstacle = Random.Range(0, 2) == 0; //50% chances
       int obstacleIndex = -1;
       if (placeObstacle)
       {
           //select a random spawn point, apart from the first one
           //since we do not want an obstacle there
           obstacleIndex = Random.Range(1, StuffSpawnPoints.Length);
  
           CreateObject(StuffSpawnPoints[obstacleIndex].position, Obstacles[Random.Range(0, Obstacles.Length)]);
       }
  
       for (int i = 0; i < StuffSpawnPoints.Length; i++)
       {
           //don't instantiate if there's an obstacle
           if (i == obstacleIndex) continue;
           if (Random.Range(0, 3) == 0) //33% chances to create candy
           {
               CreateObject(StuffSpawnPoints[i].position, Bonus[Random.Range(0, Bonus.Length)]);
           }
       }
  
   }


Start方法

决定是否在当前路径创建障碍物(50%概率)。如果判定通过,它将随机选取一个障碍物预制件并创建。

StuffSpawnPoints数组中的其它位置(即非obstacleIndex的位置),会决定是否创建糖果(33%的概率)。如果判定通过,它会随机在Bonus数组中选取一个预制件并创建。

CharacterSidewaysMovement类——角色横向移动控制类

该脚本仅用于在“直线型道路”关卡中移动Max。Max带有CharacterController组件。如果想了解如何对带有CharacterController组件的游戏对象进行移动控制,请查阅Unity文档。

[C#] 纯文本查看 复制代码
private Vector3 moveDirection = Vector3.zero;
public float gravity = 20f;
private CharacterController controller;
private Animator anim;
  
private bool isChangingLane = false;
private Vector3 locationAfterChangingLane;
//distance character will move sideways
private Vector3 sidewaysMovementDistance = Vector3.right * 2f;
  
public float SideWaysSpeed = 5.0f;
  
public float JumpSpeed = 8.0f;
public float Speed = 6.0f;
//Max gameobject
public Transform CharacterGO;
  
IInputDetector inputDetector = null;


声明一些变量,通过其名称就能很容易了解它们的用途。

[C#] 纯文本查看 复制代码
void Start()
 {
     moveDirection = transform.forward;
     moveDirection = transform.TransformDirection(moveDirection);
     moveDirection *= Speed;
  
     UIManager.Instance.ResetScore();
     UIManager.Instance.SetStatus(Constants.StatusTapToStart);
  
     GameManager.Instance.GameState = GameState.Start;
  
     anim = CharacterGO.GetComponent();
     inputDetector = GetComponent();
     controller = GetComponent();
 }


在Start中设置moveDirection向量,用于帮助Max以给定的速度直线前进。然后为组件的相关变量设置正确的值,并更改游戏状态以开始游戏。 

[C#] 纯文本查看 复制代码
private void CheckHeight()
{
    if (transform.position.y < -10)
    {
        GameManager.Instance.Die();
    }
}


因为Max是可以进行跳跃操作的,所以很有可能造成人物跳跃出路径之外。CheckHeight方法检测Max是否掉落到平台的高度之下,如果是,则游戏失败。

[C#] 纯文本查看 复制代码
private void DetectJumpOrSwipeLeftRight()
{
    var inputDirection = inputDetector.DetectInputDirection();
    if (controller.isGrounded && inputDirection.HasValue && inputDirection == InputDirection.Top && !isChangingLane)
    {
        moveDirection.y = JumpSpeed;
        anim.SetBool(Constants.AnimationJump, true);
    }
    else
    {
        anim.SetBool(Constants.AnimationJump, false);
    }


DetectJumpOrSwipeLeftRight方法首先检测玩家是否转向或跳跃(即是否按左、右或者上方向键)。如果游戏已经检测到玩家按上方向键,则再检测当前Max是否正在落地或切换路径(这里禁止2次跳跃)。在通过了这些检测之后,再设置moveDirection向量的y值,并播放Max的跳跃动画。

[C#] 纯文本查看 复制代码
if (controller.isGrounded && inputDirection.HasValue && !isChangingLane)
    {
        isChangingLane = true;
  
        if (inputDirection == InputDirection.Left)
        {
            locationAfterChangingLane = transform.position - sidewaysMovementDistance;
            moveDirection.x = -SideWaysSpeed;
        }
        else if (inputDirection == InputDirection.Right)
        {
            locationAfterChangingLane = transform.position + sidewaysMovementDistance;
            moveDirection.x = SideWaysSpeed;
        }
    }
}


然后,检测玩家是否左右滑动。如果Max当前不是跳跃状态,也未移动至道路边缘,则检测到左或右方向键输入后,让Max向左或右移动。接着,通过为moveDirection向量的x值加或减SideWaysSpeed变量值,来实现以上操作。在路径切换完成的同时,保存Max的最后位置。

接下来看看Update。这里通过switch语句处理3种不同的游戏状态。

[C#] 纯文本查看 复制代码
void Update()
    {
        switch (GameManager.Instance.GameState)
        {
            case GameState.Start:
                if (Input.GetMouseButtonUp(0))
                {
                    anim.SetBool(Constants.AnimationStarted, true);
                    var instance = GameManager.Instance;
                    instance.GameState = GameState.Playing;
  
                    UIManager.Instance.SetStatus(string.Empty);
                }
                break;


在Start状态中,游戏会检测玩家是否点击屏幕。如果检测到点击后,将AnimationStarted的值设为true,将Max的状态切换为run,此时游戏状态也会切换为Playing。

[C#] 纯文本查看 复制代码
case GameState.Playing:
    UIManager.Instance.IncreaseScore(0.001f);
  
    CheckHeight();
  
    DetectJumpOrSwipeLeftRight();
  
    //apply gravity
    moveDirection.y -= gravity * Time.deltaTime;
  
    if (isChangingLane)
    {
        if (Mathf.Abs(transform.position.x - locationAfterChangingLane.x) < 0.1f)
        {
            isChangingLane = false;
            moveDirection.x = 0;
        }
    }
  
    //move the player
    controller.Move(moveDirection * Time.deltaTime);
  
    break;


在Playing状态中,首先检查Max的Y方向位置。然后根据玩家的具体操作,控制Max左右移动或跳跃。如果Max正在切换路径,则判断Max当前的X轴位置,并对比切换路径的目标位置。如果两者之间相差不大,则表示Max成功完成了移动,系统可以安全地将isChangingLane参数设置成false并防止Max移动过头。最后,使用CharacterController的Move方法将Max移动到下一段路径。

[C#] 纯文本查看 复制代码
case GameState.Dead:
               anim.SetBool(Constants.AnimationStarted, false);
               if (Input.GetMouseButtonUp(0))
               {
                   //restart
                   SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
               }
               break;
           default:
               break;
       }
  
   }


Update方法的最后一部分在Max死亡时调用,即Dead状态。当Max死亡,系统会将AnimationStarted参数设置成false,并提示玩家点击屏幕重新开始游戏。

[C#] 纯文本查看 复制代码
public void OnControllerColliderHit(ControllerColliderHit hit)
   {
       //if we hit the left or right border
       if(hit.gameObject.tag == Constants.WidePathBorderTag)
       {
           isChangingLane = false;
           moveDirection.x = 0;
       }
   }

OnControllerHit仅用于“直线型道路”关卡。路径的左右边界设置标签为WidePathBorder,当Max与其产生碰撞,系统会判定转向结束,因为已经碰到边缘了。如果这里不做判断,Max会继续移动直至穿墙,这就会导致严重Bug。

Rotated path关卡

“旋转型道路”关卡与“直线型道路”关卡有些相似,但路径的预制件不一样。对比“直线型道路” 关卡,“旋转型道路”关卡中包含了更多的NewPathSpawns,以及一个SwipeCollider。

 
Rotated path关卡的层级结构

PathSpawnCollider用作生成下一段路径和RedBorder预制件的触发器。SwipeCollider是唯一允许玩家进行滑动操作的地方。SpawnPoints就是糖果及障碍物的生成点。

当Max进入PathSpawnCollider时,从数组NewPathSpawn的3个位置中随机选择一个位置,实例化一段新的路径。

 
NewPathSpawnPoint左边

 
NewPathSpawnPoint右边

 
NewPathSpawnPoint前方

在这三个位置中随机选择一个作为下一段路径的起点,并在其它两个位置生成RedBorder障碍物。

Stuff Spawner 与Path Spawn Collider

这些脚本都仅用于“直线型道路”关卡,在此不多做描述。

Swipe Collider类——滑动碰撞体

在“旋转型道路”关卡中,当玩家位于路径中途时不允许滑动,只有到达某个特定空间才可以通过滑动让Max转向。可以在场景中放置一个隐形碰撞体Swipe Collider,当Max与Swipe Collider对象进行碰撞时,玩家才能进行滑动动作。代码如下:

[C#] 纯文本查看 复制代码
public class SwipeCollider : MonoBehaviour
{
  
    // Use this for initialization
    void OnTriggerEnter(Collider hit)
    {
        if (hit.gameObject.tag == Constants.PlayerTag)
            GameManager.Instance.CanSwipe = true;
    }
  
    void OnTriggerExit(Collider hit)
    {
        if (hit.gameObject.tag == Constants.PlayerTag)
            GameManager.Instance.CanSwipe = false;
    }
}


SwipeCollider脚本只用于设置GameManager对象的公共变量,该变量控制Max是否可以左右滑动。当Max进入到碰撞体时执行OnTriggerEnter方法,当Max离开碰撞体时,执行OnTriggerExit方法。

CharacterRotateMovement类——角色转向移动控制类

该脚本与之前“直线型道路”关卡中的CharacterSidewaysMovement脚本非常相似,主要区别在于DetectJumpOrSwipeLeftRight方法,游戏通过Quaternion.AngleAxis来实现Max的转向。

[C#] 纯文本查看 复制代码
if (GameManager.Instance.CanSwipe && inputDirection.HasValue &&
         controller.isGrounded && inputDirection == InputDirection.Right)
        {
            transform.Rotate(0, 90, 0);
            moveDirection = Quaternion.AngleAxis(90, Vector3.up) * moveDirection;
            //allow the user to swipe once per swipe location
            GameManager.Instance.CanSwipe = false;
        }
        else if (GameManager.Instance.CanSwipe && inputDirection.HasValue &&
         controller.isGrounded && inputDirection == InputDirection.Left)
        {
            transform.Rotate(0, -90, 0);
            moveDirection = Quaternion.AngleAxis(-90, Vector3.up) * moveDirection;
            GameManager.Instance.CanSwipe = false;
        }


从阅读代码片段可以发现,这里之所以没有使用OOP的继承来创建所有的移动类,是因为这里涉及的移动比较简单。如果您游戏的移动机制比较复杂,建议使用继承会更加便于代码管理。

总结

以上示例虽然包含了3D无尽模式跑酷游戏的基本机制,但也还有一些可以继续改善的地方。例如,加入对象池、增加关卡难度、完善Max的死亡动画动作等,希望大家可以自行发挥。

关于本教程如有任何疑问,也请在下方留言提问。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引