![39e572d67aa770a4d2a4b6ff3e33d959.png](https://i-blog.csdnimg.cn/blog_migrate/11e19be86ba5f79647bb3ae85feab283.jpeg)
这是一篇译文,附上原文链接
https://catlikecoding.com/unity/tutorials/basics/building-a-graph/catlikecoding.com- 创建prefab
- 通过立方体实例化一条线
- 展示一个数学函数
- 创建一个自定义shader
- 使图形动起来
在本教程中,我们将使用游戏对象来构建图形,以便我们展示数学公式。我们还将使函数依赖于时间,从而生成一个动画图形。
本教程假设你已经完成了游戏对象和脚本 (创建一个时钟)教程并且使用的Unity为2017.1.0以上版本。
![c7fb2ee6f3f3f77a5b1eaa7cbc411e15.png](https://i-blog.csdnimg.cn/blog_migrate/1f8947d7ebae9965df8c8503618c8e8b.jpeg)
- 使用立方体创建一条线
良好的数学基础对于编程是必不可少的。最基本的数学是对表示数字的符号处理。解方程可以归结为重写一组符号,使之成为另一组符号(通常是更短的一组符号)。数学规则规定了如何重写。
例如,我们现在有一个函数
![afe4fcfd04ad1d906ec3a3c5fb98800a.png](https://i-blog.csdnimg.cn/blog_migrate/1e814fcc8d98ceafbfe2a88bbf60d2fb.png)
通过观察图形可以快速的让我们理解函数表达了什么。这是一个方便的工具,让我们在Unity中创建一个。通过File->New Scene菜单创建一个新场景,或者使用一个新项目的默认场景。
1.1 Prefabs
图形是通过在适当的坐标系上放置点创建的。要做到这一点,我们需要一个3D可视化的点。我们将简单地使用Unity默认立方体游戏对象。添加一个到场景中,并移除collider组件,因为我们不需要使用物理特性。
Ps:立方体是可视化图形的最佳方式吗?
你也可以使用粒子系统或线段,但是使用单个立方体是最简单的。
我们将使用一个脚本来创建许多立方体的实例并将它们摆放在正确的位置。为此,我们将使用立方体作为一个模板。将立方体从hierarchy窗口拖动到project窗口。这将创建一个带有蓝色立方体图标的新资源,称作预制体。它是一个存在于项目中,但不在场景中的预制游戏对象。
![baf25b718c6a4b3bd148a2864129fe13.png](https://i-blog.csdnimg.cn/blog_migrate/063c4060858bd0702398c65635b6d3be.jpeg)
预制是配置游戏对象的一种简便方法。如果更新预制资源,场景中所有的预制实例都将以相同的方式更改。例如,改变预制的缩放也会更改在场景中的立方体缩放。但是,每个实例都使用自己的位置和旋转。此外,游戏对象也可以修改它们的属性,这将重写预制的属性值。如果进行了较大的更改,例如添加或删除组件,则会破坏预制和实例之间的关系。
我们将通过脚本创建预制体的实例,因此不再需要当前场景中的立方体实例。所以我们删除它。
1.2 图形组件
我们需要一个C#脚本来生成图形。创建一个名为Graph的脚本。我们从一个继承自MonoBehaviour的简单类开始,这样它就可以用作为游戏对象的组件。定义一个名为pointPrefab的公共字段来保存用于创建点的预制的引用。由于我们需要访问Transform组件来对这些点设置坐标,所以将字段类型设置为Transform。
using
通过GameObject->Create Empty向场景中添加一个名为Graph的空游戏对象,并将它放置在原点。通过拖动或者Add Component按钮,将Graph组件添加给这个游戏对象。然后将预制资源赋值给Graph组件的Point Prefab字段。现在Graph组件就持有了预制体的Transform组件的引用。
![49247f68fbc220781bc011e928eda2ea.png](https://i-blog.csdnimg.cn/blog_migrate/c555f6c08fc16c416e200ef14ff610f1.png)
1.3 实例化预制体
通过Instantiate方法完成游戏对象的实例化。这是Unity对象类型的一个公共可用方法,Graph从MonoBehaviour间接继承了它。这个实例化方法克隆作为参数提供的任何Unity对象。在预制作为参数的情况下,它将添加一个实例到当前场景中。让我们在Graph组件唤醒时这样做。
public
![569a8315556756a6ec7fe21a67e30b79.png](https://i-blog.csdnimg.cn/blog_migrate/6736fbf6fa5450e12cc5e8a96db557e8.png)
此时,进入播放模式将在原点生成一个立方体,前提是将预制资源的位置设置为零。要将实例放在其他位置,我们需要调整实例的位置。Instantiate方法为我们返回了它所创建对象的引用。由于我们给它提供了一个Transform组件的引用,这也将是它返回给我们的结果的类型。让我们定义一个变量来跟踪它。
private
现在我们可以通过赋值一个3D向量来调整点的位置。就像我们在游戏对象和脚本 (创建一个时钟)中调整Clock的指针局部旋转一样,我们将通过localPosition属性来调整点的局部坐标,而不是point属性。
3D向量是通过Vector3结构体创建的。由于它是一个结构体,它就像一个值,类似于一个数字,而不是一个对象。例如,让我们把点的X坐标设置为1,让它的Y和Z坐标为零。Vector3对此有一个right属性。
Transform
Ps:属性不是应该大写吗?
按照惯例,属性是应该大写,但是Unity通常不会这样做。
现在我们进入游戏模式,我们仍然得到一个立方体,只是在一个稍微不同的位置。让我们实例化第二个,并将其放在更右边一点。我们可以通过Vector3.right*2来实现。重复实例化和设置坐标代码,然后将乘法添加到新代码中。
Transform
Ps:能够将结构体和数字相乘吗?
通常情况下是不能,但是可以定义这样的功能。这是通过创建具有特殊语法的方法来实现的,因此可以像调用乘法一样调用它。在本例中,看似简单的乘法实际上是一个方法调用。类似于Vector3.Multiply(Vector3.right, 2f)。
使方法能够像使用简单操作一样调用,可以使编写代码更快、更易阅读。这不是必须的,但确实最好的方式,就像能够隐式的使用namespace一样。这种方便的语法被称为语法糖。
尽管如此,只有当方法严格符合该操作符的原始含义时,才应该将其用作操作符。对于本例中的向量来说,一些数学运算符是定义明确的,所以这样的操作没什么问题。
现在代码将导致编译错误,由于我们重复定义了point变量。如果我们向使用另一个变量,我们需要给它一个不同的名称。另外,我们可以重复使用以及存在的变量。我们不需要保留对第一个点的引用。所以只需要将新的点赋值给相同的变量。
Transform
![ad7726aba46a982e21d74366b694a9c7.png](https://i-blog.csdnimg.cn/blog_migrate/76259a69d13081b9760d3c6adbc62ca4.png)
1.4 代码循环
让我们创建更多的点,直到拥有十个。我们重复编写同样的代码8次以上,但这是非常低效的编程。理想情况下,我们只需编写创建一个点的代码,并让程序多次执行它,只需做轻微的变化。
While语句可以用于使代码块重复执行。将它应用到方法的前两行并删除其余行。
private
与if语句类似,while后面必须跟一个圆括号内的表达式;并且while后面的代码块仅在表达式计算结果为true时才会执行。然后,程序将循环会while语句。如果表达式再次计算为true,代码块将再次执行。这将重复执行直到表达式的计算结果为false。
所以我们必须在while后面加上一个表达式。我们必须小心确保循环不会永远执行。无线循环会导致程序卡主,需要用户手动终止。最安全的编译表达式是false。
while
Ps:可以在循环内部定义point?
当然。虽然代码会重复,但是我们只定义了变量一次。它可以在循环的每个迭代中重用。就像我们前面手工做的那样。
也可以在循环之前定义point。这就允许在循环之外使用该变量。否则,它的作用于尽在while循环的代码块之内。
我们可以通过追踪重复执行代码的次数来限制循环。我们用一个整型变量来保持追踪。它将包含循环的迭代次数,我们将它命名为i。为了让它能够在while条件中使用,它必须提前定义。
private void Awake()
{
int i;
while (false)
{
Transform point = Instantiate(pointPrefab);
point.localPosition = Vector3.right;
}
}
每次迭代,将数字加1.
int
这将导致编译错误,由于我们试图在为i赋值之前使用它。我们需要在定义i的时候显示的为它赋值为0.
int
在第一次迭代后i的值将变为1,第二次迭代后i的值将变为2,以此类推。但是while表达式值在每次迭代之前求值的。所以第一次迭代之前i的值为0,第二次迭代之前i的值为1,以此类推。所以第十次迭代之后i的值为10。此时我们希望停止循环,所以表达式的值应该为false。换句话说,当i小于10我们就应该继续循环。在数学上,它表示为i<10,在代码中也一样,也是使用小于操作符。
int
现在我们进入游戏模式将得到十个立方体。但是它们现在都在同一坐标点。为了让它们沿X轴排成一行,用Vector3.right*i。
point
![bed7b1c8ff94dee284d87819d26c4529.png](https://i-blog.csdnimg.cn/blog_migrate/abab8f54e1248b423ac8b7f5fcdc29fb.png)
注意,当前第一个立方体X坐标为1,最后一个立方体的X坐标为10.理想情况下,我们想让第一个立方体的位置在原点。我们可以通过使用(i-1)替换i使所有点向左移动一个单位。然而,我们可以将i+1移动到代码块的末尾来跳过这个额外的减法。
while (i < 10)
{
// i = i + 1;
Transform point = Instantiate(pointPrefab);
point.localPosition = Vector3.right * i;
i = i + 1;
}
1.5 简洁语法
由于循环是很常见的操作,所以尽可能简短的语法是很方便的。一些语法糖可以帮助我们做到这一点。
首先,让我们来看增加迭代次数。当编写x = x * y时,库将其缩短为x *= y。这适用于所有作用于想同类型的两个操作数的操作符。
// i = i + 1;
更进一步,当一个数增加或减少1时,可以缩写为++x或--x。
// i += i;
++i;
赋值语句的一个属性是也可以被用作表达式。这意味着你可以编写类似y = (x += 3)的语句。这将让x增加3并把结果赋值给y。这意味着我们可以在while表达式中增加i,从而缩短代码块。
while
然而,现在我们在比较之前增加i,而不是在比较之后,这将减少一次迭代。在这种特殊的情况下,递增和递减运算符也可以放在变量后面。这样表达式使用的是修改前的原始值。
//while (++i < 10)
虽然while语句适用于所有类型的循环,但是有一种替代语法特别适用于范围遍历。它是for循环。它的工作原理与while类似,只是迭代器变量声明及其比较都包含在圆括号中,用分号分隔。
//int i = 0;
这将导致编译错误,因为需要三个部分。第三部分是增加迭代器,将它和比较分开。
//for (int i = 0; i++ <10)
Ps:为什么使用i++而不是++i?
由于自增表达式没有被用作它用,我们无论使用什么版本都没有影响。我们也可以用 i += 1 或 i = i + 1。
经典for循环的形式为for (int i = 0; i < someLimit; i++)。你将在许多程序和脚本中遇到这样的代码片段。
1.6 更改位置范围
现在,我们的立方体X坐标为0~9。对于函数来说这不是一个方便处理的范围。通常,X的范围为0~1。或者函数处理范围以0为中心,范围为-1~1。让我们重新定位相应的立方体。
将我们的立方体沿着一条2个单位长度的线段放置将会导致它们重叠。为了防止重叠,我们需要对它们进行缩放。每个立方体每个维度的默认大小为1,为了让它适应我们的范围我们需要将它们缩放为2/10=1/5。我们可以通过设置每个点的localScale属性为Vector3.one/5来达到目的。
for
![a4b2043033738168b74d330a18a443ea.png](https://i-blog.csdnimg.cn/blog_migrate/8e0a7352564caa8699f1479d0dcf1b64.png)
要将立方体重新组合在一起,也需要将它们的位置除以5。
point
这使得它们的覆盖范围为0~2.要把它变成-1~1的范围,在缩放向量之前减去1。
point
现在第一个立方体的X坐标为-1,但是最后一个立方体的X坐标为0.8。然而,立方体的大小为0.2。由于立方体是以其位置为中心,第一个立方体的左侧为-1.1,而最后一个立方体的右侧为0.9。为了使立方体整齐的填充-1~1的范围,我们必须把它们向右移动半个立方体。可以通过在i上加上0.5以后再做除法实现。
point
1.7 把向量从循环中提出来
由于每个立方体的缩放是一样的,我们在循环代码每次迭代中都在计算它。我们没有必要这样做。相反,我们可以在循环之前计算一次,将它存储在Vector3变量中,并在循环中使用它。
Vector3
我们也在循环之前可以定义位置变量。由于我们是沿X轴创建的一条线,仅需要在循环中调整坐标的X值。所以我们不再需要乘以Vector3.right。
Vector3
Ps:能够单独改变向量的分量吗?
Vector3结构体有3个浮点数分量,分别为x,y,z。它们是public的,所以能够修改它们。
因为结构的行为类似于简单的值,所以它们应该是不可变的。一旦定义,它们就不应该改变。如果想使用不同值,请为字段或变量分配一个新的结构体,就像处理数字一样。如果开始设置x=3后面又设置x=5,这将导致给它分配了一个不同的数字。我们不能改变数字3的值为5。然而,Unity的向量类型是可变的。这样做是为了方便和性能,因为向量的分量通常是独立操作的。
要了解如何处理可变向量,可以考虑使用Vector3作为使用三个独立浮点值的替代品。你可以独立的访问它们,也可以将它们作为一个组复制和分配。
这将导致编译错误,由于使用了没有赋值的变量。这是因为我们给position赋值了X值但是没有对Y和Z赋值。在循环之前应该对position显示的赋值。
Vector3
1.8 通过X定义Y
这个思想是把立方体的坐标定义为
for
![8245988c77b81f780938d7dc9f2bf689.png](https://i-blog.csdnimg.cn/blog_migrate/1ef19a6f894139b2843a35faaea43089.png)
一个稍微不那么明显的函数是f(x) = x2,它定义了一个最小值为0的抛物线。
position
![1d4c089e1ee69fd7a68de57591970951.png](https://i-blog.csdnimg.cn/blog_migrate/fb1511881b2615e3520d88fd162f538d.png)
2. 创建更多的立方体
尽管我们现在有一个函数图形,但是它很难看。由于我们只使用了10个立方体,所以连接的线看上去就是一块一块的比较离散。如果我们使用更多的小立方体看上去就会更好。
2.1 分辨率变量
相对于使用固定的立方体数量,我们可以让它可配置。给Graph组件添加一个名为resolution的公共整型字段来实现这一点。给它一个我们现在使用的值10为默认值10。
public
![688ef6c7df2a077636bbb4701069c4d7.png](https://i-blog.csdnimg.cn/blog_migrate/cf8817f3d11ea1890ed81ece29b28837.png)
我们可以通过在inspector中改变这个值来调整图形的分辨率。然而,并非所有的整数都是有效分辨率。至少它们必须是正数。我们可以只是inspector为我们的分辨率设定一个范围。这是通过在定义的字段前面加上Range属性实现的。
[Range]
Range是一个Unity定义的属性类型。属性是将元数据附加到代码结构的一种方式,在本例中是字段。Unity的inspector会检查一个字段是否有Range属性附加到它。如果有,它将使用滑动条来代替默认的输入框。然而,为了实现这一点需要知道范围值。所以Range有两个参数,最小和最大值。让我们使用10和100。此外,属性通常写在字段的上面而不是前面。
[Range(10, 100)]
![64770612dadad5f697e95829ff8272c2.png](https://i-blog.csdnimg.cn/blog_migrate/e45992bc2f1edc1dd6c3586211a275ca.png)
Ps:这是否能确保分辨率被限制在10~100?
不能,它所做的只是指示检查器使用具有该范围的滑动条。它不会以任何其他方式影响分辨率。所以你可以通过代码给它赋任何你想要的值。在本教程中,我们假设分辨率只通过inspector进行调整,不会再有其他地方。
2.2 实例化变量
要真正使用分辨率,我们需要改变我们要实例化立方体的数量。替换Awake函数中循环的固定次数,迭代次数现在由分辨率控制。所以,如果分辨率设置为50,进入游戏模式后我们将创建50个立方体。
for
我们还需要调整立方体的缩放和位置以保证它们在-1~1的域之内。现在我们要用2/resolution来替换1/5作为每次迭代的步长。将步长存储在一个变量中,并用它来计算立方体的缩放比例和X坐标。
private
![79a044ac7762442d739fef2c4d4a9177.png](https://i-blog.csdnimg.cn/blog_migrate/1ce8a57e641f92178180d5444afd848e.png)
2.3 设置父节点
设置分辨率为50进入游戏模式后,场景中出现了许多立方体,场景视图中也随之出现。
![695f33f431067c0c94b8aff066f7b3b7.png](https://i-blog.csdnimg.cn/blog_migrate/f0cba835617f79877bd7e0828748e4df.png)
这些立方体目前是根对象,但它们应该是Graph的子对象才更合适。在实例化立方体之后,我们可以通过调用它Transform组件的SetParent方法来设置节点关系。我们需要提供新父节点的Transform组件。我们可以通过Graph继承的transform属性直接访问图形对象的Transform组件。
for
![3232b098fa5037782c2dcbfab632b746.png](https://i-blog.csdnimg.cn/blog_migrate/6e5f9eab07d0ffab9ff950771552f70a.png)
当设置一个新的父节点时,Unity将尝试保持对象的原始世界坐标,旋转和缩放。在本例中,我们不需要那样做。我们可以通过向SetParent提供false作为第二参数来表明这一点。
point.SetParent(transform, false);
3. 为图形着色
白色的图形看起来不太好看。我们可以用另外一种纯色,但这也不是很有趣。我们可以根据点的位置来确定它的颜色。
调整每个立方体颜色的直接方式是设置材质的颜色属性。我们可以在循环中完成。由于每个立方体将得到不同的颜色,这意味着最终每个对象将会有一个独立的材质。虽然这样做可行,但是效率却不高。如果我们可以使用一种使用位置作为颜色的单一材质,那就简单多了。不幸的是,Unity并没有提供这样的材质。我们需要自己创建。
3.1 创建自定义shader
GPU运行shader程序来渲染3D对象。Unity的材质资源决定使用哪个shader并且允许属性可配置。我们需要创建一个自定义shader来获得我们想要的功能。用过Assets->Create->Shader->Standard Surface Shader创建一个名为ColoredPoint的shader。
![9b0fb66ccf42a26bb8d7ca252eff390d.png](https://i-blog.csdnimg.cn/blog_migrate/f6ee81fcec000b521ef812b28bb325a5.png)
现在我们有了一个着色器资源,你可以像打开脚本一样打开它。着色器文件包含顶一个表面着色器的代码,它使用与C#不同的语法。下面是改文件的内容,为了简洁,删除了所有注释行。
Shader
Ps:表面着色器是如何工作的? Unity提供了一个框架来快速生成执行默认光照计算的着色器。你可以通过调整某些值来影响默认光照计算。这样的着色器称为表面着色器。
我们的新着色器的属性有一个纯色,纹理以及光泽和金属度。由于我们的颜色将基于点的位置,所以我们不需要颜色和纹理属性。以下代码是删除了所有不需要的代码,留下了纯黑的albedo并且alpha值为1.
Shader
Ps:什么是albedo和alpha。
材料的漫反射颜色被称为albedo。Albedo是拉丁写法。它描述了r、g、b通道中有多少是被漫反射的,其余的被吸收了。
Alpha是用来衡量不透明度的。Alpha为0时,表面时完全透明的;Alpha为1时,是完全不透明的。
此时,shader是不能编译的,因为表面着色器不能使用空的输入结构。这个结构是我们定义需要什么自定义数据为像素着色的地方。在我们的例子中,我们需要点的坐标。我们可以通过向input中添加float3 worldPos来获取世界坐标。
struct
Ps:这意味着移动图形也会影响到颜色吗?
是的。使用这种方法,只有当图形位于原点时,才能正确着色。
还要注意,这个位置是由每个顶点决定的。在我们的例子中,这是一个立方体的每个角。颜色将在立方体的各个面上进行插值。立方体越大,颜色转换就越明显。
现在我们有了一个功能shader,为它创建一个ColoredPoint的材质。通过shader下拉列表选择Custom->Colored Point让材质使用自定义shader。
![3a8f290b0fe80d462fd65e458ec364d2.png](https://i-blog.csdnimg.cn/blog_migrate/1a02c8ba0e6fbf72840ccd1abb244419.png)
用这个材质替换立方体预制体资源的默认材质。可以通过拖拽这个材质到预制体资源上完成。
3.2 基于世界坐标的着色
当进入游戏模式,我们的图形将实例化黑色的立方体。为了给它们颜色,我们需要修改o.Albedo。使它的红色分量等于X坐标来代替保持默认值。
o
![dd7da4e0bd4e46b48877fcc08eac89fe.png](https://i-blog.csdnimg.cn/blog_migrate/2840e7b6c2020c21815f27a073d29ab8.png)
Ps:我们不需要完整的实例o.Albedo?
我们不需要设置g和b组件,因为在调用surf代码之前它们已经被值为0.
X坐标为正的立方体变得越来越红。X坐标为负的保持黑色,由于颜色值不能为负。为将红色从-1转换到1,我们需要将坐标减半并加上0.5。
o
![d9278bd6c868342127af89650f6736c4.png](https://i-blog.csdnimg.cn/blog_migrate/ce0c3d8d94a9d1c3846fcfb89b1e088f.png)
我们用同样的计算方式让Y坐标表示绿色分量。在shader中,我们可以在一行中用IN.worldPos.xy为o.Albedo.rg赋值。
o
![6db9fe62-802b-eb11-8da9-e4434bdf6706.png](http://p01.5ceimg.com/content/6db9fe62-802b-eb11-8da9-e4434bdf6706.png)
红色加绿色就变成了黄色,所以我们的图形从浅绿色变成了黄色。如果Y坐标从-1开始,我们也会得到深绿色。例如,修改Graph。Awake的代码让它显示函数
position
![02e62bc07a40297d0f47519b857e4826.png](https://i-blog.csdnimg.cn/blog_migrate/3c91c60867bd94f896148d8880f93861.png)
4. 让图形动起来
显示静态图形很有用,但是动起来的图形更好看。让我们添加对动画函数的支持。这是通过将时间作为函数的额外参数实现的,使用f(x,t)代替f(x),t表示时间。
4.1 保持对点的追踪
要使图形有动画效果,我们必须随着时间的推移调整点的位置。我们可以在每次更新的时候删除所有的点再创建新的点来实现,但那是很低效的方式。更好的方式是使用同样的点,每次Update调整它们的位置。为了简单起见,我们将使用一个字段来保存点的引用。为Graph添加一个Transform类型的points字段。
private
这个字段允许我们持有单个点的引用,但是我们需要所有点的引用。为了实现这一点,我们需要使用一个数组。我们可以在类型后面添加[]将字段转换为数组。
private
现在points是一个元素类型为Transform的数组。数组是对象,而不是简单的值类型。我们必须显示的创建并用一个字段引用它。这是通过在new关键字后面加上数组类型实现的,在我们的例子中就是new Transform[]。在Awake方法中,在循环之前创建一个数组并赋值给points。
points
当穿件数组是,需要指出大小。这定义了它拥有多少元素,元素个数在创建之后不能改变。这个长度在创建数组时写在[]之内。在我们的例子中,它的长度等于resolution。
points
现在我们可以为数组填充点的引用。通过在数组字段或变量后面的[]中写入索引来访问数组元素。第一个数组元素的索引就像循环迭代计数器一样从0开始。所以我们可以通过他来访问适当的数组元素。
points
现在我们循环遍历points数组。由于数组的长度和resolution一样,我们也可以用它来约束循环。每个数组都有一个长度属性。
points
4.2 更新点
要是图形具有实际的动画效果,我们需要在组件的Update方法中设置点的Y坐标。所以我们不再需要在Awake中计算它们。
Vector3
添加一个带有代码块中没有任何代码的for循环的Update方法。
private
每次迭代,我们都从获取对当前数组元素的引用开始,然后我们检索那个点的位置。
for
现在我们能够推导出位置的Y坐标,就像我们之前做的那样。
for
因为vector不是对象,我们仅仅调整了局部变量。要把它应用到点上,我们必须重新设置它的位置。
for
Ps:我们不能直接给point.localPosition.y赋值吗?
如果localPosition是一个字段,那么这是可能的。我们可以直接设置点位置的Y坐标。然而,localPosition是一个属性。它返回一个向量给我们,或者接受一个向量设置。所以我们最终调整的是一个局部向量的值,这根本不会影响点的位置。由于我们没有显示的将其存储在变量中,所以该操作没有任何意义,并会产生编译错误。
4.3 显示一条Sine曲线
从现在开始,在游戏模式下,每一帧都会获取图形点的位置。我们只是没有注意到,因为它们总是在相同的位置。我们必须在这个函数中加入时间才能使它改变。然而,简单的添加时间将导致图形上升并迅速的消失在视图之外。为了防止这种情况的发生,我们必须使用一个函数,它是在一个固定范围之内发生变化的。Sine函数就比较理想, 所有我们使用
position
![7b13b712ee81e1c0c809e7f40eff29e1.png](https://i-blog.csdnimg.cn/blog_migrate/8c66b1cd36b4c3e631e80a7e8b98eb0c.png)
Ps:Mathf的作用是啥?
它是一个包含一组处理数字和向量的数学函数和常数的结构体。由于它处理浮点数,所以被赋予了f后缀。
正弦曲线在-1~1之间波动。它在每2π单位之间重复。由于我们图形的X坐标在-1~1之间,我们目前看到的只是不到重复模式的三分之一。为了看到完整的正弦曲线,我们使用
position
![204701c0b946e40ede5edf33b53445d2.png](https://i-blog.csdnimg.cn/blog_migrate/e38a7a3b8709faf2e357acfa0f4a9a41.png)
Ps:什么是正弦波和π?
Sin是一个作用于角度的三角函数。在我们的例子中,最有用的例子是半径为1的单位圆。在这个圆上的每个点都有一个角度θ与之对应,就像2D坐标一样。定义这些点的坐标系的方式之一是[sin(θ),sin(θ+π/2)]。这表示从圆的顶部开始,沿顺时针方向旋转。可以用cos替换sin(θ+π/2),表示为[sin(θ),cos(θ)]。
![f8e43f71077bd929b0e05fdb991b9f0c.png](https://i-blog.csdnimg.cn/blog_migrate/a5db24b1f2f8c65694b87d498efb0544.png)
这个角θ用弧度表示,弧度表示单位圆沿圆周的距离。180°表示沿圆周行进的距离为π=3.14。所以完整的圆周距离为2π。换句话说π是圆周距离和直径的比例。
要使函数具有动画效果,在计算sin之前将当前游戏时间添加到X坐标上。如果我们用时间乘以π,函数将每两秒重复一次。所以使用
position