此前为大家分享了使用Unity创建3D无尽跑酷游戏的上半部分,今天这篇文章将继续分享下半部分。

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

关卡

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

Straight Paths Level

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

183155gvhnu4z47grg88zn.png
Straight Paths Level中游戏对象的层级结构

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


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

183156iewewo3kwirgktir.png
当玩家与PathSpawnCollider发生碰撞时,在NewPathSpawn位置生成新路径
183156kermrmrowsx4kwsv.png
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++) { //dont instantiate if theres 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。

183157le91jmqbtbqb1oq0.png
Rotated path关卡的层级结构
PathSpawnCollider用作生成下一段路径和RedBorder预制件的触发器。SwipeCollider是唯一允许玩家进行滑动操作的地方。SpawnPoints就是糖果及障碍物的生成点。

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

183157w9yj7kfp7ez4yb1k.png
NewPathSpawnPoint左边
183158cfv22ffgcagv57e0.png
NewPathSpawnPoint右边
183159x544rb4661n9nz2b.png
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的死亡动画动作等,希望大家可以自行发挥。

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


相关阅读:
使用Unity创建3D无尽跑酷游戏(上)


原文连接:Creating an infinite 3D runner game in Unity (like Temple Run, Subway Surfers), part 2
原文作者:Dimitris
转载请注明来源:Unity官方中文社区(forum.china.unity3d.com)。请勿私自更改任何版权说明信息。 Unity, 教程, 新手, 进阶, 3D游戏 本主题由 admin 于 2018-1-19 02:52 删除回复锐亚教育

锐亚教育 锐亚科技 unity unity教程