喵的Unity游戏开发之路 - 生成区:各色场景
如果丢失格式、图片或视频,请查看原文:https://mp.weixin.qq.com/s/uGWQhm-lHkz7zEuxy7Pr2g
很多童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始学。为此我们精选了一套国外优秀的Unity3D游戏开发教程,翻译整理后放送给大家,教您从零开始一步一步掌握Unity3D游戏开发。 本文不是广告,不是推广,是免费的纯干货!本文全名:喵的Unity游戏开发之路 - 对象管理 - 生成区:各色场景
创建一个生成区域并对其进行转换。
使用小控件可视化生成区域。
每个场景支持不同的生成区域。
连接来自不同场景的对象。
创建多种生成区类型。
这是有关对象管理的系列教程中的第五篇。这是关于使对象以更多不同的模式生成,每个关卡均可配置。
本教程使用Unity 2017.4.4f1制作。
通过生成小形状来创建大形状。
产生点
我们的简单游戏是生成随机形状。每种形状的材料和颜色都是随机选择的,其位置,旋转和比例也是如此。尽管生成点是随机的,但它们被约束在以世界原点为中心的半径为5个单位的球形区域中。生成足够多的对象后,它们将形成可识别的球体。这是我们已硬编码到游戏中的衍生区域。
我们不必将自己限制在一个生成区。我们可以使形状以不同的配置生成。为此,我们必须用可配置的生成区域替换固定代码。
生成区域组件
创建一个新的SpawnZone
组件类型。它的唯一目的是提供生成点,因此为其提供Vector3 SpawnPoint
属性。这提供了一种获取点的方法,而无需设置它们,因此只需要一个get
块即可。这使它成为仅具有getter或readonly属性。我们将首先返回半径为5个单位的球体内的随机点。
-
using UnityEngine;public class SpawnZone : MonoBehaviour { public Vector3 SpawnPoint { get { return Random.insideUnitSphere * 5f; } }}
将Spawn Zone游戏对象添加到主场景并将新组件附加到主场景。现在,我们在游戏中有一个生成区域,但是现在还没有使用它。
使用区域
下一步是
Game
从单独的生成区域检索其生成点。为此添加一个公共字段,并使用它CreateShape
来生成派生点。 -
-
public SpawnZone spawnZone;
-
…
-
void CreateShape () {
Shape instance = shapeFactory.GetRandom();
Transform t = instance.transform;
//t.localPosition = Random.insideUnitSphere * 5f;
t.localPosition = spawnZone.SpawnPoint;
…
}
通过检查器连接生成区域。尽管游戏的行为仍然相同,但它现在依赖于Spawn Zone对象。
转变区域
因为生成区域是游戏对象的一部分,所以我们可以将其移动。要影响生成点,请将对象的位置添加到随机点。通过使用组件Transform的
position
属性而不是localPosition
,可以使生成区域成为另一个对象的子级。这样,可以将生成区域附加到其他可能正在移动的区域。 -
-
public Vector3 SpawnPoint {
get {
return Random.insideUnitSphere * 5f+ transform.position;
}
}
我们可以更进一步,将游戏对象层次结构的整个转换应用于生成点。然后,我们还可以旋转和缩放区域。为此,请使用随机点作为参数来调用区域
Transform
组件的TransformPoint方法。现在,我们可以取消乘以五,而通过设置对象的比例来控制区域的半径。 -
//return Random.insideUnitSphere * 5f + transform.position;
return transform.TransformPoint(Random.insideUnitSphere);
通过使用不均匀的比例,这也可以使球体变形。
仅表面
我们不必在球体半径范围内选择生成点。通过使用
Random.onUnitSphere
代替Random.insideUnitSphere也可以在球体的表面上获得一个点。通过在区域中添加切换字段surfaceOnly,使该选项成为可能。 -
-
[
bool surfaceOnly;
-
public Vector3 SpawnPoint {
get {
return transform.TransformPoint(
surfaceOnly ? Random.onUnitSphere : Random.insideUnitSphere
);
}
}
仅在表面上产生才可以使球体的形状更加明显。
区域可视化
现在可以调整生成区域了,如果可以看到其形状而不必生成很多点,将很有帮助。通过向SpawnZone中添加
void OnDrawGizmos
方法,我们可以在场景视图中绘制视觉辅助。这是一种特殊的Unity方法,每次绘制场景窗口时都会调用该方法。在OnDrawGizmos内部,调用
Gizmos.DrawWireSphere
以绘制一个球体的线表示,该球表示三个圆。我们必须为其提供位置和半径,我们将使用零向量和1来描述单位球面。 -
-
void OnDrawGizmos () {
Gizmos.DrawWireSphere(Vector3.zero, 1f);
}
我们还能在游戏窗口中看到小玩意吗?
是的,在游戏窗口工具栏的右侧有一个Gizmos选项。这仅适用于编辑器,小控件不包含在构建中。
默认的Gizmo颜色为白色,但是可以通过更改
Gizmos.color
属性使用其他颜色。这有助于将其与其他小玩意区分开。让我们将青色用于我们的生成区gizmo。 -
void OnDrawGizmos () {
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(Vector3.zero, 1f);
}
目前,我们的线球体是在原点绘制的,半径为1,与区域的变换无关。默认情况下,小控件在世界空间中绘制。要更改此设置,我们必须通过
Gizmos.matrix
属性指定应使用哪个转换矩阵。我们可以通过区域Transform
组件的localToWorldMatrix属性获得所需的矩阵。 -
void OnDrawGizmos () {
Gizmos.color = Color.cyan;
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.DrawWireSphere(Vector3.zero, 1f);
}
我们是否需要重置Gizmo的颜色和矩阵?
不,它们会自动重置。
每个关卡一个区域
现在我们可以配置生成区域了,下一步是使每个关卡都有自己的生成区域。
迁移到其他场景
通过在层次结构窗口中拖动,我们可以在打开的场景之间移动对象。使用Spawn Zone对象执行此操作,将其从Main Scene移到Level 1。
该区域现在是关卡的一部分,但是Unity警告我们它检测到跨场景引用。不幸的是,由于场景可能不会同时打开,因此无法保存不同场景中对象之间的直接引用。当前,Game的生成区域参考指示场景不匹配,保存或播放后将清除它。
游戏需要对生成区域的引用,但是由于我们现在将其存储在其他场景中,因此无法保存此类引用。然后,最简单的更改将是
spawnZone
用公共财产替换该字段。让我们显式命名为SpawnZoneOfLevel
,以表明它不是主场景的一部分,而是关卡场景的一部分。 -
-
//public SpawnZone spawnZone;
public SpawnZone SpawnZoneOfLevel { get; set; }
-
…
-
void CreateShape () {
Shape instance = shapeFactory.GetRandom();
Transform t = instance.transform;
t.localPosition =SpawnZoneOfLevel.SpawnPoint;
…
}
寻找游戏
有人需要设置
SpawnZoneOfLevel
属性。仅在加载关卡之后才能执行此操作。实际上,每次加载关卡时都必须执行此操作,因为每个关卡必须具有自己的生成区域。问题是谁对此负责。尽管
Game
控制关卡的加载,但它不能直接访问关卡内容。它需要检索关卡场景的根对象,然后搜索正确的对象。另外,我们可以让该关卡负责在加载SpawnZoneOfLevel属性之后设置该属性。来做吧。为了进行SpawnZoneOfLevel设置,关卡必须首先以某种方式获取对主场景中Game对象的引用。只有一个
Game
实例,因此我们可以将对它的引用存储在Game
类的静态Instance属性中。每个人都可以获取此参考,但只能Game
设置它。这是单例设计模式的一个示例。 -
-
public static Game Instance { get; private set; }
当我们的游戏实例唤醒时,它应该将自己分配给该
Instance
属性。对象可以通过this
关键字获得对自身的引用。 -
void Start () {
Instance = this;
…
}
我们不应该强制只存在一个单例实例吗?
通常,这可能是个好主意。但是在我们的特定情况下,我们Game在主场景中只有一个组件实例,该实例仅被加载一次,而从未卸载。如果不是这种情况,那么我们要么在编辑场景时犯了一个错误,要么不只一次加载主场景。
虽然这在进入播放模式和构建时有效,但是static属性不会在编辑器中处于播放模式的编译之间持久存在,因为它不是Unity游戏状态的一部分。为了从重新编译中恢复,我们也可以在
OnEnable
方法中设置属性。每次启用组件时,Unity都会调用该方法,每次重新编译后也会发生这种情况。 -
void OnEnable() {
Instance = this;
}
OnEnable何时准确调用?
每次启用禁用的组件时都会调用它。如果在游戏模式下进行重新编译,则首先禁用所有活动组件,然后保存游戏状态,进行编译,恢复游戏状态,并再次启用先前的活动组件。是的,还有一种OnDisable方法,实际上它是在重新编译之前被调用的。
同样,除非组件以禁用状态保存,否则在组件OnEnable的Awake方法之后立即被调用。稍后我们将利用这个事实。
请注意,OnEnable在更改关卡后也会调用它,因为Game在加载关卡时我们暂时将其禁用。这不是问题,因为我们最终用相同的引用替换了旧的引用。
由于我们现在依赖于其他代码来访问
Game
,因此正确隐藏其配置字段是一个好主意。与其使用公共字段,不如使用序列化的私有字段,就像我们已经对factory和spawn区域所做的那样。 -
//public ShapeFactory shapeFactory;
[SerializeField] ShapeFactory shapeFactory;
…
我仅显示了shapeFactory的更改,但对关键配置字段,存储和关卡计数进行了相同的更改。通常,属性放置在它们适用的任何内容之上,但是由于存在很多字段,因此在这种情况下,我将它们放在同一行上。
游戏关卡
要使关卡连接到生成区域,我们需要添加代码来执行此操作。尽管我们可以向中添加此功能
SpawnZone
,但理想情况下,该类应该专用于生成区域,而不负责其他任何事情。它不需要了解游戏的其余部分。因此,我们将创建一个新的GameLevel
组件类型来进行设置。它需要知道要使用哪个生成区域,因此为其提供一个配置字段。然后,当它变为活动状态时,使其获取全局可用Game.Instance
属性。它可以用来设置Game的SpawnZoneOfLevel
属性。我们将在Start中建立连接,因此它将在加载关卡之后发生。另外,在编辑器中进入播放模式时,将首先加载当前活动的场景。通过等到Start,即使主场景不是活动场景,我们也保证
Game.OnEnable
已经执行并设置了Game.Instance。 -
-
using UnityEngine;
-
public class GameLevel : MonoBehaviour {
-
[
SpawnZone spawnZone;
-
void Start () {
Game.Instance.SpawnZoneOfLevel = spawnZone;
}
}
将具有此组件的游戏对象添加到关卡场景并将其连接到生成区域。
这意味着“ 游戏关卡”对象保存了对“生成区域”对象的保存引用 ,这是允许的,因为两者都存在于同一场景中。当游戏在玩时,“ 游戏关卡”将通过Game.Instance获取对Game 的临时引用,它用于为Game提供对Spawn Zone的临时引用。因此,
GameLevel
将事情联系起来,并实现到Game
和SpawnZone
。反过来,Game
仅实现SpawnZone
。最后,SpawnZone
完全不了解其他两个。这是设计依赖项的最佳方法吗?
没有通用的最佳设计方法。在我们的案例中,我们调整了的现有spawnZone引用Game并将其作为属性,引入了GameLevel对象以连接事物。我们还可以朝另一个方向前进,并通过静态属性GameLevel将其启用,该属性将用于Game到达生成区域。或者提供Game一个GameLevel属性而不是一个属性SpawnZone,通过该属性可以间接访问生成区域。
当前方法行之有效,因为GameLevel的唯一目的是将生成区域连接到游戏。如果GameLevel获得更多的责任或联系,我们可能必须调整设计。此类代码更改是开发过程的一部分,因此我也将其包含在我的教程中。
同时为2关卡提供自己的Spawn Zone和Game Level对象。游戏将像以前一样运行,但是现在您可以按关卡调整生成区域。
区域类型
由于生成区域具有自己的类,因此现在可以对其进行扩展并创建其他区域类型。例如,除了球体区域,我们还可以添加对立方体区域的支持。
抽象生成区
无论特定的生成区域类型如何,它们的通用功能都是提供生成点。在
SpawnZone
类定义此基础上。删除所有特定于球体区域的代码,仅保留该SpawnPoint
属性的默认定义。public class SpawnZone : MonoBehaviour {
//[SerializeField]
//bool surfaceOnly;
public Vector3 SpawnPoint {get; }
// get {
// return transform.TransformPoint(
// surfaceOnly ? Random.onUnitSphere : Random.insideUnitSphere
// );
// }
//}
//void OnDrawGizmos () {
// …
//}
}
这定义了生成区域的抽象功能。为了使之明确,请在类上标记
abstract
关键字,并在属性上标记。 -
-
-
public abstract class SpawnZone : MonoBehaviour {
-
public abstract Vector3 SpawnPoint { get; }
}
SpawnZone
现在是抽象类型,无法创建其实例的类。结果,Unity将抱怨我们的生成区域组件已失效。我们必须将它们替换为特定的子类。球体区
首先,我们将重新创建球形的生成区组件,但现在将其作为
SphereSpawnZone
可扩展的新类型SpawnZone
。与旧代码的唯一不同之处在于,我们必须指出它通过SpawnPoint
具体的实现覆盖了abstract 属性。必须通过向其添加override
关键字来使其明确。 -
-
-
using UnityEngine;
-
public classSphereSpawnZone:SpawnZone{
-
[
bool surfaceOnly;
-
publicoverrideVector3 SpawnPoint {
get {
return transform.TransformPoint(
surfaceOnly ? Random.onUnitSphere : Random.insideUnitSphere
);
}
}
-
void OnDrawGizmos () {
Gizmos.color = Color.cyan;
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.DrawWireSphere(Vector3.zero, 1f);
}
}
调整1关卡场景的Spawn Zone对象,以使其使用此组件。还恢复游戏关卡的参考,当SpawnZone成为无效组件时会丢失。2关卡也需要修复。
立方体区域
接下来,还要创建一个名为CubeSpawnZone的多维数据集生成区域类型。从生成区域的最小功能开始,生成区域只是
SpawnPoint
返回零向量的属性。 -
-
-
using UnityEngine;
-
public class CubeSpawnZone : SpawnZone {
-
public override Vector3 SpawnPoint {
get {
return Vector3.zero;
}
}
}
没有方便的
Random.insideUnitCube
属性,因此我们必须自己构造随机点。单位立方体以原点为中心,边长为1个单位。因此,它的体积在每个维度的两个方向上延伸了一半。为了在该空间内获得一个随机点,我们可以分别调用Random.Range(-0.5f, 0.5f)
三个向量坐标中的每一个,然后转换结果点。 -
-
public override Vector3 SpawnPoint {
get {
Vector3 p;
p.x = Random.Range(-0.5f, 0.5f);
p.y = Random.Range(-0.5f, 0.5f);
p.z = Random.Range(-0.5f, 0.5f);
returntransform.TransformPoint(p);
}
}
有一种
Gizmos.DrawWireCube
方法,因此我们可以使用它来显示立方体区域的Gizmo。它的第一个参数是立方体的中心,而第二个参数是其边缘长度。 -
void OnDrawGizmos () {
Gizmos.color = Color.cyan;
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.DrawWireCube(Vector3.zero, Vector3.one);
}
我们还要为立方体区域添加仅表面选项。启用后,我们必须调整生成点,使其最终出现在多维数据集的一个面上。我们可以通过在多维数据集内的一个随机点开始然后沿一个轴移动它直到与一个面对齐来做到这一点。轴的索引可以随机选择。
-
-
[
bool surfaceOnly;
-
public override Vector3 SpawnPoint {
get {
Vector3 p;
p.x = Random.Range(-0.5f, 0.5f);
p.y = Random.Range(-0.5f, 0.5f);
p.z = Random.Range(-0.5f, 0.5f);
if (surfaceOnly) {
int axis = Random.Range(0, 3);
}
return transform.TransformPoint(p);
}
}
可以像使用数组一样使用此索引访问Vector3值,获取或设置其对应的坐标。这样,我们可以使该分量与沿轴的正或负面对齐。我们可以使用原始坐标来决定要选择哪一侧。如果是负数,我们将其移至负数,否则移至正数。这会将点移动到两个面中最近的一个。
-
-
int axis = Random.Range(0, 3);
p[axis] = p[axis] < 0f ? -0.5f : 0.5f;
复合区
最后,让我们创建一个复合的生成区域类型,它由其他生成区域的集合定义。这样就可以创建更复杂的区域,该区域由多个单独的区域(可能是重叠的区域)组成。
添加一个
CompositeSpawnZone
类,让它扩展SpawnZone
,并给它一个spawnZones
数组字段。 -
-
using UnityEngine;
-
public class CompositeSpawnZone : SpawnZone {
-
[
SpawnZone[] spawnZones;
}
它的
SpawnPoint
属性从zones数组中选择一个随机索引,然后使用该区域的属性获取派生点。 -
-
public override Vector3 SpawnPoint {
get {
int index = Random.Range(0, spawnZones.Length);
return spawnZones[index].SpawnPoint;
}
}
我们不应该检查数组是否为空吗?
你可以那样做。您还可以检查数组是否存在,因为null如果在播放模式下创建了组件,则将检查该数组。但是我们的想法是,我们在编辑模式下设计生成区域,并确保它们在进入播放模式或进行构建之前是正确的。因此,当复合生成区域为空时,我们不必担心该怎么办。保留一个空将是一个错误,并且在尝试检索不存在的数组索引时,Unity将记录一个错误。
创建一个3关卡场景并增加关卡数,
Game
以尝试我们的新复合生成区域。确保它还具有一个Game Level对象,该对象获得对生成区域的引用,烘烤其照明并将其包括在构建设置中。为了使复合区域正常工作,我们必须创建更多其他不同类型的区域。例如,创建两个球体区域和两个立方体区域,分别是一个实体和仅一个曲面版本,因此您可以同时看到它们。将这四个区域拖到复合区域的“生成区域”阵列字段上。一种快速的方法是在选中复合区域时锁定检查器,方法是单击检查器窗口右上方的锁定图标。然后选择其他四个区域,并将整个选择拖动到阵列上。之后,解锁检查器。
属于复合区域的区域可以在同一场景中的任何位置。它们不必是复合区域对象的子代,但是如果进行转换,则复合区域将影响它们。
甚至可以将多个生成区域组件添加到同一个游戏对象,但是您不能单独转换它们。
除了球形,立方体和复合区域外,您还可以创建更多的生成区域类型。我已经在本教程中包括了最直接的内容。此外,还有仅用于立方体和球体的便捷小控件。您必须更具创造力才能显示其他形状的小物件。
下一个教程是 更多游戏状态。
资源库(Repository)
https://bitbucket.org/catlikecodingunitytutorials/object-management-05-spawn-zones
往期精选
Unity3D游戏开发中100+效果的实现和源码大全 - 收藏起来肯定用得着
喵的Unity游戏开发之路 - 从入门到精通的学习线路和全教程
声明:发布此文是出于传递更多知识以供交流学习之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与我们联系,我们将及时更正、删除,谢谢。
原作者:Jasper Flick
原文:
https://catlikecoding.com/unity/tutorials/object-management/spawn-zones/
翻译、编辑、整理:MarsZhou
More:【微信公众号】 u3dnotes