找回密码
 立即注册
查看: 379|回复: 0

[笔记] Unity基础 02_创建图形

[复制链接]
发表于 2023-1-4 10:25 | 显示全部楼层 |阅读模式
原文地址 :Building a Graph

  • 创建一个预置。 实例化多个多维数据集。 展示一个数学函数。 创建表面着色器和着色器图形。 制作图表动画。
创建一条线


  • 编程时很好地理解数学是必不可少的。在最基本的层面上,数学是对代表数字的符号的操作。求解一个方程可以归结为重写一组符号,使其成为另一组(通常更短)的符号。数学规则决定了重写是如何完成的。
  • 例如,我们有函数 f(x)=x+1。我们可以用一个数字来代替它的 x 参数,比如说3。这导致了 f(3)=3+1=4。我们提供3作为输入参数,最后以4作为输出。我们可以说函数映射3到4。一种更简短的写法是输入输出对,比如(3,4)。我们可以创建许多对的形式(x,f(x)),例如(5,6)和(8,9)和(1,2)和(6,7)。但是当我们根据输入的数字对配对进行排序时,就更容易理解这个函数了。(1,2)和(2,3)和(3,4)等等。
  • 该功能  f(x)=x+1很好理解。f(x)=(x-1)^4+5x^3-8x2+3x更难。我们可以写下一些输入-输出对,但这可能不会让我们很好地理解它所代表的映射。我们需要很多紧密相连的点。这将导致数字的海洋,难以解析。相反,我们可以将这些对解释为形式的二维坐标[x f(x)]。这是一个2D向量,上面的数字代表X轴上的水平坐标,下面的数字代表Y轴上的垂直坐标。换句话说, y=f(x)。我们可以在一个表面上画出这些点。如果我们使用足够多的非常靠近的点,我们最终会得到一条线。结果是一个图表



Graph with x between 2 and 2, made with Desmos.


  • 看一个图可以很快给我们一个函数行为的概念。这是一个方便的工具,所以让我们在Unity中创建一个。我们将从一个新项目开始,正如上一教程的第一部分所描述的
Prefabs

  • 通过在适当的坐标上放置点来创建图形。为此,我们需要一个点的三维可视化。我们将简单地使用Unity的默认立方体游戏对象。向场景中添加一个,并将其命名为Point
  • Are cubes the best way to visualize graphs  ?  你也可以使用粒子系统或线段,但是单个立方体是最简单的
  • 我们将使用一个自定义组件来创建这个立方体的许多实例,并正确定位它们。为了做到这一点,我们将把立方体变成一个游戏对象模板。将多维数据集从“层次结构”窗口拖到“项目”窗口中。这将创建一个新的资产,称为预置。它是一个预先制作的游戏对象,存在于项目中,而不是场景中



Point prefab asset, one and two column layout.


  • 我们用来创建预设的游戏对象仍然存在于场景中,但是现在是一个预设实例。它在“层次”窗口中有一个蓝色图标,右侧有一个箭头。它的检查器的标题也表明它是一个预置,并显示更多的控件。位置和旋转现在以粗体文本显示,这表明实例的值覆盖了预设的值。您对实例所做的任何其他更改也将以这种方式表示



Point prefab instance.


  • 当选择预置资产时,它的检查员将显示它的根游戏对象和一个大按钮来打开预置
  • 单击“打开预设”按钮将使场景窗口显示一个只包含预设对象层次的场景。也可以通过实例的“打开”按钮、“层次”窗口中实例旁边的右箭头,或者通过双击“项目”窗口中的资源来打开。当一个预置有一个复杂的层次时,这是有用的,但是这不是我们简单的点预置的情况



Hierarchy window for our prefab.


  • 你可以通过层次窗口中名称左边的箭头退出预设的场景
Why is the background of the prefab scene uniform dark blue?

  • 预设是一种配置游戏对象的便捷方式。如果你改变预设资产,它在任何场景中的所有实例都会以同样的方式改变。例如,改变预设的比例也会改变仍然在场景中的立方体的比例。但是,每个实例都使用自己的位置和旋转。此外,游戏对象实例可以被修改,这将覆盖预设的值。请注意,在播放模式下,预设和实例之间的关系被破坏
  • 我们将使用一个脚本来创建预置的实例,这意味着我们不再需要当前场景中的预置实例。因此,通过编辑/删除、指示的键盘快捷键或层次窗口中的上下文菜单来删除它
Graph Component

  • 我们需要一个C#脚本 Component 来生成一个带有我们的点预制图形。创建一个并将其命名为Graph



Graph C# asset in Scripts folder.


  • 我们从一个简单的类开始,它扩展了MonoBehaviour,因此它可以用作游戏对象的组件。给它一个可序列化的字段来保存对预置的引用,用于实例化点,名为pointPrefab。我们将需要访问Transform组件来定位这些点,因此使其成为字段的类型



序列化的字段来保存对预置的引用


  • 将一个空游戏对象添加到场景中,并将其命名为Graph。确保其位置和旋转为零,并且其比例为1。将我们的Graph 组件添加到该对象中。然后拖动我们的预置资产到图形的点预置字段。<b>空游戏对象<b>现在拥有对预设的变换组件的引用



Graph game object with reference to prefab.

Instantiating Prefabs

  • 游戏对象的实例化是通过对象完成的。实例化方法。这是 Unity 对象类型的一个公开可用的方法,Graph通过扩展MonoBehaviour间接继承了它。Instantiate方法克隆作为参数传递给它的任何Unity对象。在预设的情况下,它将导致一个实例被添加到当前场景中。当我们的Graph组件醒来时,让我们这样做
using UnityEngine;

public class Graph : MonoBehaviour
{
    [SerializeField]
    Transform pointPrefab ;

    void Awake()
    {
        Instantiate(pointPrefab);
    }
}

  • What is the full inheritance chain of MonoBehaviour? MonoBehaviour扩展了行为,行为扩展了组件,组件扩展了对象
  • 如果我们现在进入游戏模式,在世界原点将会产生一个点预置的实例。它的名字和预设的名字一样,只是附加了(克隆)



Instantiated prefab, looking down the Z axis in the scene window.


  • Can you have the scene window open while in play mode ? 是的,但是当进入游戏模式时,Unity总是将游戏窗口推到前台。如果游戏窗口与场景窗口共享一个面板,那么场景窗口将被隐藏。但是你可以在游戏模式下切换回场景窗口。此外,您可以配置编辑器布局,以便一个或多个游戏和场景窗口同时可见。请记住,Unity必须渲染所有这些窗口,所以你打开的窗口越多,速度就越慢。
  • 要将 point 放在其他地方,我们需要调整实例的位置。Instantiate方法为我们提供了对它所创建的任何内容的引用。因为我们给了它一个对转换组件的引用,这就是我们得到的回报。让我们用一个变量来跟踪它



  • 在上一个教程中,我们通过将四元数分配给轴变换的 localRotation 属性来旋转时钟臂。更改位置的工作方式相同,只是我们必须将一个3D向量赋给 localPosition 属性。 3D向量是使用Vector3结构类型创建的。例如,让我们将点的X坐标设置为1,将其Y和Z坐标设置为零。Vector3有一个右属性,给了我们这样一个向量。使用它来设置点的位置



Cube one unit to the right.


  • 当进入游戏模式时,我们仍然得到一个立方体,只是位置略有不同。让我们实例化第二个并把它放在右边一个额外的步骤中。这可以通过将右向量乘以2来实现。重复实例化和定位,然后将乘法添加到新代码中



重复实例化和定位


  • 这段代码会产生一个编译器错误,因为我们试图定义point变量两次。如果我们想使用另一个变量,我们必须给它一个不同的名字。或者,我们重用已经拥有的变量。一旦我们完成了对第一个点的引用,我们就不需要再坚持了,所以把新点赋给同一个变量
    void Awake()
    {
       Transform point = Instantiate(pointPrefab);
       point.localPosition = Vector3.right;

       point = Instantiate(pointPrefab);
       point.localPosition = Vector3.right * 2.0f;
    }



Two instances, with X coordinates 1 and 2.

Code Loops:

  • 让我们创造更多的点,直到我们有10个。我们可以重复同样的代码八次以上,但这将是非常低效的编程。理想情况下,我们只编写一点的代码,并指示程序多次执行,略有变化。 while语句可用于使代码块重复。将其应用于我们方法的前两条语句,并删除其他语句。
  • while 关键字后面必须跟一个圆括号内的表达式。while后面的代码块只有在表达式的计算结果为true时才会执行。之后,程序将循环回到while语句。如果此时表达式再次评估为true,代码块将再次执行。如此重复,直到表达式的计算结果为假。然后程序跳过while语句后面的代码块,并在它下面继续。 所以我们必须在while之后添加一个表达式。我们必须小心确保循环不会永远重复。无限循环导致程序停滞,需要用户手动终止。编译的最安全的表达式就是false
  • Can we define point inside the loop ? 是的。尽管代码会重复,但我们只定义了一次变量。它在循环的每次迭代中都被重用,就像我们之前手工做的那样。 你也可以在循环之前定义点。这也允许你在循环之外使用变量。否则,它的范围仅限于while循环的块。
  • 限制循环可以通过记录代码重复的次数来实现。我们可以用一个整型变量来跟踪它。它的类型是int。它将包含循环的迭代次数,所以我们把它命名为I。它的初始值是零。为了能够在while表达式中使用它,必须在它上面定义它。
  • 每一次迭代,通过将它设置为自身加1,将数字增加1



  • 现在 i 在第一次迭代开始时变成 1,在第二次迭代开始时变成 2,依此类推。但是while表达式是在每次迭代之前计算的。所以在第一次迭代之前,i 是0,第二次迭代之前是 1,依此类推。所以在第十次迭代后,i 是10。此时我们想要停止循环,所以它的表达式应该计算为false。换句话说,只要我不到十岁,我们就应该继续。数学上,这可以表示为  i < 10。在代码中也是这样写的,使用 < 小于运算符



  • 现在,进入游戏模式后,我们将获得十个立方体。但最后都在同一个位置。要将它们沿X轴排成一行,将右向量乘以 i 。
point.localPosition = Vector3.right * i;  



Ten cubes in a row along the X axis.


  • 注意,目前第一个立方体的X坐标为1,最后一个立方体的X坐标为10。让我们改变这一点,让我们从零开始,将第一个立方体定位在原点。我们可以通过将右乘以(i - 1)而不是 i ,将所有点向左移动一个单位。但是,我们可以通过在乘法之后,在块的末尾增加 i,而不是在开始时增加 i,来跳过额外的减法。
Concise Syntax:

  • 因为循环一定次数是很常见的,所以保持循环代码简洁是很方便的。一些句法糖可以帮助我们做到这一点。 首先,让我们考虑增加迭代次数。当执行x = x * y形式的运算时,它可以缩短为x *= y。这适用于作用于两个操作数的所有运算符



  • 更进一步,当一个数增加或减少1时,可以缩短为++x或 -- x。
  • 赋值语句的一个特性是它们也可以用作表达式。这意味着你可以写成y = (x += 3)这将使x增加3,并将结果分配给y。这表明我们可以在while表达式中增加 i,从而缩短代码块



  • 然而,现在我们在比较之前而不是之后增加 i,这将导致少一次迭代。特别是在这种情况下,递增和递减运算符也可以放在变量之后,而不是之前。该表达式的结果是更改前的原始值



  • 虽然while语句适用于所有类型的循环,但是有一种替代语法特别适合于范围内的迭代。这是for循环。它的工作方式类似while,只是迭代器变量声明和它的比较都包含在圆括号中,用分号分隔



  • 这将产生一个编译器错误,因为在另一个分号之后,还有第三部分用于递增迭代器,使其与比较分开。这一部分在每次迭代结束时执行


Changing the Domain

  • 目前,我们的点被赋予X坐标0到9。使用函数时,这不是一个方便的范围。通常,x的取值范围为0–1,或者当使用以零为中心的函数时,取值范围为1–1。让我们相应地重新定位我们的观点。 沿着两个单位长的线段放置十个立方体会使它们重叠。
  • 为了防止这种情况,我们将缩小它们的规模。默认情况下,每个立方体在每个维度上的大小都是1,所以为了使它们合适,我们必须将它们的比例缩小到 2/10= 1/5. 我们可以通过将每个点的局部比例设置为Vector3.one属性除以5来实现。除法是用 / 斜杠运算符完成的
        for(int i = 0 ; i < 10 ; i++)
        {         
          Transform point     = Instantiate(pointPrefab);
          point.localPosition = Vector3.right * (i / 5.0f - 1.0f);      
          point.localScale    = Vector3.one / 5.0f;         
            
        }



From 1 to 0.8.


  • 现在,第一个立方体的X坐标为 1,而最后一个立方体的X坐标为0.8。但是,立方体大小为0.2。由于立方体以其位置为中心,第一个立方体的左侧为1.1,而最后一个立方体的右侧为0.9。为了用我们的立方体整齐地填充1–1范围,我们必须将它们向右移动半个立方体。这可以通过在除以 i  之前加0.5来实现
        for(int i = 0 ; i < 10 ; i++)
        {         
          Transform point     = Instantiate(pointPrefab);
          point.localPosition = Vector3.right * ( (i + 0.5f) / 5.0f - 1.0f);      
          point.localScale    = Vector3.one / 5.0f;         
            
        }



From 1 to 1.

Hoisting the Vectors out of the Loop

  • 尽管所有的立方体都有相同的尺度,我们在循环的每一次迭代中都重新计算它。我们不需要这样做,比例是不变的。相反,我们可以在循环之前计算它一次,将它存储在一个缩放变量中,并在循环中使用它。
    void Awake()
    {
        var sacle = Vector3.one / 5.0f;
        
        for(int i = 0 ; i < 10 ; i++)
        {         
          Transform point     = Instantiate(pointPrefab);
          point.localPosition = Vector3.right * ( (i + 0.5f) / 5.0f - 1.0f);      
          point.localScale    = sacle;      
            
        }
      

  • 我们也可以为循环前的位置定义一个变量。当我们沿着X轴创建一条线时,我们只需要调整循环内部位置的X坐标。所以我们不再需要乘以Vector3.right
  • 这将导致编译器错误,抱怨使用了未赋值的变量。发生这种情况是因为我们在给某物指定位置时,还没有设置它的Y和Z坐标。我们可以通过将position初始设置为零向量,将Vector3.zero赋给它来解决这个问题
void Awake()
    {
        var position = Vector3.zero ;
        var sacle = Vector3.one / 5.0f;
        
        for(int i = 0 ; i < 10 ; i++)
        {         
          span class="n">Transform point     = Instantiate(pointPrefab);
          position.x =  (i + 0.5f) / 5.0f - 1.0f ;
          point.localPosition = position ;      
          point.localScale    = sacle;      
            
        }
      

    }
Using X to Define Y

  • 这个想法是我们的立方体的位置被定义为  [x,f(x),0],所以我们可以用它们来显示一个函数。在这一点上,Y坐标总是零,这代表平凡的函数f(x)=0。为了显示不同的函数,我们必须确定循环内部的Y坐标,而不是在循环之前。让我们从让Y等于X开始,代表函数f(x)=x



Y equals X


  • 一个稍微不太明显的功能是f(x)=x^2,定义了一条最小值为零的抛物线



Y equals X squared

Creating More Cubes:

  • 虽然我们在这一点上有一个功能图,但它很难看。因为我们只使用了十个立方体,所以建议的线条看起来非常块状和不连续。如果我们用更多更小的立方体会更好看。
  • Variable Resolution:我们可以使它可配置,而不是使用固定数量的立方体。要实现这一点,请为图形的解析添加一个可序列化的整数字段。给它一个默认值10,这就是我们现在使用的。



Configurable resolution.


  • 现在,我们可以通过检查器来改变图形的分辨率。但是,并非所有整数都是有效的分辨率。至少他们必须是积极的。我们可以指示检查员为我们的解决方案设定一个范围。这是通过将Range属性附加到它来实现的。我们可以将resolution的两个属性放在各自的方括号中,或者将它们组合在一个逗号分隔的属性列表中。让我们做后者



Resolution slider set to 50.


  • Does this guarantee that resolution is constrained to 10–100?“范围”属性所做的只是指示检查器在该范围内使用滑块。它不会以任何其他方式影响分辨率。所以我们可以写代码给它赋一个超出范围的值,但是我们不会这样做
Variable Instantiation

  • 为了利用配置的分辨率,我们必须改变我们实例化的立方体的数量。不再是在Awake中循环固定的次数,迭代次数现在受分辨率的限制,不再总是10次。因此,如果分辨率设置为50,我们将在进入播放模式后获得50个立方体



  • 我们还必须调整立方体的比例和位置,使它们保持在1–1域内。我们每次迭代的每步大小现在是2除以分辨率。将该值存储在一个变量中,并使用它来计算立方体的比例及其X坐标
using UnityEngine;

public class Graph : MonoBehaviour
{
    [SerializeField]
    Transform pointPrefab ;
     [SerializeField,Range(10,100)]
    int resolution =10 ;

    void Awake()
    {
        float step = 2.0f/ resolution;
        var position = Vector3.zero * step ;
        var sacle = Vector3.one  * step ;
        
        for(int i = 0 ; i < resolution ; i++)
        {         
          Transform point     = Instantiate(pointPrefab);
          position.x =  (i + 0.5f) * step - 1.0f ;
          position.y =  position.x * position.x ;
          point.localPosition = position ;      
          point.localScale    = sacle;      
            
        }
      

    }
}



Using resolution 50.


  • 进入分辨率为50的播放模式后,大量实例化的立方体出现在场景中,因此也出现在项目窗口中



  • 这些点目前是根对象,但是它们作为图形对象的子对象是有意义的。我们可以在实例化一个点之后建立这种关系,方法是调用它的Transform组件的SetParent方法,将所需的父转换传递给它。我们可以通过graph的transform属性得到Graph对象的Transform组件,它是从component继承来的。在循环块的末尾这样做
  • 当设置新的父对象时,Unity将尝试保持对象的原始世界位置、旋转和缩放。我们不需要这个。我们可以通过将false作为第二个参数传递给SetParent来表示这一点


Coloring the Graph

  • 白色的图表看起来并不美观。我们可以使用另一种纯色,但那也不是很有趣。用一个点的位置来确定它的颜色更有意思。 调整每个立方体颜色的直接方法是设置其材质的颜色属性。
  • 我们可以在循环中进行。由于每个立方体将获得不同的颜色,这意味着我们将为每个对象创建一个唯一的材质实例。当我们稍后制作图表动画时,我们也必须一直调整这些材质。虽然这种方法可行,但效率不是很高。如果能使用直接以位置为颜色的单一材质就好很多了。可惜Unity没有这样的素材。所以我们自己做吧
Universal Render Pipeline

  • 除了默认的渲染管道,Unity还有通用和高清渲染管道,简称URP和HDRP。两种渲染管道都有不同的功能和限制。当前的默认渲染管道仍然起作用,但其功能集已被冻结。几年后,URP可能会成为违约国。让我们的图表也适用于URP。 如果你还没有使用URP,请转到软件包管理器,安装最新的通用RP软件包。我的情况是10.4.0



URP package installed.


  • 这不会自动使Unity使用URP。我们首先要通过Assets/Create/Rendering/Universal Render Pipeline/Pipeline Asset(正向渲染器)为它创建一个资源。我把它命名为URP。这也将自动为渲染器创建另一个资产,在我的例子中命名为URP渲染器



URP assets in separate folder, one and two column layout.


  • 接下来,转到项目设置的图形部分,并将URP资源分配给可脚本化渲染器管道设置字段



Using the URP.


  • 要稍后切换回默认渲染管道,只需将可脚本化的渲染器管道设置设置为None。这只能在编辑器中完成,不能在构建的独立应用程序中更改渲染管道
Creating a Shader Graph

  • 我们当前的材质只能使用默认的渲染管道,不能使用URP。因此,当URP被使用时,它被Unity的错误材料所取代,这是固体洋红色
  • 我们必须为URP创建一个单独的着色器。我们可以自己写一个,但是目前很难,升级到新的URP版本时可能会崩溃。最好的方法是使用Unity的着色器图形包来可视化地设计着色器。URP依赖于这个软件包,所以它会自动与URP软件包一起安装。 通过资产/创建/着色器/通用渲染管道/光照着色器图形创建一个新的着色器图形,并将其命名为点URP



Point URP shader graph asset, one and two column layout.


  • 可以通过在项目窗口中双击图形的资源或按其检查器中的“打开着色器编辑器”按钮来打开图形。这将为它打开一个着色器图形窗口,该窗口可能会被多个节点和面板弄得乱七八糟。这些是黑板、图形检查器和主预览面板,它们可以调整大小,也可以通过工具栏按钮隐藏。还有两个链接节点:一个顶点节点和一个片段节点。这两个用于配置着色器图形的输出



Default lit shader graph with everything visible.


  • 着色器图形由表示数据或操作的节点组成。目前,片段节点的平滑度值设定为 0.5。要使其成为可配置的着色器属性,请按点URP背板面板上的加号按钮,选择“Float ”,并将新条目命名为“smooth”。它向黑板添加一个表示属性的圆形按钮。选择它并将图形检查器切换到其“节点设置”选项卡,以查看该属性的配置



Smoothness connected.


  • 现在,您可以通过“保存资源”工具栏按钮保存图形,并创建一个名为“点URP”的材质来使用它。着色器的菜单项是着色器图形/点URP。然后使用该材质而不是点表面来制作点预制件



Material for URP using our shader graph.

Programming with Nodes

  • 为了给这些点上色,我们必须从一个位置节点开始。通过在图形的空白部分打开一个上下文菜单并从中选择New Node来创建一个。选择 Input / Geometry / Position 或只搜索位置



World position node.


  • 我们现在有一个位置节点,默认设置为世界空间。当光标悬停在预览可视化上时,可以通过按下出现的向上箭头来折叠预览可视化
  • 使用相同的方法创建一个乘法和一个加法节点。使用这些将位置的XY分量缩放0.5,然后加上0.5,同时将Z设置为零。这些节点根据它们所连接的内容来调整它们的输入类型。所以首先连接节点,然后填入它们的常量输入。然后将结果连接到 Fragment 的基色输入



Compacted shader graph.


  • 保存着色器资源后,我们现在在播放模式中获得了与使用默认渲染管道时相同的颜色点。除此之外,在播放模式下,调试更新程序会出现在一个单独的DontDestroyOnLoad场景中。这是为了调试URP,可以忽略



URP debug updater in play mode.


  • 从这一点上,你可以使用默认的渲染管道或URP。从一个切换到另一个后,你还必须改变预置点的材质,否则它会变成洋红色。如果您对图形生成的着色器代码感兴趣,可以通过图形检查器的“查看生成的着色器”按钮来获得它
图形运动


  • 显示静态图是有用的,但是看动态图更有趣。所以让我们添加对动画功能的支持。这是通过使用以下形式的函数将时间作为附加函数参数来实现的 f(x,t)而不是f(x),其中 t 是时候了
Keeping Track of the Points

  • 为了制作图表动画,我们必须随着时间的推移调整它的点。我们可以通过删除所有的点并在每次更新时创建新的点来做到这一点,但这是一种低效的方法。最好继续使用相同的点,每次更新时调整它们的位置。为了实现这一点,我们将使用一个字段来保存对我们的点的引用。向转换类型的图形添加点字段



  • 该字段允许我们引用单个点,但是我们需要访问所有的点。我们可以通过在字段类型后面加上空的方括号,将字段转换成数组
Transform[] points;

  • points 字段现在是对一个数组的引用,该数组的元素属于Transform类型。数组是对象,不是简单的值。我们必须显式地创建这样一个对象,并让我们的字段引用它。这是通过编写new后跟数组类型来实现的,在我们的例子中是new Transform[]。在我们的循环之前,在Awake中创建数组,并将它赋给点。
  • 当创建一个数组时,我们必须指定它的长度。这定义了它有多少个元素,这些元素在创建后不能更改。构造数组时,长度写在方括号内。使其等于图形的分辨率
points = new Transform[resolution];

  • 现在我们可以用对我们的点的引用来填充数组。访问数组元素是通过在数组引用后面的方括号中写入其索引来完成的。数组索引从第一个元素的零开始,就像我们循环的迭代计数器一样。所以我们可以用它来赋值给合适的数组元素



  • 如果我们在一行中多次赋值同一个东西,我们可以把这些赋值链接在一起,因为赋值表达式的结果就是被赋值的内容,就像前面的教程中解释的那样
  • 我们现在循环遍历我们的点数组。因为数组的长度与分辨率相同,所以我们也可以用它来约束我们的循环。为此,每个数组都有一个长度属性,所以让我们使用它
Transform point =  points = Instantiate(pointPrefab);

  • 我们现在循环遍历我们的点数组。因为数组的长度与分辨率相同,所以我们也可以用它来约束我们的循环。为此,每个数组都有一个长度属性,所以让我们使用它


Updating the Points

  • 为了调整每一帧的图形,我们需要在更新方法中设置点的Y坐标。所以我们不再需要在清醒时计算它们。我们仍然可以在这里设置X坐标,因为我们不会改变它们



  • 像Awake一样添加一个带有for循环的Update方法,但是它的块中还没有任何代码



  • 我们将通过获取对当前数组元素的引用并将其存储在变量中来开始循环的每次迭代



  • 之后,我们检索该点的本地位置,并将其存储在一个变量中



  • 现在我们可以根据X设置位置的Y坐标,就像我们之前做的那样



  • 因为位置是一个结构,所以我们只调整了局部变量的值。为了应用它,我们必须再次设置它的位置



  • Couldn't we directly set point.localPosition.y?  如果localPosition是一个公共字段,那么我们可以直接设置点的位置的Y坐标。但是,localPosition是一个属性。它会将向量值的副本传递给我们,或者复制我们赋予它的值。所以我们最终会调整一个局部向量值,这根本不会影响点的位置。因为我们没有先将它显式地存储在变量中,所以该操作没有意义,并且会产生一个编译器错误
Showing a Sine Wave

  • 从现在开始,在游戏模式中,我们图表中的点会在每一帧中定位。我们还没有注意到这一点,因为它们总是在相同的位置结束。我们必须将时间纳入函数中,以使其发生变化。然而,简单地增加时间会导致函数上升并迅速消失在视野之外。为了防止这种情况发生,我们必须使用一个变化但保持在固定范围内的函数。正弦函数非常适合这种情况,所以我们将使用 f(x)=sin(x)。我们可以使用Mathf。Sin方法来计算它



  • 正弦波在1和1之间振荡。它每隔2π重复一次,发音为两个饼形单位,这意味着它的周期大约为6.28。由于我们的图形的X坐标在1和1之间,我们目前看到的重复模式不到三分之一。为了看到它的整体,我们把X乘以π,所以我们最后得到 f(x)=sin(πx)。我们可以使用Mathf。π常数是π的近似值



  • 要激活这个函数,在计算正弦函数之前,将当前游戏时间加到X上。它是通过Time.time找到的。如果我们也用π来换算时间,这个函数将每两秒重复一次。所以使用 f(x,t)=sin(π(x+t)),其中t是经过的游戏时间。这将随着时间的推移推进正弦波,使其向负X方向移动



  • 因为Time.time的值对于循环的每次迭代都是相同的,所以我们可以在循环之外提升属性调用




https://www.zhihu.com/video/1577751745912070144

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-12-22 10:47 , Processed in 0.096683 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表