构造一个分形大体步骤 实例化游戏对象 使用递归 使用协程 添加随机数 分形是有趣,并且美丽的。在这篇文章中,我们将写一个C#脚本,修改一些分形的行为(旋转、颜色、网格等等)。 假设你已经了解了Unity的基础操作和UnityC#基础。如果你完成过时钟的教程,那么你就有了一定的基础可以继续阅读下去。 最终效果 怎么制作分形呢?我们想要创建一个3D分形。我们使用分形的详细概念便能知道怎么做。我们可以应用这个概念到UnityHierarchy面板中的一个游戏对象上去。首先,使用一些根(root)对象,然后添加根对象的子对象,这些子对象除了大小不同外,其他地方完全相同。这样做会很麻烦,所以我们将创建一个脚本去帮助我们完成这件事情(就是为根物体创建子物体)。 首先,创建一个空的项目。然后定位灯光的方向,移动相机到一个合适的角度,你怎么喜欢怎么来。继续创建一个材质(material)用于给分形使用。使用系统自带的高光(specular)Shader,保持系统默认参数即可,使用高光Shader会比默认的Shader效果很更好一些。 创建一个空的游戏物体,并且让它位于原点(0,0,0),并命名为“Fractal”,这就是我们分形的根(root)物体。然后创建一个C#脚本,命名为“Fractal”,并为空物体(Fractal)添加该脚本。 usingUnityEngine; usingSystem.Collections; publicclassFractal:MonoBehaviour { } 目前所做的如上图所示 显示一些东西我们的分形会是什么样子的呢?让我们制作它吧。首先,为我们的脚本添加一个公有的(接下来公有的指的是public)Mesh(网格)和一个公有的Material(材质)变量。然后再Start()函数里为我们的Fractal物体添加MeshFilter和MeshRenderer组件(通过AddComponent添加)。在添加组件的同时,顺便为该组件添加相应的参数。 usingUnityEngine; usingSystem.Collections; publicclassFractal:MonoBehaviour { publicMeshmesh; publicMaterialmaterial; voidStart() { gameObject.AddComponentMeshFilter().mesh=mesh; gameObject.AddComponentMeshRenderer().material=material; } } 什么是Mesh(网格)? Mesh是通过使用图形硬件绘制复杂的东西的一个概念。它是一个导入到Unity的3D模型的默认形状或者通过代码来生成。 在3D空间中,Mesh包含至少一个点的集合,通过这些点去组合一些基本的二维图形,使用比较广的是三角形。Mesh的表面一般由一个个三角形组成。通常,你不会注意到这些三角形。 什么是Material(材质)? 材质常用于定义物体对象的视觉效果。他们可以从非常简单(如一些常见的颜色)到复杂(如高光、反射等等)。 材质由一个Shade(着色器)r和Shader的一些需要的数据组成。Shader也是一种脚本语言。用于通知显卡应该如何绘制物体。 Start()什么时候调用的? 一旦游戏被运行,Start()函数在组件创建之后通过Unity来调用。它在Update()函数执行的前调用,且只调用一次。 AddComponent()函数是如何运作的? AddConponent()函数用于为游戏物体添加一个新的组件。该函数添加一个组件,且还会返回一个参数。这就是我们为什么可以立即访问组件的值的原因(就是上面的代码:gameObject.AddComponentMeshFilter().mesh=mesh;)。你也可以使用该组件的变量,如下所示。 MeshFilterfilter=gameObject.AddComponentMeshFilter(); filter.mesh=mesh; 这是一个比较通用的方法。它实际上是一个模板方法(就是所谓的泛型)。你可以在尖括号()去指定添加组件的类型。 现在,我们可以为Fractal脚本的属性卡槽分配相应的组件了。Material卡槽可以使用拖拽的方式将之前制作好的Fractal材质拖进卡槽中,然而Mesh卡槽可以点击卡槽旁边的圆点来进行选择分配(在这里我们选择的是Cube)。这时候点击Play你将会在场景中看到一个立方体。 在Play模式下显示 制作子物体我应该如何创建分形物体的子物体呢?最简单的方式就是在Start()函数里创建一个新的游戏物体,并且为其添加Fractal组件(也就是Fractal脚本)。你可以尝试一下。 voidStart() { gameObject.AddComponentMeshFilter().mesh=mesh; gameObject.AddComponentMeshRenderer().material=material; newGameObject(FractalChild).AddComponentFractal(); } new做了什么事情? new关键字用于构造一个对象(object)或者结构(struct)的实例化。它允许调用一个构造函数来实例化对象或结构体。 问题是,每一个新的分形的实例化都将会创建另一个新的分形。这将会导致每一帧都执行该操作,永不停止。让它运行一会儿,你的计算机将会由于内存耗尽导致崩溃。典型的递归算法不会停止,这将会耗尽你的电脑资源,导致堆栈溢出异常或崩溃。 为了防止这类事情发生,我们采用最大深度的概念。我们实例化第一个分形的深度为0。它的子物体的深度将为1。子物体的子物体的深度为2,以此类推。直到到达最大深度为止。 添加一个公有的名为“maxDepth“的int类的变量。在Inspector面板中设置它的值为4。再次添加一个私有的(private)名为”depth“的int类型的变量。如果当前深度小于最大深度的话,我们就创建一个新的子物体。 当我们进入Play模式时会发生什么事情呢? usingUnityEngine; usingSystem.Collections; publicclassFractal:MonoBehaviour { publicMeshmesh; publicMaterialmaterial; publicintmaxDepth; privateintdepth; voidStart() { gameObject.AddComponentMeshFilter().mesh=mesh; gameObject.AddComponentMeshRenderer().material=material; if(depthmaxDepth) { Debug.Log(depth); newGameObject(FractalChild).AddComponentFractal(); } } } 最大深度 只有一个子物体被创建。为什么呢?因为我们还没有给depth变量赋值,它的值便一直为0.因为0比4小,我们的根物体便创建了一个子物体。无论如何,我们从来没有设置子物体的maxDepth,因为它的值一直为0。 除此之外,子物体也缺少一个材质(material)和网格(Meth)。我们需要从父物体上复制那些参数。让我们添加一个新的函数,该函数负责所有的必要的初始化。 usingUnityEngine; usingSystem.Collections; publicclassFractal:MonoBehaviour { publicMeshmesh; publicMaterialmaterial; publicintmaxDepth; privateintdepth; voidStart() { gameObject.AddComponentMeshFilter().mesh=mesh; gameObject.AddComponentMeshRenderer().material=material; if(depthmaxDepth) { Debug.Log(depth); newGameObject(FractalChild).AddComponentFractal().Initialize(this); } } voidInitialize(Fractalparent) { mesh=parent.mesh; material=parent.material; maxDepth=parent.maxDepth; depth=parent.depth+1; } } this是什么? this关键字指的是当前对象(就是自身)或结构体的方法被调用。它常用于使用当前类中隐藏的行为或属性(比如继承下来的一些函数和属性等等)。例如,我们可以通过depth访问该变量,也可以通过this.depth来访问改变了,没有本质的区别。 当你需要传递一个对象自身的引用时,你便可以使用this关键字来实现。例如上面的Initialize(this)一样。为什么?因为我们调用的是新的子物体上的Initialize()函数,而不是父物体上的Initialize()函数。 Initialize()在Start()函数之前调用? 是的。是这样的。首先,创建新的游戏物体。然后一个新的Fractal组件(就是Fractal脚本)被创建出来,并且添加到新的游戏物体上。就在这时候,Awake()和OnEnable()函数将会被调用。在调用Initialize()函数之后,AddComponent()函数便执行完毕。直到下一帧之前Start()函数不会被调用。 PS:这里的Start()函数指的是子物体的Start()函数。而这里的Initialize()函数指的是父物体的Initialize()函数。 当我们进入Play模式时,不出所料,四个子物体便被创建出来。但是他们不是真正的子物体,因为他们都出现在Hierarchy面板上,而不是作为Fractal的子物体出现。因此,我们要做的就是让子物体的transform等于父物体的transform,如下所示: usingUnityEngine; usingSystem.Collections; publicclassFractal:MonoBehaviour { publicMeshmesh; publicMaterialmaterial; publicintmaxDepth; privateintdepth; voidStart() { gameObject.AddComponentMeshFilter().mesh=mesh; gameObject.AddComponentMeshRenderer().material=material; if(depthmaxDepth) { Debug.Log(depth); newGameObject(FractalChild).AddComponentFractal().Initialize(this); } } voidInitialize(Fractalparent) { mesh=parent.mesh; material=parent.material; maxDepth=parent.maxDepth; depth=parent.depth+1; transform.parent=parent.transform; } } 子物体没有使用嵌套 子物体使用了嵌套 塑造子物体到目前为止,子物体算是叠加到父物体上了,这意味着我们只能看到一个正方体。我们需要在本地坐标上移动它们,使它们都能被看到。我们还需要较少它们的大小,让子物体变得比父物体更小。 首先缩小大小。要缩小多大呢?让我们使用一个名为“childScale”的新的变量来配置大小,并且在Inspector面板中设置它的值为0.5。不要忘了从父物体向子物体传递这个值。然后使用它去设置自物体的本地大小。 下一步,我们应该在什么地方移动子物体呢?让我们简单的移动它们,以便它们能触碰到他们的父物体。我们采取父物体各个方向上的大小,这样有利于保持立方体的形状。向上移动,然后让子物体与父物体能触碰到,因此我们需要一个额外的操作,那就是使用子物体大小的一半加上0.5(下面我会画图说明),如下所示: usingUnityEngine; usingSystem.Collections; publicclassFractal:MonoBehaviour { publicMeshmesh; publicMaterialmaterial; publicintmaxDepth; privateintdepth; publicfloatchildScale; voidStart() { gameObject.AddComponentMeshFilter().mesh=mesh; gameObject.AddComponentMeshRenderer().material=material; if(depthmaxDepth) { Debug.Log(depth); newGameObject(FractalChild).AddComponentFractal().Initialize(this); } } voidInitialize(Fractalparent) { mesh=parent.mesh; material=parent.material; maxDepth=parent.maxDepth; depth=parent.depth+1; childScale=parent.childScale; transform.parent=parent.transform; transform.localScale=Vector3.one*childScale; transform.localPosition=Vector3.up*(0.5f+0.5f*childScale); } } 下面是画图说明: Play模式下便能看到如图所示 调整childScale的值(由0.3-0.7),便能看到如图所示效果。 制作多个子物体我们创建的形状像个塔一样,这不是一个分形。我们需要它有分支,通过每一个父物体创建多个子物体。这很简单,只是创建第二个对象而已,但是它需要在不同的方向生长。那么,让我们为我们的Intialize()函数添加一个方向参数,用它来定位孩子初始化的位置方向。 这是什么意思呢? 这意味着我没有做任何改变,便能省略了一大块代码。我们只需要传入一个方向便能在某个方向生成子物体,然而我们只添加了一句代码,便能做到这个事情,是不是很酷啊!! Play模式下便能看到如图效果 这回看起来有点分形的样子了!!你知道这些立方体都是在什么时候创建的吗?他们在几帧内就可以创建完成了,因此它生长的很快,我们几乎无法看到生长的过程。我认为延缓它们生长的过程,且能看到这个过程是非常有趣的。那么,我们可以通过使用一个协程(Coroutie)来完成这件事情。 可以把协程看作一个函数,你可以在协程里插入暂停(延迟操作)语句。当函数调用暂停语句时,其余的程序仍在执行。尽管这种观点过于简单化了,但现在我们需要使用它(协程)。 删除那两条创建子物体的语句(在下面代码中以红色显示),然后创建一个新的函数,命名为“CreateChildren”。这个函数需要使用IEnumerator作为返回类型参数,IEnumerator被放在System.Collections命名空间下。这就是为什么Unity默认模板里包含这两个命名空间的原因(UnityEngine和System.Collections)。 我们需要在Start()函数里通过使用StartCoroutine()函数来开启这个协程。 然后添加一条暂停语句,让创建子物体的操作延迟0.5秒(yieldreturnnewWaitForSeconds(0.5f))。如下所示: usingUnityEngine; usingSystem.Collections; publicclassFractal:MonoBehaviour { publicMeshmesh; publicMaterialmaterial; publicintmaxDepth; privateintdepth; publicfloatchildScale; voidStart() { gameObject.AddComponentMeshFilter().mesh=mesh; gameObject.AddComponentMeshRenderer().material=material; if(depthmaxDepth) { //newGameObject(FractalChild).AddComponentFractal().Initialize(this,Vector3.up); //newGameObject(FractalChild).AddComponentFractal().Initialize(this,Vector3.right); StartCoroutine(CreateChildren()); } } IEnumeratorCreateChildren() { yieldreturnnewWaitForSeconds(0.5f); newGameObject(FractalChild).AddComponentFractal().Initialize(this,Vector3.up); yieldreturnnewWaitForSeconds(0.5f); newGameObject(FractalChild).AddComponentFractal().Initialize(this,Vector3.right); } voidInitialize(Fractalparent,Vector3direction) { mesh=parent.mesh; material=parent.material; maxDepth=parent.maxDepth; depth=parent.depth+1; childScale=parent.childScale; transform.parent=parent.transform; transform.localScale=Vector3.one*childScale; transform.localPosition=Vector3.up*(0.5f+0.5f*childScale); transform.localPosition=direction*(0.5f+0.5f*childScale); } } 什么是枚举器(enumerator)? 枚举器是一个存放一条一条数据的容器,它和数组很相像,数据都是有序存放的。枚举器或迭代器是一个对象提供了一个接口。System.Collections.IEnumerator描述了这个接口。 我们为什么需要枚举器呢?因为协程使用了枚举器。这也是为什么Unity默认使用那两个命名空间的原因。 return做了些什么事情? 使用return关键字去指明一个函数执行完成。有时你还必须return函数指定的类型。如果是函数的返回类型为void则无需返回任何指定类型。 在一个函数块内可以有多个return声明。在这种情况下,表明该函数有不同的退出函数的方法。通常情况下使用if或ifelse的嵌套来指定退出函数的方式。 yield做了些什么事情? 在语句中使用yield关键字,表示在该关键字所在的方法、运算符或get访问器是迭代器。通过使用yield定义迭代器,可在实现自定义集合类型的IEnumerable和IEnumerator模式时无需其他显式类,我们的CreateChildren()函数恰好就是IEnumerator类型,因此我们可以使用yield作为它的返回类型。 协程是如何工作的? 当你在Unity中创建一个协程时,事实上你创建的是一个迭代器。当你使用StartCoroutine()函数调用这个协程时,该协程的每一个任务项将被储存在每一帧中,并在每一帧中执行任务,直到完成。 yiled的声明语句就是一个任务项。 你可以在你需要的地方使用yiled语句。如使用WaitForSeconds()去让控制你的代码的执行。但总的来说,可以把协程看作诗一个简单的迭代器。 现在,我们可以观察分形的生长过程了!!使用这种方式(协程)你有没有发现一个问题呢?让我们为每一个子物体添加三个子物体,三个子物体的方向分别为上、左、右。 usingUnityEngine; usingSystem.Collections; publicclassFractal:MonoBehaviour { publicMeshmesh; publicMaterialmaterial; publicintmaxDepth; privateintdepth; publicfloatchildScale; voidStart() { gameObject.AddComponentMeshFilter().mesh=mesh; gameObject.AddComponentMeshRenderer().material=material; if(depthmaxDepth) { //newGameObject(FractalChild).AddComponentFractal().Initialize(this,Vector3.up); //newGameObject(FractalChild).AddComponentFractal().Initialize(this,Vector3.right); StartCoroutine(CreateChildren()); } } IEnumeratorCreateChildren() { yieldreturnnewWaitForSeconds(0.5f); newGameObject(FractalChild).AddComponentFractal().Initialize(this,Vector3.up); yieldreturnnewWaitForSeconds(0.5f); newGameObject(FractalChild).AddComponentFractal().Initialize(this,Vector3.right); yieldreturnnewWaitForSeconds(0.5f); newGameObject(FractalChild).AddComponentFractal().Initialize(this,Vector3.left); } voidInitialize(Fractalparent,Vector3direction) { mesh=parent.mesh; material=parent.material; maxDepth=parent.maxDepth; depth=parent.depth+1; childScale=parent.childScale; transform.parent=parent.transform; transform.localScale=Vector3.one*childScale; transform.localPosition=Vector3.up*(0.5f+0.5f*childScale); transform.localPosition=direction*(0.5f+0.5f*childScale); } } 标准和透视视图 打开透视视图 现在的问题就是子物体生长的方向和父物体相同(就是父物体生长的方向有上,左,右,而子物体的方向也是上,左,右)。这便导致了一个物体和父物体重叠了(如向左边生长的子物体,它向右生长的子物体将与父物体重叠,有点绕,下面有图解)。解决这个问题,我们需要旋转子物体的方向来避免发生重叠。 我们将通过在Initialize()函数中添加一个方向参数解决这个问题。它将使用一个四元素来设置新生成的子物体的本地旋转。朝上生长的子物体不需要旋转,朝右生长的子物体需要朝顺时针旋转90°,朝左生长的子物体需要朝逆时针旋转90°。 usingUnityEngine; usingSystem.Collections; publicclassFractal:MonoBehaviour { publicMeshmesh; publicMaterialmaterial; publicintmaxDepth; privateintdepth; publicfloatchildScale; voidStart() { gameObject.AddComponentMeshFilter().mesh=mesh; gameObject.AddComponentMeshRenderer().material=material; if(depthmaxDepth) { //newGameObject(FractalChild).AddComponentFractal().Initialize(this,Vector3.up); //newGameObject(FractalChild).AddComponentFractal().Initialize(this,Vector3.right); StartCoroutine(CreateChildren()); } } IEnumeratorCreateChildren() { yieldreturnnewWaitForSeconds(0.5f); newGameObject(FractalChild).AddComponentFractal() .Initialize(this,Vector3.up,Quaternion.identity); yieldreturnnewWaitForSeconds(0.5f); newGameObject(FractalChild).AddComponentFractal() .Initialize(this,Vector3.right,Quaternion.Euler(0f,0f,-90f)); yieldreturnnewWaitForSeconds(0.5f); newGameObject(FractalChild).AddComponentFractal() .Initialize(this,Vector3.left,Quaternion.Euler(0f,0f,90f)); } voidInitialize(Fractalparent,Vector3direction,Quaternionorientation) { mesh=parent.mesh; material=parent.material; maxDepth=parent.maxDepth; depth=parent.depth+1; childScale=parent.childScale; transform.parent=parent.transform; transform.localScale=Vector3.one*childScale; transform.localPosition=Vector3.up*(0.5f+0.5f*childScale); transform.localPosition=direction*(0.5f+0.5f*childScale); transform.localRotation=orientation; } } 解决问题之后的显示效果 现在子物体已经旋转了,它们不会很快就生长完成了。但是你也许会注意到那些小的子物体还是发生了重叠现象。这是由于我将Scale设置为0.5。你可以缩小Scale的大小去解决这个问题,或者换成球体试试。 球体的显示效果图 由于白癜风怎么治最好北京哪家治白癜风医院比较好
|