喵的Unity游戏开发之路 - 各色对象
如果丢失格式、图片或视频,请查看原文:https://mp.weixin.qq.com/s/cvxAupkZOExAWZSEkp24gQ
很多童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始学。为此我们精选了一套国外优秀的Unity3D游戏开发教程,翻译整理后放送给大家,教您从零开始一步一步掌握Unity3D游戏开发。 本文不是广告,不是推广,是免费的纯干货!本文全名:喵的Unity游戏开发之路 - 对象管理 - 各色对象
创建形状的工厂。
保存和加载形状标识符。
支持多种材质和随机颜色。
启用GPU实例化。
这是有关对象管理的系列教程中的第二篇。在这一部分中,我们将添加对具有不同材质和颜色的多种形状的支持,同时保持与游戏先前版本的向后兼容性。
本教程使用Unity 2017.4.1f1制作。
形状工厂
本教程的目的是通过允许创建除白色立方体之外的其他形状,使我们的游戏更加有趣。就像位置,旋转和比例一样,每次播放器生成新形状时,我们都会随机选择创建的形状。
形状等级
我们将具体说明我们的游戏产生什么样的东西。它生成形状,而不是通用的可持久对象。因此,创建一个新Shape类,该类代表3D几何形状。它只是扩展了PersistableObject而没有添加任何新东西,至少目前是这样。
using UnityEngine;public class Shape : PersistableObject {}
从多维数据集预制件上删除组件PersistableObject,然后给它一个Shape组件。它不能同时具有两者,因为我们赋予PersistableObject了DisallowMultipleComponent属性,该属性也适用于Shape。
这打破了Game对象对预制件的引用。但是因为Shape也是PersistableObject,我们可以再次分配它。
多种不同形状
创建一个默认的球体和胶囊对象,为每个对象赋予一个Shape组件,并将它们也转换为预制件。这些是我们的游戏将支持的其他形状。
圆柱呢?
您也可以添加一个圆柱对象,但是我省略了它,因为圆柱没有自己的碰撞类型。取而代之的是,他们使用了一个不太适合的胶囊对撞机。现在这不是问题,但可能稍后。
工厂资产
当前,Game只能生成一件东西,因为它只有一个对预制件的引用。要支持所有三种形状,将需要三个预制参考。这将需要三个字段,但这并不灵活。更好的方法是使用数组。但是也许以后我们会想出另一种方式来创建形状。这可能会变得Game相当复杂,因为它还负责用户输入,跟踪对象以及触发保存和加载。
为Game简单起见,我们将负责在自己的类中支持哪些形状。此类将像工厂一样,按需创建形状,而其客户不必知道如何制作这些形状,甚至不必知道有多少种不同的选择。我们将为此类命名ShapeFactory。
using UnityEngine;
public class ShapeFactory {}
工厂的唯一责任是交付形状实例。它不需要位置,旋转或缩放,也不需要Update更改状态的方法。因此,它不必是组件,而必须将其附加到游戏对象上。相反,它可以单独存在,而不是作为特定场景的一部分,而是作为项目的一部分。换句话说,它是一种资产。通过使其扩展ScriptableObject而不是可以实现MonoBehaviour。
public class ShapeFactory: ScriptableObject{}
现在,我们有了一个自定义资产类型。要将这样的资产添加到我们的项目中,我们必须在Unity菜单中为其添加一个条目。最简单的方法是将CreateAssetMenu属性添加到我们的类中。
[CreateAssetMenu]
public class ShapeFactory : ScriptableObject {}
您现在可以通过Assets› Create› Shape Factory来创建我们的工厂。我们只需要一个。
为了让我们的工厂知道形状预制件,请给它一个Shape[] prefabs数组字段。我们不希望这个领域公开,因为它的内部运作不应暴露于其他类别。因此,请保密。要使数组显示在检查器中并由Unity保存,请向其中添加SerializeField属性。
public class ShapeFactory : ScriptableObject {
[SerializeField]
Shape[] prefabs;
}
字段出现在检查器中之后,将所有三个形状的预制件拖到其上,以便将对它们的引用添加到阵列中。确保多维数据集是第一个元素。将球体用于第二个元素,将胶囊用于第三个元素。
获取形状
为了使工厂有用,必须有一种方法可以使形状实例脱离工厂。因此,给它一个公共Get方法。客户端可以通过形状标识符参数指示所需的形状。为此,我们将简单地使用整数。
public Shape Get (int shapeId) {}
为什么不使用枚举?
当然可以,因此您可以这样做。但是我们实际上并不关心在代码中标识确切的形状类型,因此整数可以正常工作。这样就可以通过更改工厂的数组内容来控制纯粹支持哪些形状,而无需更改任何代码。
我们可以直接使用标识符作为索引来找到合适的形状预制件,将其实例化并返回。这意味着0表示一个立方体,1表示一个球体,而2表示一个胶囊。即使我们稍后更改工厂的工作方式,也必须确保此标识保持不变,以保持向后兼容。
public Shape Get (int shapeId) {
return Instantiate(prefabs[shapeId]);
}
除了请求特定形状外,我们还可以通过一种GetRandom方法从工厂中获取随机形状实例。我们可以使用该Random.Range方法随机选择一个索引。
public Shape GetRandom () {
return Get(Random.Range(0, prefabs.Length));
}
不是应该用Random.Range(0, prefab.Length - 1)吗?
Unity的Random.Range带有整数参数的方法使用一个互斥最大值。输出范围是从最小到最大负1。之所以这样做,是因为通常的用例都希望获得随机数组索引,而这正是我们在这里所做的。
请注意,Random.Range对于float参数,请使用包含性最大值。
获取形状
由于我们现在要在中创建形状,因此我们Game将其显式并将其列表重命名为shapes。因此,无论到哪里编写objects,都将其替换为shapes。最简单的方法是使用代码编辑器的重构功能来更改该字段的名称,并且它将在使用该字段的所有位置进行重命名。
List<PersistableObject> shapes;
还将列表的项目类型更改为Shape,这更加具体。
List<Shape> shapes;
void Awake () {
shapes = new List<Shape>();
}
接下来,删除预制字段,并添加一个shapeFactory字段以保留对形状工厂的引用。
// public PersistableObject prefab;
public ShapeFactory shapeFactory;
在中CreateObject,我们现在将通过调用shapeFactory.GetRandom而不是实例化显式的预制来创建任意形状。
void CreateObject () {
// PersistableObject o = Instantiate(prefab);
Shape o = shapeFactory.GetRandom();
…
}
我们还要重命名实例的变量,以便非常明确地表明我们正在处理形状实例,而不是仍需要实例化的预制引用。再一次,您可以使用重构来快速一致地重命名该变量。
void CreateShape() {
Shapeinstance= shapeFactory.GetRandom();
Transform t =instance.transform;
t.localPosition = Random.insideUnitSphere * 5f;
t.localRotation = Random.rotation;
t.localScale = Vector3.one * Random.Range(0.1f, 1f);
shapes.Add(instance);
}
加载时,我们现在还必须使用形状工厂。在这种情况下,我们不需要随机形状。我们以前只使用过多维数据集,因此我们应该通过调用来获得多维数据集shapeFactory.Get(0)。
public override void Load (GameDataReader reader) {
int count = reader.ReadInt();
for (int i = 0; i < count; i++) {
// PersistableObject o = Instantiate(prefab);
Shape o = shapeFactory.Get(0);
o.Load(reader);
shapes.Add(o);
}
}
我们还要在这里明确说明我们正在处理实例。
Shapeinstance= shapeFactory.Get(0);
instance.Load(reader);
shapes.Add(instance);
在给游戏提供我们工厂的参考之后,它现在将在每次玩家生成新的形状时创建随机形状,而不是总是获得立方体。
记住形状
虽然现在可以创建三个不同的形状,但是此信息尚未保存。因此,每次加载保存的游戏时,我们最终只能得到多维数据集。这对于以前保存的游戏是正确的,但对于我们添加了对多种形状的支持后保存的游戏却不正确。我们还必须添加对保存不同形状的支持,理想情况下,同时仍然能够加载旧的保存文件。
形状标识符属性
为了能够保存对象的形状,对象必须记住此信息。最简单的方法是在Shape中添加形状标识符字段。
public class Shape : PersistableObject {
int shapeId;
}
理想情况下,此字段是只读的,因为形状实例始终是一种类型,并且不会更改。但是必须以某种方式为它分配一个值。我们可以将私有字段标记为可序列化,并通过每个预制件的检查器为其分配一个值。但是,这不能保证标识符与工厂使用的数组索引匹配。我们也有可能在其他地方使用形状预制件,这与工厂无关,或者甚至在某个时候将其添加到另一个工厂。因此,形状标识取决于工厂,而不取决于预制件。因此,这是每个实例而不是每个预制件要跟踪的东西。
默认情况下,私有字段不会序列化,因此预制与它无关。一个新实例将简单地获取该字段的默认值,在这种情况下为0,因为我们没有给它另一个默认值。为了使标识符可公开访问,我们将在中添加一个ShapeId属性Shape。除了第一个字母是大写字母外,我们使用相同的名称。属性是伪装成字段的方法,因此它们需要一个代码块。
public int ShapeId {}
int shapeId;
属性实际上需要两个单独的代码块。一种获取它表示的值,另一种进行设置。这些通过get和set关键字标识。可以仅使用其中之一,但是在这种情况下,我们需要两者。
public int ShapeId {
get {}
set {}
}
getter部分仅返回私有字段。设置者仅分配给私有字段。设置器具有value为此目的命名的适当类型的隐式参数。
public int ShapeId {
get {
return shapeId;
}
set {
shapeId = value;
}
}
int shapeId;
通过使用属性,可以在看起来很简单的检索或分配中添加其他逻辑。在我们的情况下,形状标识符必须在出厂时实例化时每次实例设置一次。在那之后再次设置它将是一个错误。
我们可以通过验证标识符在分配时是否仍具有其默认值来检查分配是否正确。如果是这样,则该分配有效。如果没有,我们将记录一个错误。
public int ShapeId {
get {
return shapeId;
}
set {
if (shapeId == 0) {
shapeId = value;
}
else {
Debug.LogError("Not allowed to change shapeId.");
}
}
}
但是,0是有效的标识符。因此,我们必须使用其他值作为默认值。让我们使用最小的可能整数int.MinValue,即-2147483648。另外,我们应确保标识符不能重置为其默认值。
public int ShapeId {
…
set {
if (shapeId ==int.MinValue && value != int.MinValue) {
shapeId = value;
}
…
}
}
int shapeId= int.MinValue;
为什么不只是使用readonly财产?
readonly只能为字段或属性分配默认值,或者在构造函数方法中将其分配给该字段或属性。不幸的是,我们在实例化Unity对象时不能使用构造函数方法。因此,我们必须使用这样的方法。
调整ShapeFactory.Get以便它在返回实例之前设置实例的标识符。
public Shape Get (int shapeId) {
// return Instantiate(prefabs[shapeId]);
Shape instance = Instantiate(prefabs[shapeId]);
instance.ShapeId = shapeId;
return instance;
}
识别文件版本
我们之前没有形状标识符,因此我们没有保存它们。如果我们从现在开始保存它们,我们将使用其他保存文件格式。如果我们的游戏的旧版本(来自上一教程)不能读取此格式,则很好,但是我们应确保新游戏仍然可以使用旧格式。
我们将使用保存版本号来标识保存文件使用的格式。现在,我们开始介绍此概念时,从版本1开始。将其作为常量整数添加到中Game。
const int saveVersion = 1;
const是什么意思
它声明一个简单的值是一个常量,而不是一个字段。它无法更改,并且不存在于内存中。相反,它只是代码的一部分,其显式值在引用的任何地方都会使用,并在编译期间替换。
保存游戏时,请先编写保存版本号。加载时,请先阅读存储的版本。这告诉我们正在处理什么版本。
public override void Save (GameDataWriter writer) {
writer.Write(saveVersion);
writer.Write(shapes.Count);
…
}
public override void Load (GameDataReader reader) {
int version = reader.ReadInt();
int count = reader.ReadInt();
…
}
但是,这仅适用于包含保存版本的文件。上一教程中的旧保存文件没有此信息。相反,写入这些文件的第一件事是对象计数。因此,我们最终将计数解释为版本。
存储在旧的保存文件中的对象计数可以是任何数字,但始终至少为零。我们可以使用它来区分保存版本和对象计数。通过不逐字写保存版本来完成。而是在编写版本时翻转版本的符号。从1开始,这意味着存储的保存版本始终小于零。
writer.Write(-saveVersion);
阅读版本时,请再次翻转其符号以获取原始编号。如果我们正在读取旧的保存文件,则最终会翻转计数的符号,因此它变为零或负数。因此,当我们最终得到的版本小于或等于零时,我们知道我们正在处理一个旧文件。在那种情况下,我们已经有了计数,只有一个翻转的符号。否则,我们仍然必须读取计数。
int version =-reader.ReadInt();
int count =version <= 0 ? -version :reader.ReadInt();
问号是什么意思?
它是三元运算符,condition ? trueResult : falseResult它是if-else表达式的简写形式。在这种情况下,代码等效于以下代码:
int version = -reader.ReadInt();
int count;
if (version <= 0) {
count = -version;
}
else {
count = reader.ReadInt();
}
这使得新代码可以处理旧的保存文件格式。但是旧代码无法处理新格式。我们对此无能为力,因为旧的代码已经编写好了。我们能做的是确保从现在开始游戏将拒绝加载它不知道如何处理的将来的保存文件格式。如果加载的版本高于我们当前的保存版本,请记录错误并立即返回。
int version = -reader.ReadInt();
if (version > saveVersion) {
Debug.LogError("Unsupported future save version " + version);
return;
}
保存形状标识符
形状不应该写自己的标识符,因为必须读取它以确定要实例化的形状,并且只有在此之后才能加载自身。因此Game,编写标识符是责任。因为我们将所有形状存储在一个列表中,所以我们必须在形状保存之前写出每个形状的标识符。
public override void Save (GameDataWriter writer) {
writer.Write(-saveVersion);
writer.Write(shapes.Count);
for (int i = 0; i < shapes.Count; i++) {
writer.Write(shapes[i].ShapeId);
shapes[i].Save(writer);
}
}
请注意,这不是保存形状标识符的唯一方法。例如,也可以对每种形状类型使用单独的列表。在这种情况下,每个列表只需写入一次每个形状标识符。
加载形状标识符
对于列表中的每个形状,首先加载其形状标识符,然后使用该标识符从工厂获取正确的形状。
public override void Load (GameDataReader reader) {
…
for (int i = 0; i < count; i++) {
int shapeId = reader.ReadInt();
Shape instance = shapeFactory.Get(shapeId);
instance.Load(reader);
shapes.Add(instance);
}
}
但这仅对新的保存版本1有效。如果我们正在从旧的保存文件中读取,则只需获取多维数据集即可。
int shapeId =version > 0 ?reader.ReadInt(): 0;
材质变体
除了改变生成的对象的形状外,我们还可以改变它们的组成。目前,所有形状都使用相同的材质,这是Unity的默认材质。让我们将其更改为随机选择的材质。
三种材质
创建三种新材质。命名第一个Standard,使其保持不变,以便与Unity的默认材质匹配。将第二个命名为Shiny并将其“ 平滑度”增加到0.9。将第三个命名为Metallic,并将其Metallic和Smoothness都设置为0.9。
当从工厂获得形状时,现在还应该可以指定必须使用哪种材质。这需要ShapeFactory知道允许的材质。因此,为其提供一个材质阵列(就像其预制阵列一样),并为其分配三种材质。确保标准材质是第一个元素。第二种是有光泽的材质,第三种是金属。
[SerializeField]
Material[] materials;
设置形状的材质
为了保存形状具有的材质,我们现在还必须跟踪材质标识符。为此添加一个属性Shape。但是,除了显式地编码属性的工作方式之外,请省略getter和setter的代码块。请以分号结尾。这将生成一个默认属性,并带有一个隐式的隐藏私有字段。
public int MaterialId { get; set; }
设置形状的材质时,我们必须同时为其提供实际的材质及其标识符。这表明我们必须一次使用两个参数,但是对于属性来说这是不可能的。因此,我们不会依赖该属性的设置者。要禁止在Shape类本身之外使用它,请将设置器标记为私有。
public int MaterialId { get;privateset; }
相反,我们添加了SetMaterial带有必需参数的公共方法。
public void SetMaterial (Material material, int materialId) {}
该方法可以通过调用该GetComponent<MeshRenderer>方法来获取形状的MeshRenderer分量。请注意,这是一个通用方法,就像List通用类一样。设置渲染器的材质以及材质标识符属性。确保将参数分配给属性,区别在于M是否为大写字母。
public void SetMaterial (Material material, int materialId) {
GetComponent<MeshRenderer>().material = material;
MaterialId = materialId;
}
使用材质获取形状
现在我们可以进行调整ShapeFactory.Get以处理材质。给它第二个参数来指示应该使用哪种材质。然后使用它来设置形状的材质及其材质标识符。
public Shape Get (int shapeId, int materialId) {
Shape instance = Instantiate(prefabs[shapeId]);
instance.ShapeId = shapeId;
instance.SetMaterial(materials[materialId], materialId);
return instance;
}
调用Get的任何人可能都不在乎材质,而对标准材质感到满意。我们可以使用单个形状标识符参数来支持Get的变体。为此,我们可以使用0 为其materialId参数分配默认值。这样做可以在调用Get时省略该materialId参数。结果,现有代码在此时编译时没有错误。
public Shape Get (int shapeId, int materialId= 0) {
…
}
我们可以对shapeId参数执行相同的操作,也将其默认值设置为0。
public Shape Get (int shapeId= 0, int materialId = 0) {
…
}
您如何指示所需的默认值?
要省略materialId,只需将其忽略即可,因此您可以调用Get(0)。您也可以通过调用Get()忽略两个参数。但是,如果要省略shapeId而不是materialId,则必须明确说明要提供的参数。您可以通过标记参数,在参数值之前写入参数名称以及冒号来做到这一点。例如,Get(materialId: 0)。
该GetRandom方法现在应该同时选择随机形状和随机材质。因此,它也可以用Random.Range来选择随机的材质标识符。
public Shape GetRandom () {
return Get(
Random.Range(0, prefabs.Length),
Random.Range(0, materials.Length)
);
}
保存和加载物料标识符
保存材质标识符与保存形状标识符相同。将其写在每个形状的形状标识符之后。
public override void Save (GameDataWriter writer) {
…
for (int i = 0; i < shapes.Count; i++) {
writer.Write(shapes[i].ShapeId);
writer.Write(shapes[i].MaterialId);
shapes[i].Save(writer);
}
}
加载也一样。我们不会费心增加此更改的保存版本,因为我们仍在同一教程中,该教程象征着一个公共发行版。因此,对于存储形状标识符但不存储材质标识符的保存文件,加载将失败。
public override void Load (GameDataReader reader) {
…
for (int i = 0; i < count; i++) {
int shapeId = version > 0 ? reader.ReadInt() : 0;
int materialId = version > 0 ? reader.ReadInt() : 0;
Shape instance = shapeFactory.Get(shapeId, materialId);
instance.Load(reader);
shapes.Add(instance);
}
}
随机颜色
除了整个材质,我们还可以改变形状的颜色。我们通过调整每个形状实例材质的颜色属性来实现。
我们可以定义一组有效的颜色并将其添加到形状工厂中,但是在这种情况下,我们将使用不受限制的颜色。这意味着工厂不必了解形状颜色。而是设置形状的颜色,就像其位置,旋转和比例一样。
形状颜色
添加一种SetColor方法,使其Shape可以调整其颜色。它必须调整使用的任何材质的颜色属性。
public void SetColor (Color color) {
GetComponent<MeshRenderer>().material.color = color;
}
为了保存和加载形状的颜色,必须跟踪它。我们不需要提供对颜色的公共访问权限,因此可以通过设置一个私有字段SetColor。
Color color;
public void SetColor (Color color) {
this.color = color;
GetComponent<MeshRenderer>().material.color = color;
}
保存和加载颜色是通过覆盖PersistableObject的Save和Load方法来完成的。首先要照顾好基础,然后是色彩数据。
public override void Save (GameDataWriter writer) {
base.Save(writer);
writer.Write(color);
}
public override void Load (GameDataReader reader) {
base.Load(reader);
SetColor(reader.ReadColor());
}
但是,这假设存在用于写入和读取颜色的方法,当前情况并非如此。因此,让我们添加它们。首先是GameDataWriter的一种新的Write方法。
public void Write (Color value) {
writer.Write(value.r);
writer.Write(value.g);
writer.Write(value.b);
writer.Write(value.a);
}
然后也是一种GameDataReader到底ReadColor方法。
public Color ReadColor () {
Color value;
value.r = reader.ReadSingle();
value.g = reader.ReadSingle();
value.b = reader.ReadSingle();
value.a = reader.ReadSingle();
return value;
}
我们是否需要将颜色通道存储为浮点数?
您也可以决定将它们存储为字节,但是如果这样做,最好始终Color32在任何地方使用。这样可以确保保存和加载的数据始终相同。您不必为此烦恼,只需为每个形状节省十二个字节,除非您确实需要最小化保存文件的大小。同样,您可以决定跳过Alpha通道,因为不透明的材质不需要它,但通常也不值得担心。
其余向后兼容
尽管此方法可以存储形状颜色,但现在假定颜色存储在保存文件中。旧的保存格式不是这种情况。为了仍然支持旧格式,我们必须跳过加载颜色。在Game中,我们使用读取版本来决定要做什么。但是,Shape不知道版本。因此,Shape在加载数据时,我们必须以某种方式将正在读取的数据的版本进行通信。将版本定义为GameDataReader的属性是有意义的。
由于读取文件的版本在读取时不会更改,因此该属性应仅设置一次。与GameDataReader不是Unity对象类一样,我们可以通过仅提供get一部分属性来使用只读属性。可以通过构造函数方法初始化此类属性。为此,我们必须将版本添加为构造函数参数。
public int Version { get; }
BinaryReader reader;
public GameDataReader (BinaryReader reader, int version) {
this.reader = reader;
this.Version = version;
}
我们不能满足Version = version吗?
是的,但是我为清楚起见添加this了。
现在,编写和阅读版本号已成为PersistentStorage的责任。该版本必须作为其Save方法的参数添加,该方法必须在其他任何方法之前将其写入。并且该Load方法在GameDataReader构造时读取它。同样在这里,我们将执行符号更改技巧,以支持读取零版本文件。
public void Save (PersistableObject o, int version) {
using (
var writer = new BinaryWriter(File.Open(savePath, FileMode.Create))
) {
writer.Write(-version);
o.Save(new GameDataWriter(writer));
}
}
public void Load (PersistableObject o) {
using (
var reader = new BinaryReader(File.Open(savePath, FileMode.Open))
) {
o.Load(new GameDataReader(reader, -reader.ReadInt32()));
}
}
这意味着Game不再需要编写保存版本。
public override void Save (GameDataWriter writer) {
// writer.Write(-saveVersion);
writer.Write(shapes.Count);
…
}
而是在调用PersistentStorage.Save时必须将其作为参数提供。
void Update () {
…
else if (Input.GetKeyDown(saveKey)) {
storage.Save(this, saveVersion);
}
…
}
现在,在其Load方法中,可以通过reader.Version检索版本。
public override void Load (GameDataReader reader) {
int version =reader.Version;
…
}
现在我们还可以检查Shape.Load中的版本。如果我们至少有版本1,请阅读颜色。否则,请使用白色。
public override void Load (GameDataReader reader) {
base.Load(reader);
SetColor(reader.Version > 0 ?reader.ReadColor(): Color.white);
}
选择形状颜色
要创建具有任意颜色的形状,只需在Game.CreateShape中调用SetColor新实例。我们可以使用该Random.ColorHVS方法生成随机颜色。没有参数,该方法可以创建任何有效的颜色,这可能会有点混乱。通过将饱和度范围限制为0.5-1,将值范围限制为0.25-1,让我们将自己局限于彩色调色板。由于我们此时尚未使用alpha,因此我们始终将其设置为1。
void CreateShape () {
Shape instance = shapeFactory.GetRandom();
Transform t = instance.transform;
t.localPosition = Random.insideUnitSphere * 5f;
t.localRotation = Random.rotation;
t.localScale = Vector3.one * Random.Range(0.1f, 1f);
instance.SetColor(Random.ColorHSV(0f, 1f, 0.5f, 1f, 0.25f, 1f, 1f, 1f));
shapes.Add(instance);
}
使用ColorHVS的所有8个参数很难理解,因为尚不清楚哪个值控制什么。通过显式命名参数,可以使代码更易于阅读。
instance.SetColor(Random.ColorHSV(
hueMin:0f,hueMax:1f,
saturationMin:0.5f,saturationMax:1f,
valueMin:0.25f,valueMax:1f,
alphaMin:1f,alphaMax:1f
));
记住渲染器
现在,我们在设置材质和颜色时都需要访问它们Shape的MeshRenderer组件。使用GetComponent<MeshRenderer>两次是不理想的,尤其是如果我们决定将来多次更改形状的颜色时。因此,让我们将引用存储在私有字段中,并使用Shape的新方法Awake对其进行初始化。
MeshRenderer meshRenderer; void Awake () { meshRenderer = GetComponent<MeshRenderer>(); }
public void SetColor (Color color) { this.color = color;// GetComponent<MeshRenderer>().material.color = color; meshRenderer.material.color = color; } public void SetMaterial (Material material, int materialId) {// GetComponent<MeshRenderer>().material = material; meshRenderer.material = material; MaterialId = materialId; }
使用属性块
设置材质颜色的一个缺点是,这会导致创建新的形状独特的材质。每次设置颜色时都会发生这种情况。我们可以通过使用一个 Material属性块来避免这种情况。创建一个新的属性块,设置一个名为_Color的颜色属性,然后通过调用MeshRenderer.SetPropertyBlock将其用作渲染器的属性块。
public void SetColor (Color color) {
this.color = color;
// meshRenderer.material.color = color;
var propertyBlock = new MaterialPropertyBlock();
propertyBlock.SetColor("_Color", color);
meshRenderer.SetPropertyBlock(propertyBlock);
}
除了使用字符串来命名color属性之外,还可以使用标识符。这些标识符由Unity设置。它们可以更改,但每个会话保持不变。因此,我们只需获取一次color属性的标识符,并将其存储在静态字段中就足够了。通过Shader.PropertyToID使用名称调用方法来找到标识符。
static int colorPropertyId = Shader.PropertyToID("_Color");
…
public void SetColor (Color color) {
this.color = color;
var propertyBlock = new MaterialPropertyBlock();
propertyBlock.SetColor(colorPropertyId, color);
meshRenderer.SetPropertyBlock(propertyBlock);
}
也可以重用整个属性块。设置渲染器的属性时,将复制块的内容。因此,我们不必为每个形状创建新的块,我们可以为所有形状不断更改同一块的颜色。
我们可以再次使用静态字段来跟踪该块,但是不可能通过静态初始化来创建一个块实例。Unity不允许这样做。相反,我们可以在使用该块之前检查该块是否存在。如果没有,我们将在那时创建它。
static MaterialPropertyBlock sharedPropertyBlock;
…
public void SetColor (Color color) {
this.color = color;
// var propertyBlock = new MaterialPropertyBlock();
if (sharedPropertyBlock == null) {
sharedPropertyBlock = new MaterialPropertyBlock();
}
sharedPropertyBlock.SetColor(colorPropertyId, color);
meshRenderer.SetPropertyBlock(sharedPropertyBlock);
}
现在,我们不再获得重复的材质,您可以通过在播放模式中使用形状时调整其中一种材质来进行验证。这些形状将根据更改来调整外观,如果使用重复的材质则不会发生这种情况。当然,当您调整材质的颜色时,这是行不通的,因为每种形状都使用自己的color属性,该属性会覆盖材质的颜色。
GPU实例化
当我们使用属性块时,可以使用GPU实例化在单个绘制调用中组合使用相同材质的形状,即使它们具有不同的颜色。但是,这需要支持实例颜色的着色器。这是一个着色器,您可以在Unity GPU Instancing手册页上找到它。唯一的区别是我删除了注释并添加了#pragma instancing_options assumeuniformscaling指令。假设统一缩放比例可以使实例化效率更高,因为它需要较少的数据,并且可以工作,因为我们所有的形状都使用统一比例尺。
Shader "Custom/InstancedColors" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Standard fullforwardshadows
#pragma instancing_options assumeuniformscaling
#pragma target 3.0
sampler2D _MainTex;
struct Input {
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color)
UNITY_INSTANCING_BUFFER_END(Props)
void surf (Input IN, inout SurfaceOutputStandard o) {
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) *
UNITY_ACCESS_INSTANCED_PROP(Props, _Color);
o.Albedo = c.rgb;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
更改我们的三种材质,以便他们使用此新着色器代替标准着色器。它支持较少的功能,并具有不同的检查器界面,但足以满足我们的需求。然后,确保对所有材质都选中了“ 启用GPU实例化”。
您可以通过“ 游戏”窗口的“ 统计数据”叠加层来验证差异。
下一个教程是 对象复用。
资源库(Repository)
https://bitbucket.org/catlikecodingunitytutorials/object-management-02-object-variety/
往期精选
Unity3D游戏开发中100+效果的实现和源码大全 - 收藏起来肯定用得着
Shader学习应该如何切入?
喵的Unity游戏开发之路 - 从入门到精通的学习线路和全教程
声明:发布此文是出于传递更多知识以供交流学习之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与我们联系,我们将及时更正、删除,谢谢。
原作者:Jasper Flick
原文:
https://catlikecoding.com/unity/tutorials/object-management/object-variety/
翻译、编辑、整理:MarsZhou
More:【微信公众号】 u3dnotes