最近开始学习unity,在网上找到了一个教程合集,其中有大量翻译的catlike教程,实际学习了一下感觉很好,比较细致深入。地址如下: https://zhuanlan.zhihu.com/p/151238164
构造分形(递归的实现细节)
本节学习了分形的构造方法以及一些让分形更加好看的技巧。
1. gameObject 和 Component 的关系
gameObject是存在于游戏场景中的对象,承载着游戏世界的种种操作和功能,本身是不可见的。不同的gameObject之间可以具有层次关系,外层的对象被称为内层对象的父对象,相应的内层对象则被称为子对象。这种父子关系让父对象的某些变换(如旋转、运动)得以传递给子对象,也定义了除世界坐标之外,以父对象为基点构成的局部坐标系。
gameObject显示在unity的hierarchy栏,层次关系一目了然。
component(组件)则是gameObject(对象)的“左膀右臂”。没有组件的对象无法在游戏场景中发挥作用。每个对象至少拥有一个transform 组件,用于标定该对象的坐标、大小和旋转角度。除了transform组件外,还可以为对象定义碰撞体、着色器等组件。
我们用脚本定义的继承自MonoBehavior的类,实际上也是一种组件,在代码中使用GameObjec类的AddComponent方法添加。
new GameObject("Fractal Child").
AddComponent<Fractal>().Initialize(this, i);
(教程里的某段代码,Fractal是定义的类)
AddComponent方法可以创建特定类型的新组件,并将其附加到游戏对象,返回对其的引用。这就是为什么我们可以立即访问组件的值。当然也可以使用中间变量。
2. 在类中声明自身类型的变量(延伸学习)
本问题以c++为例子的答案见:https://www.zhihu.com/question/341035289/answer/1047747826
明显,直接声明是不被接受的。声明自身类型的指针会分配一个指针域,而声明静态类型的成员变量则稍微难理解一点。(只能声明,定义要在类外面做)
由于静态成员实际上属于类而不是对象,所以会被单独分配一块内存空间。实例化对象的过程并不会因为这个静态成员的类型是类自身而产生嵌套。所以在外面定义这个静态成员的时候,相当于实例化了该类的一个对象,对象所需的内存大小里并不包括静态成员所需的内存空间。(有点儿绕)
实际使用的时候,则可以无限套娃,想用多少层用多少层,实际上只是在反复自己访问自己。
3. start方法和awake方法的区别
为分形创造子节点的时候,我们在脚本的start方法中新建了一个GameObject类的对象,并为其添加Fractal组件。组件添加完毕后则会调用Initialize对数据成员进行初始化。
某个阶段的代码:
private void Start(){
gameObject.AddComponent<MeshFilter> ().mesh = mesh;
gameObject.AddComponent<MeshRenderer> ().material = material;
if (depth < maxDepth) {
new GameObject ("Fractal Child").
AddComponent<Fractal> ().Initialize(this);
}
}
private void Initialize(Fractal parent){
mesh = parent.mesh;
material = parent.material;
maxDepth = parent.maxDepth;
depth = parent.depth + 1;
}
注意此处:为了避免无穷无尽的分形导致电脑死机,教程为分形的创建设立了最大层数,在创建新的分形前需要将当前层数与最大层数进行比对,若当前层数已达到最大层数,分形的创建就停止了。
此时Start方法的调用时刻就显得尤为重要。因为Start方法是自动调用的,我起初理解为当Fractal组件被挂载时它就会被调用,可是一旦如此,子对象的Start方法又会试图产生下一层分形,此时父对象的Initialize还未被调用,depth字段未更新,分形的创建将不会停止。
实际情况却是,创建过程老老实实地停在了规定最大层数,说明子对象的Start调用在Initialize之后。
除此之外,在之前的学习中还出现了一个Awake方法,也是自动调用的,那么Start和 Awake 的区别,以及他们和自定义方法Initialize的调用时机分别是什么呢。
查阅了一些资料之后发现 顺序是awake ->Initialize ->start 具体可看本链接:https://gameinstitute.qq.com/community/detail/128335
稍微总结一下,就是Awake和Start在对象的声明周期中都只会被调用一次,不同的是,Awake在对象被挂上脚本的时候就会被调用了,不管这个对象或脚本是否是激活(enable)状态,而Start一定是对象被激活的时候才会被调用。
可以这样理解:在挂载脚本的那一帧,Awake被系统调用,并确认其状态是否是激活的(默认激活),如果是,那么下一帧将自动调用Start。
这也就解释了Initialize的问题,Initialize是父对象Start方法的一部分,父对象的Start方法会在同一帧内完成执行,而在父对象Start方法执行完毕后的下一帧,才会执行新创建的子对象的Start方法。
不过,上面链接中提到的Awake和find方法的问题,我认为find是否能够找到对象,只取决于对象是否处于active状态,和是否挂载脚本没有关系,自然更和对象脚本的Awake方法是否执行没关系了。
4. localPosition 与 localScale
在Initialize中有过这样几行代码(与最终代码略有不同):
transform.parent = parent.transform;
transform.localScale = Vector3.one * childScale;
transform.localPosition = Vector3.up * (0.5f + 0.5f * childScale);
其中,Vector3.up = (0, 1, 0), Vector3.one = (1, 1, 1), childScale = 0.5 这两句代码是用来初始化生成的新对象的大小和位置的,可是很容易就能发现,里面所有的值都是常量,计算出来的结果也是两个常量。那么为什么体现在最终视图里的,是一个比一个小的立方体,和一个叠一个的状态呢。
深入了解之后我才知道,就像在1中提到的那样,如果在对象之间建立父子关系,子对象就可以使用以父对象为基点建立的局部坐标系。这里的localPosition、localScale乃至后来的localRotation都是使用局部坐标系的意思。
局部坐标系:以父对象的坐标为原点,父对象的x、y、z轴为坐标轴方向,父对象在该方向上的scale为单位长度
以父对象为原点,父对象的x、y、z轴为坐标轴方向这两条比较好理解,然而以父对象的scale为单位长度就不太理所当然了。我也不能肯定这一点因为网上没有搜索到相关的资料。
以下是用于测试的代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestFractal : MonoBehaviour
{
public float childScale;
public Mesh mesh;
public Material material;
private float depth;
private const float maxDepth = 5;
void Start()
{
gameObject.AddComponent<MeshFilter>().mesh = mesh;
gameObject.AddComponent<MeshRenderer>().material = material ;
if (depth < maxDepth)
{
new GameObject("Child").AddComponent<TestFractal>().Initialize(this);
}
}
private void Initialize( TestFractal parent)
{
childScale = parent.childScale;
depth = parent.depth + 1;
mesh = parent.mesh;
material = parent.material;
transform.parent = parent.transform;
transform.localScale = Vector3.one * childScale;
transform.localPosition = Vector3.up * (0.5f + 0.5f * childScale);
}
}
在unity中将根对象的scale设置成(1, 2, 1) 当在y轴方向生成分形时,它是这样的:
在x轴方向生成分形时是这样的: 而这产生两个结果的代码只有一个地方不同
transform.localPosition = Vector3.up * (0.5f + 0.5f * childScale);
向量数乘的结果分别为(0, 0.75, 0) (0.75, 0, 0),它们明明拥有一样的模长,却引导着子对象在世界坐标系中移动了不一样长的距离(图1中是1.5,图2中是0.75)。能够解释这一现象的原因就只有子对象以父对象scale为单位长度这一点了。
5. 分形数量的递归公式
在教程里出现了这样一个数学公式用来计算分形的数量:f(n) = f(n-1)*5 + 1
首先用g(x)表示每一层的分形个数,g(0)=1, g(1) = 5, g(2) = 25, …, g(n) = 5^n 那么f(n-1) = g(0) + g(1) + … + g(n-1) f(n) = g(0) + g(1) + … + g(n) = g(0) + 5 * ( g(0) + g(1) +… + g(n-1)) = 1 + 5 * f(n-1)
之所以专门算一下这个,是因为此公式与我想当然的结果 f(n) = f(n-1) + 5^n 不一样
6. 协程、StartCoroutine 、IEnumerater 与yield return
资料:https://www.cnblogs.com/fly-100/p/3910515.html unity文档:https://docs.unity.cn/cn/2018.4/ScriptReference/MonoBehaviour.StartCoroutine.html StartCoroutine、IEnumerater和yield return是一块儿用的,对于初学者来说十分难以理解,所以我是似懂非懂,相当的迷惑 协程,协助程序,可以帮助脚本跨帧执行某些操作。有的时候一个操作需要的时间比较长,或者期望他可以以一种较慢的节奏执行,但游戏主程序不可能为这项操作中断帧刷新和其他操作的执行,这时就可以使用协程来处理。协程是可中断的,且在重新启动后可从中断的位置而非开头继续向下执行。 IEnumerater是一个类型,可以翻译为迭代器,作用是记录协程从暂停(yield return后)到重新启动需要执行的操作,以及重新启动的位置。(这一点是我猜的) yield return的值不同,协程需要暂停的时间也不同。可看http://blog.sina.com.cn/s/blog_aaa4ce8d010131kr.html
结尾:附catlike分形教程最终代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Fractal : MonoBehaviour
{
public Mesh mesh;
public Material material;
public int maxDepth;
public float childScale;
private int depth;
private static Vector3[] childDirections =
{
Vector3.up,
Vector3.right,
Vector3.left,
Vector3.forward,
Vector3.back
};
private static Quaternion[] childOrientations =
{
Quaternion.identity,
Quaternion.Euler(0f, 0f, -90f),
Quaternion.Euler(0f, 0f, 90f),
Quaternion.Euler(90f, 0f, 0f),
Quaternion.Euler(-90f, 0f, 0f)
};
private Material[ , ] materials;
private void InitializeMaterials ()
{
materials = new Material[maxDepth + 1, 2];
for (int i = 0; i <= maxDepth; i++)
{
float t = i / (maxDepth - 1f);
t *= t;
materials[i, 0] = new Material(material);
materials[i, 0].color = Color.Lerp(Color.white, Color.yellow, t);
materials[i, 1] = new Material(material);
materials[i, 1].color = Color.Lerp(Color.white, Color.cyan, t);
}
materials[maxDepth, 0].color = Color.magenta;
materials[maxDepth, 1].color = Color.red;
}
public Mesh[] meshes;
public float spawnProbability;
public float maxRotationSpeed;
private float rotationSpeed;
private void Start()
{
rotationSpeed = Random.Range(-maxRotationSpeed, maxRotationSpeed);
if (materials == null)
{
InitializeMaterials();
}
gameObject.AddComponent<MeshFilter>().mesh =
meshes[Random.Range(0, meshes.Length)];
gameObject.AddComponent<MeshRenderer>().material =
materials[depth, Random.Range(0, 2)];
if ( depth < maxDepth)
{
StartCoroutine(CreateChildren());
}
}
private IEnumerator CreateChildren ()
{
for ( int i = 0; i < childDirections.Length; i++)
{
if(Random.value < spawnProbability)
{
yield return new WaitForSeconds(Random.Range(0.1f, 0.5f));
new GameObject("Fractal Child").
AddComponent<Fractal>().Initialize(this, i);
}
}
}
private void Initialize (Fractal parent, int childIndex)
{
mesh = parent.mesh;
meshes = parent.meshes;
materials = parent.materials;
material = parent.material;
maxDepth = parent.maxDepth;
childScale = parent.childScale;
depth = parent.depth + 1;
spawnProbability = parent.spawnProbability;
maxRotationSpeed = parent.maxRotationSpeed;
transform.parent = parent.transform;
transform.localScale = Vector3.one * childScale;
transform.localPosition = childDirections[childIndex] * (0.5f + 0.5f * childScale);
transform.localRotation = childOrientations[childIndex];
}
private void Update ()
{
transform.Rotate(0f, rotationSpeed * Time.deltaTime, 0f);
}
}
|