目录
1 更多形状
1.1 立方体嵌入球
1.2 复合胶囊体
1.3 复合立方体
1.4 生成新的形状
1.5 配置要调整的Renderer
1.6 非同一颜色
1.7 保存所有的颜色
1.8 可选统一颜色
1.9 健壮的保存
2 第二个工厂
2.1 复合形状工厂
2.2 每个生成区分配工厂
2.3 用生成区取代配置
2.4 回收形状
2.5 保存原始工厂
收起
本文重点:
1、创建复合形状
2、每个形状支持多个颜色
3、为每个生成区选择工厂
4、保持对形状原始工厂的追踪
这是有关对象管理的系列教程中的第八篇。它介绍了与多个工厂合作的概念以及更复杂的形状。
本教程是CatLikeCoding 系列的一部分,原文地址见文章底部。“原创”标识意为原创翻译而非原创教程。
本教程使用Unity 2017.4.12f1制作。
(更多形状、更多工厂、更多变化)
1 更多形状
立方体,球体和胶囊并不是我们可以使用的唯三形状。我们可以导入任意的网格。同样,形状不必由单个对象组成,也可以具有自己的对象层次结构,并具有多个网格,动画,行为和其他内容。为了说明这一点,我们将通过组合多个默认网格来创建一些复合形状。
1.1 立方体嵌入球
我们先将一个立方体与一个球简单地组合在一起。创建一个立方体对象,然后创建一个均位于原点的球体。然后使球体成为立方体的子级。在默认比例下,球体隐藏在立方体内部。增大球体的比例,使其与立方体的面相交。比例为√2时,球体将接触立方体的边缘。使用较小的比例(如1.35)可使我们在立方体的每个面上产生凸起。
(立方体和球融合)
要将其变成合适的形状,请将Shape组件添加到根立方体对象中。再将两个对象的材质设置为所有其他形状使用的相同白色材质。然后将其变成预制件。
1.2 复合胶囊体
通过组合三个旋转的胶囊可以制成更复杂的形状。从默认胶囊开始,然后给它两个子胶囊。将子节点旋转90°,一个围绕其X轴旋转,另一个围绕其Z轴旋转。结果是沿主轴具有六个突起的圆形形状,有点像之前的形状,但它没有立方体。
(复合的胶囊体)
再次向根胶囊添加形状组件并设置材质,然后将其变为预制件。
1.3 复合立方体
对于最终的合成形状,我们执行相同操作,但现在使用一个立方体和两个立方体子节点。在这种情况下,请沿两个轴将子项旋转45°,一个轴XY,另一个轴YZ。这样就创建了立方体复合物变体之一,它是具有十字形挤压形状的复杂形状。
(复合的立方体)
将Shape组件添加到根立方体,并将其也转换为预制件。
1.4 生成新的形状
为了能够生成这些新形状,我们所要做的就是将它们添加到我们的工厂中。
(六种形状的工厂)
从现在开始,可以与旧形状一起生成新形状。但是它们看起来大多是白色的,因为只有具有Shape组件的根对象才具有随机的材质和颜色。子对象不受影响。
(新的复合对象大部分保留白色)
1.5 配置要调整的Renderer
要改变作为复合形状一部分的所有对象的颜色和材质,shape需要访问所有相关的MeshRenderer组件。为此,我们给它一个可配置数组。
现在,我们必须遍历所有形状的预制件,并手动包括所有受影响的渲染器。请注意,可以有目的的排除某些内容,因此形状的某些部分可以具有固定的材质。你可以将对象直接拖到数组上,Unity会将其转换为对其渲染器的引用。
(给复合胶囊材质设置Mesh renderer)
Shape唤醒时不再需要检索单个渲染器组件,因此可以删除meshRenderer字段和Awake方法。
在SetMaterial中,我们必须遍历所有渲染器并将其材质设置为提供的材质。
SetColor也是一样。
(复合形状正确的上色)
1.6 非同一颜色
现在,假设所有渲染器都被设置为受影响,我们最终得到颜色均匀的复合形状。但是,我们不必将自己限制为每种形状只有一种颜色。让我们使复合形状的每个部分都有其自己的颜色。
为了支持每个形状多种颜色,同时仍然能够正确保存它,我们必须将颜色字段替换为颜色数组。形状Awake时应创建该数组,其长度应与meshRenderers数组的长度相同。因此,我们再次需要一个Awake方法。
通过SetColor配置颜色时,还必须设置colors数组的所有元素。
但这仍然使所有颜色相同。要为每个渲染器支持不同的颜色,请添加一个变体SetColor方法,该方法仅调整通过index参数标识的单个颜色元素。
这需要外界知道多少种颜色,因此添加一个公共的ColorCount getter属性,该属性仅返回colors数组的长度。
1.7 保存所有的颜色
我们的代码尚未编译,因为我们还必须更改颜色数据的保存方式。首先,将Game中的保存版本增加到5。
然后调整Shape.Save,使其写入所有颜色,而不是旧的颜色字段。
加载时,如果要加载版本5或更高版本的文件,我们现在必须读取颜色并为每个元素调用SetColor。否则,我们将像以前一样设置单一颜色。
1.8 可选统一颜色
形状是否应具有统一的颜色可以根据每个生成区域来确定。因此,向SpawnZone.SpawnConfiguration中添加一个UniformColor切换。
当我们配置一个新生成的形状时,我们不需要统一的颜色,而是为每个颜色索引选择一个随机的颜色。
(不一致颜色的形状)
每个形状是否可以使用相同的色调?
当然,你可以为整个形状随机选择一次色相,而饱和度和值则保持随机,也可以使用另一个配置选项来控制它。实际上,你可以使用三个单独的开关来代替色调,饱和度和值,而不是单个统一的颜色切换。当然,这会使设置颜色的代码更加复杂。
1.9 健壮的保存
至此,我们支持复合形状,每个渲染器可以具有不同的颜色。但是我们将来可能会决定更改哪些渲染器可着色。发生这种情况时,颜色量会发生变化,但是旧的保存文件中存储的颜色数保持不变。这将导致不匹配,从而导致加载失败。为避免这种情况,我们可以像保存形状列表一样,通过存储保存的颜色数量来使保存格式。
现在,加载颜色变得更加复杂,因此让我们将该代码移至单独的LoadColors方法。
在加载颜色时,我们必须首先读取保存的颜色数量,这可能与我们当前期望的颜色数量不匹配。可以安全地读取和设置的最大颜色数量等于加载的或当前的计数,以较低的为准。但是在此之后可能还有工作要做,所以在循环之外定义迭代器变量,以便以后使用。
当两个计数最终相等时,我们要做的就是这种情况,并且大部分时候都是如此。但是,如果它们不同,则有两种可能性。第一种情况是我们存储的颜色超出了当前的需要。这意味着保存了更多的颜色,即使我们不使用它们也必须读取。
另一种情况是我们存储的颜色少于当前需要的颜色。我们已经读取了所有可用数据,但是仍然需要设置颜色。不能单纯的忽略它们,因为这样我们最终会得到随机颜色。我们需要保持一致,因此只需将其余颜色设置为白色即可。
2 第二个工厂
目前,我们使用一个工厂来处理所有形状实例。当我们只有几个形状并且不在乎将它们分类为子类别时,这很好用。但是现在,我们可以确定两个形状类别:简单形状和复合形状。每个类别使用单独的工厂可以区别对待它们,从而使我们可以更好地控制生成的形状。
2.1 复合形状工厂
通过复制现有工厂来创建另一个形状工厂资产。保持相同的材质,但确保仅引用三个复合形状的预制件。将其命名为Composite Shape Factory。将原始工厂重命名为Simple Shape Factory并从中删除复合预制引用。
(两个工厂)
现在,通过将相应的工厂分配给Game,我们可以控制是生成简单形状还是合成形状。
2.2 每个生成区分配工厂
生成时有多个工厂可供选择,因此现在有可能在每个生成区域选择一个工厂,而不是整个游戏全局。而且,我们不必局限于单一工厂的选择。相反,我们将向SpawnZone.SpawnConfiguration添加工厂引用数组。
为每个生成区域指定在生成形状时要使用的工厂的引用。每个区域至少需要一个工厂,但是你可以提供多个。生成时,我们将随机选择其中一个工厂。
(生成区的工厂配置)
你还可以不止一次包含一个工厂。这使得它更有可能被选择。例如,如果将复合工厂包含两次,而将简单工厂包含一次,则生成复合形状的可能性是简单形状的两倍。
(通过添加次数来决定概率)
2.3 用生成区取代配置
在按区域选择工厂的情况下,Game不再需要生成新形状。SpawnZone现在有责任生成形状,而不仅仅是配置形状。但是Game仍然需要跟踪形状。因此,我们将SpawnZone.ConfigureSpawn方法更改为SpawnShape,该方法没有参数,并使用配置的工厂之一返回它产生的新形状。
对CompositeSpawnZone进行相同的更改。
并在GameLevel中将ConfigureSpawn转换为SpawnShape。
最后,Game.CreateShape现在只需在当前关卡上调用SpawnShape并将返回的形状添加到其列表中。
(每个子区域使用不同的工厂)
2.4 回收形状
因为我们使用的是两个工厂,所以在玩游戏时我们还可以获得两个工厂场景,形状最终出现在它们相应的工厂场景中。
(形状来自多个工厂的实例)
尽管通过不同工厂创建形状似乎可以正常工作,但它们的重用却会出错。所有形状最终都由一家工厂回收了。这是因为Game始终使用相同的工厂来回收形状,无论它们在何处生成。
实际上,形状必须由产生它们的同一家工厂回收。为了使之成为可能,每种形状都必须跟踪其起源的工厂。将一个OriginFactory属性添加到Shape中,类似于ShapeId,但用于ShapeFactory引用。
将ShapeFactory设置为它产生的每个形状实例的起点。
现在,我们可以使用正确的工厂来回收每种形状。但是,我们无需编写诸如shape.OriginFactory.Reclaim(shape)之类的东西,而是向Shape添加了一个方便的Recycle方法,因此我们可以在不再需要它时进行调用。
在Game.DestroyShape中使用该方法。
并且在BeginNewGame中。
为安全起见,请ShapeFactory检查它是否确实是它要回收的形状的原点。如果不是,则记录错误并中止。
2.5 保存原始工厂
保存和加载也需要进行调整以支持多个工厂。我们必须保存每种形状的原始工厂,但是无法自己编写工厂资产。相反,我们需要在游戏会话之间以某种方式追踪使用了哪个工厂。为此,我们可以为每个工厂分配一个ID号并保存它。
将一个FactoryId属性添加到ShapeFactory中。我们不会通过检查器手动设置它,而是让游戏自动分配这些ID。如ShapeId一样,该属性只能设置一次。但是在这种情况下,我们要处理的资产在编辑器中的播放会话之后仍然存在,因此我们需要通过将System.NonSerialized属性附加到该字段来明确标记该字段,以使其不会被序列化。
为什么不能对factoryId进行序列化?
Unity不会保存未标记为序列化的可编写脚本对象的私有字段。但是,可编写脚本的对象实例本身可以在单个编辑器会话期间的播放会话之间保留下来。只要打开编辑器,私有字段的值就会保留,但是下次你打开Unity编辑器时,私有字段的值将被重置。通过复制创建新的工厂资产时,这会造成混乱并混淆对象,因此最好确保该字段永不持久。这确实意味着在热重载(播放模式下的重新编译)期间数据也会丢失。
为了分配ID并获得对所有工厂的引用,我们向Game添加了工厂数组。然后,我们使用该数组的索引作为工厂ID,并在OnEnable中分配它们。
我们需要使用OnEnable,以便在热重载后重新生成ID。但是,在游戏加载完成后,也会调用OnEnable,在这种情况下,不应重新分配ID。我们可以通过检查第一个ID是否设置正确来避免这种情况。
保存形状时,我们现在还必须保存其原始工厂的ID。由于选择工厂是创建形状的第一步,因此也使它成为我们为每个形状写入的第一件事。
加载形状时,除非要从旧的保存文件中读取,否则首先要读取其工厂ID。这时,我们将使用零作为默认工厂ID。然后,在获取形状实例时,使用ID检索正确的工厂。
此时,我们不再需要旧的奇异shapeFactory字段,因此将其删除。
在任何关卡中使用的所有工厂都必须被分配到游戏中。确保简单的形状工厂是第一个,这样在加载旧的安全文件时就会使用它。就像每个工厂的预制件一样,一旦一个工厂被添加到这个数组中,它就不能被再次删除或改变位置,以保证保存的文件被正确加载。
(Game下持有对所有工厂的引用)
下一个章节,形状行为。
欢迎扫描二维码,查看更多精彩内容。点击 阅读原文 可以跳转原教程。
本文翻译自 Jasper Flick的系列教程
原文地址:
https://catlikecoding.com/unity/tutorials
本文分享自微信公众号 - 壹种念头(OneDay1Idea)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。