在制作游戏项目时需要创作大部分内容,有些时候也或许需要在游戏运行时从代码中创作一个目标,在 Unity 中运用 Instantiate(实例化) 功能就可以生成目标,它允许根据项目中的现有目标在场景中创立新目标。
Unity 供给的 Instantiate 函数相对比较简单,但有许多不同的运用办法,并且,因为创立新目标或许是一个非常消耗资源的过程,所以创立和毁掉目标的办法会对功能产生很大影响。
那么在本文中,将了解运用实例化的几个不同办法。
在Unity中如何实例化对象
Unity 中的实例化是目标类的一个函数,它可以在游戏运行时从代码中在场景中生成新目标对象。
它经过传入对现有游戏目标对象的引用来工作,然后将其复制为克隆。
例如:
public GameObject objectToSpawn;
void Start()
{
Instantiate(objectToSpawn);
}
这基本上与游戏在运行形式下手动拷贝层次结构中的目标的方法相同,其中正在创立的目标将承继与从中克隆它的目标对象相同的特点和方位。
这就是为什么在使用实例化时引用的目标被称为原始目标,而创立的目标将被标记为克隆的原因。

可是,尽管能够创立场景中现已存在的游戏对象的副本,但除非这是想要的行为,否则运用 Instantiate 的典型方法是从项目的资产中创立预制游戏对象的副本。
如何生成预制件
实例化功能允许在场景中创立一个基于游戏资产中现有预制件的新目标对象。这很有用,因为它允许提早设置目标对象模板,然后能够在游戏运行时从代码中创立该模板。
如何在Unity中创立预制件
在复制预制件之前,需要首先创立一个。最简单的方法是在场景中构建目标对象。然后,完成后,将目标拖到项目的资产中以创立它的预制件。
新预制件将出现在项目的资源中,而场景中的目标将变为蓝色,突出显示预制件模板与层次结构中的目标之间的连接。
然后,将能够从场景中删去目标并在实例化功用中运用预制件。

Instantiate 函数的最基本版别选用单个参数,它是对要复制的游戏目标对象的引用。
要生成预制件,只需将要创立的目标对象从项目的资产中拖到公共游戏目标引用中,然后将其传递给 Instantiate 函数。

这将根据对象预制件在场景中创立一个克隆目标,这对于在游戏中创立作为项目中的资产进行管理的新目标对象十分有用。

实例化预制件也比简略地拷贝场景中的另一个目标更安全,由于如果原始目标被损坏,测验将其与 Instantiate 函数一起使用会导致错误。
但是通过在脚本中保留对对象预制的引用,可能会意外更改预制资产本身,而不是创立的对象克隆。
发生这种情况是因为可以在游戏以播放模式运行时从代码修改预制件。
尽管 Unity 会阻止造成任何严重损坏,例如通过损坏预制件意外删除它,但仍然可以打开或关闭组件,或更改预制件脚本中的数据。
这可能会导致对象参数出现问题,假如不小心以预制件而不是其克隆为目标,则更改其任何变量基本上都会更改预制件的默认值。
这是一个问题,由于访问的是资产,而不是正在运转的场景中的对象实例,因此更改将持续存在,即使在退出播放形式后也是如此。
例如,假如从运用刚体和磕碰器的预制件创立新对象,然后意外更改原始对象,例如封闭其磕碰器,以完全相同的方式再次运转相同的场景,将导致物体从地板上掉下来,然而曾经它是没有的。
如何获取对实例化对象的引用
使用 Instantiate 生成预制件时,有时很容易通过混淆对原始对象的引用与克隆来意外修改预制件资产。
但在使用 Instantiate 时获取对创立的实际对象的引用非常简单。
为此,只需在调用 Instantiate 函数时将一个 Game Object 变量设置为它的结果。
例如:
void SpawnOject()
{
GameObject newObject = Instantiate(objectToSpawn);
}
这是有用的,由于 Instantiate 返回了对它创立的新目标的引用,这对于在生成目标后立即设置它很有用。
如何设置新对象的旋转和位置
虽然 Instantiate 的基本重载方法只需要一个游戏对象引用即可工作,但很可能,需要传递比创立新对象时更多的信息,对吧。
默认情况下,实例化的目标会承继从中克隆它的目标的方位和旋转。那么这就意味着,假如在场景中克隆一个目标,新目标将创立在与原始目标相同的方位。
假如实例化一个预制件,Unity 将在项目中使用资产的方位和旋转数据。可是,可以经过手动传递目标的方位和旋转数据来选择创立目标的方位。
例如:
Instantiate(GameObject objectToSpawn, Vector3 position, Quaternion rotation);
它通过传入对象位置的向量 3 值和四元数旋转值来确定对象的方向。然而,虽然 Vector 3 值使用起来相对简单,但作为 Unity 在幕后使用的旋转数据类型,四元数并不是人类可读的。
虽然可以使用 Euler 函数手动传入旋转,但在许多情况下,脚本附加到的对象的位置和旋转使用现有旋转作为参考会更容易。
例如:
Instantiate(objectToSpawn, transform.position, transform.rotation);
或者预制对象的原始旋转
Instantiate(objectToSpawn, transform.position,objectToSpawn.transform.rotation);
或者,可以使用 Quaternion 类的 identity 属性来指定 no rotation ,因为这两个参数都是必需的,这对于指定新对象的位置很有用,但不能用于指定其旋转。
例如:
Instantiate(objectToSpawn, transform.position, Quaternion.identity);
如何实例化具有随机位置和旋转的对象
使用 Instantiate 时,可能并不总是希望设置特定的位置和旋转,并且对于某些任务,可能希望在随机点生成对象。
通常,创立随机位置涉及确定可以在其中生成随机 Vector 3 点的有限区域,然后将其传递给 Instantiate 函数。
例如,可以使用 Random 类的 Inside Unit Sphere 属性获取随机方向,当乘以半径并添加到原点时,将返回球体内的随机位置。
例如:
public GameObject objectToSpawn;
public Vector3 origin = Vector3.zero;
public float radius = 10;
void Start()
{
// 在半径为 10 个单位的球体中查找一个位置
Vector3 randomPosition = origin + Random.insideUnitSphere * radius;
Instantiate(objectToSpawn, randomPosition, Quaternion.identity);
}
使用最小和最大位置时,可以在立方体内生成随机位置。
例如:
public GameObject objectToSpawn;
public Vector3 minPosition;
public Vector3 maxPosition;
void Start()
{
Vector3 randomPosition = new Vector3(
Random.Range(minPosition.x, maxPosition.x),
Random.Range(minPosition.y, maxPosition.y),
Random.Range(minPosition.z, maxPosition.z)
);
Instantiate(objectToSpawn, randomPosition, Quaternion.identity);
}
虽然生成随机位置可能需要一些工作,但是,创立随机旋转要简单得多,这是因为 Random 类具有用于返回随机旋转值的内置函数,
例如:
public GameObject objectToSpawn;
public Vector3 minPosition;
public Vector3 maxPosition;
void Start()
{
//创立一个随机旋转的对象
Instantiate(objectToSpawn, transform.position, Random.random);
}
很多时候,如果只是生成简单的独立对象,则使用它们自己的位置和旋转值来实例化它们可能就是需要做的所有事情。
但是,如果想创立一个属于其他东西的对象怎么办?
在Unity中如何将对象实例化为子对象
使用 Instantiate 函数创立对象时,可以将其创立为另一个对象的子对象。通常可以这样做来创立一个附加到另一个对象的对象。
或者,如果以编程方式创立大量对象,将它们嵌套在父对象下可以使层次结构更易于使用。

例如:
void SpawnOject()
{
Instantiate(objectToSpawn, transform);
}
当将一个对象实例化为另一个对象的子对象时,新对象除了其自己的原始方向外,还将继承父对象的位置和旋转。
因此,例如,如果原始预制件旋转了 90°,并且父对象也旋转了 90°,则创立的对象的旋转将是 a,组合 180°。
这可能很有用,但是,如果希望手动设置实例化对象的位置和旋转,同时仍将其作为子对象附加到另一个对象,也可以这样做,方法是指示函数使用世界空间。
例如:
// 使用其原始位置和旋转值创立一个子对象
Instantiate(objectToSpawn, transform, true);
或者通过使用参数设置位置和旋转值,以及传入父变换。
例如:
// 创立一个子对象,但使用自定义位置和旋转值
Instantiate(objectToSpawn, transform.position, transform.rotation, transform);
如果出于组织原因想要将对象创立为另一个对象的子对象,这将很有用,因为它允许指定对象的变换值,就好像它没有父对象一样。
使用资源如何按名称实例化预制件
从预制件创立对象可以相当简单。只需使用 Inspector 连接想要创立的对象即可。但是,如果出于某种原因不想使用对象引用来生成新对象怎么办?
Resources 文件夹是 Unity 中的一个特殊文件夹,它允许使用预制对象的名称而不是对象引用来实例化预制对象。
每当使用对象引用不方便或不可能时,这都会很有用,并且允许使用路径名来代替,那么你需要在项目中添加一个名为 Resources 的文件夹,然后只需将要创立的对象放入其中。

然后,使用路径名加载资源,将其转换为游戏对象。
例如:
void SpawnOject()
{
GameObject newObject = (GameObject)Instantiate(Resources.Load("Sphere"));
}
Resources 文件夹可用于按路径名加载资产,但也有缺点。例如,默认情况下,Resources 文件夹中的所有资源都会被加载,无论它们是否被使用,这会增加完成的游戏包的大小。
更重要的是,虽然能够将资产换成具有相同名称的另一个资产的灵活性可能很有用,但也很容易弄错文本字符串,从而中断连接。
这意味着,如果打算按其名称加载预制件,而不是使用参考,那么在过度使用之前了解一下 Resources 文件夹的工作原理是值得的。
有关 Resources 文件夹的更多信息,你可以尝试前往 Unity 的官方文档。
在Unity中如何实例化一个空对象
虽然通过常可以使用 Instantiate 创立作为预制件副本的现成对象,但也可以从 Unity 中的代码创立全新的对象。
例如,可以简单地通过创立一个新的游戏对象实例来创立一个空的游戏对象。
例如:
new GameObject();
这将在的场景中添加一个空的游戏对象,位于世界的原点。但是,如果打算以这种方式创立一个对象,那么很有可能,一旦创立了它,就会想用它实际做一些事情。
出于这个原因,就像在声明一个新变量时一样,可能希望在创立空游戏对象时缓存对它的引用。
例如:
GameObject newObject = new GameObject();
当调用它时,也可以设置对象的名称,或者将一系列组件传递给游戏对象的构造函数。
例如:
GameObject newObject = new GameObject("My New Object", typeof(AudioSource), typeof(Rigidbody), typeof(BoxCollider));
或者,由于已经拥有对新的空游戏对象的引用,因此可以在创立它之后根据需要进行设置。
例如,可以通过访问对象的属性手动设置对象的名称、位置或旋转,或者可以通过使用添加组件功能向对象添加组件来赋予对象功能。
例如:
GameObject newObject = new GameObject();
newObject.name = "My New Object";
newObject.transform.position = transform.position;
newObject.transform.rotation = transform.rotation;
AudioSource audioSource = newObject.AddComponent<AudioSource>();
audioSource.playOnAwake = false;
audioSource.loop = true;
newObject.AddComponent<Rigidbody>().isKinematic = true;
newObject.AddComponent<BoxCollider>();
也可以从脚本实例化原始形状,例如立方体和其他形状体。这与在编辑器中创立 3D 形状的方式相同,但可以在代码中完成。

要做到这一点,只需调用 Game Object 类的 Create Primitive 函数,该函数可用于在场景中创立新的球体、平面、四边形、圆柱体或立方体,使用碰撞器,就像创立一个手动 3D 形状一样。
例如:
GameObject newCube = GameObject.CreatePrimitive(PrimitiveType.Cube);
了解如何从代码创立游戏对象,无论它们是基于现有的预制件还是全新的,对于在游戏运行时将对象添加到游戏中非常有用。
虽然这对于在已完成的游戏中创立功能很有用,但它还可以帮助在 Unity 编辑器中构建项目。
例如,使用 Instantiate 可以快速轻松地从代码创立大量对象,这比手动放置对象要容易得多。
那么如何使用 Instantiate 来帮助构建游戏呢?
在Unity中如何实例化多个对象
实例化函数可用于快速轻松地创立大量对象。虽然这在游戏中很有用,例如随机放置的物体,但它在编辑器中也很有用。
通常,这通过在单个帧内迭代循环一定次数来创立多个对象来工作。例如,假设想将五个对象整齐地排成一排。只需使用 for 循环对函数进行五次迭代,每次都更改位置。
例如:
public class SpawnInARow : MonoBehaviour
{
public Vector3 firstPosition;
public float gap = 2;
public GameObject objectToCreate;
void Start()
{
Vector3 position = firstPosition;
for (int i = 0; i < 5; i++)
{
Instantiate(objectToCreate, position, Quaternion.identity);
position.x += gap;
}
}
}
或者,如果想创立一个对象网格,只需将一个 for 循环嵌套在另一个 for 循环中。
例如:
public GameObject gridObject;
public Vector2Int grid;
public float y = 0;
private void Start()
{
GenerateGrid(grid);
}
// Generates a 2D grid on the floor
void GenerateGrid(Vector2Int gridSize)
{
for(int x=0; x < gridSize.x; x++)
{
for (int z = 0; z < gridSize.y; z++)
{
Instantiate(gridObject, new Vector3(x, y, z), Quaternion.identity);
}
}
}
这通过为 x 轴的每次增量生成沿 z 轴的完整行来工作,并且对于将对象放置在大网格中很有用,例如地板上的瓷砖。
但是,一旦在播放模式下创立了对象,怎么能重复使用它们呢?
要在编辑器中实际使用生成的对象,需要在退出播放模式之前保存它们。为此,只需创立要使用的对象,确保将每个实例化的子对象作为创立它的对象的父级,然后,在游戏仍在运行时,将完成的对象拖到您的资产中,将其变成预制件.

然后退出播放模式,之后可以将预制件拖回场景中并解压缩它,从而切断对象与预制件的连接。
然而,虽然一次创立多个对象对各种事情都有用,但有时可能希望以设定的时间间隔定期创立一个对象。
例如,如果想每隔几秒钟创立一个对象呢?
怎么按时间间隔实例化对象
有几种不同的方法可以按时间间隔实例化对象,但一般来说,测量间隔并在经过一定时间后生成新对象的过程在所有方法之间都是相似的。
它通常通过将最后一帧的增量时间添加到浮点值来跟踪每帧经过的时间量。
例如:
public float interval = 5;
float timer;
void Update()
{
timer += Time.deltaTime;
if(timer >= interval)
{
// Spawn an object
// Reset the timer
}
}
一旦计时器记录的时间量等于或大于要测量的时间间隔,就会生成一个对象并重置计时器。
但是,你用来重置计时器的方法可能会改变它的准确度。例如,可能想通过将计时器设置回零来重置计时器,但是,随着时间的推移,这可能会导致它变得不准确。
发生这种情况是因为在时间间隔被识别为已经过去的点上,可能已经过去了额外的时间量,因为每个帧都需要一定量的时间来处理。
但解决方案还是很简单的,无需将计时器重置为零,只需从计时器值中减去所需的时间间隔即可。
例如:
public GameObject objectToCreate;
public float interval = 5;
float timer;
void Update()
{
timer += Time.deltaTime;
if (timer >= interval)
{
Instantiate(objectToCreate);
timer -= interval;
}
}
虽然不可能在帧间隔之间执行函数,这意味着对象将始终在某一帧或下一帧生成,但以这种方式测量时间可以产生重复功能,例如每隔几秒生成一个对象,随着时间的推移更加准确。
或者,Invoke Repeating 使用类似的方法以定时间隔调用函数。
它需要一个字符串值,这是要调用的函数的名称,以及一个用于函数调用之间所需延迟的浮点值。
与相同频率的节拍器相比,减法方法和调用重复都显示为彼此一样准确,而重置方法只是将计时器变回零,随着时间的推移失去同步,因为它们之间的差异框架加起来。
在较大间隔之间生成对象时,几乎不会注意到丢失的时间,这意味着在许多情况下,不会注意到太大的差异。
但是,当以精确的间隔频繁实例化一个对象时,减去计时器间隔而不是重置它可以帮助阻止函数的节奏感觉不稳定。
这在快速生成具有声音效果的对象时尤其明显,例如在重复实例化武器中的子弹或射击弹时。
那这里提到了射击弹,那接下来3D天堂谈一谈如何创立弹丸。
如何在Unity中创立弹丸
使用 Instantiate 函数生成弹丸相对简单。然而,棘手的是将生成的对象的旋转与生成点对齐,以便在正确的位置创立射弹并朝正确的方向移动。
那么如何在 Unity 中实例化一个射弹,并确保它以正确的方式移动呢?
一种方法是使用游戏对象作为生成点,这可能是生成脚本所附加的对象,也可能是触发点的空游戏对象。

然而,重要的是,如果射击对象移动,生成点可以随之移动和旋转。其次,需要确保弹丸和生成点都指向同一个方向。
最简单的方法是确定前进的方向,例如对象的蓝色 z 轴,并确保生成脚本和射弹指向相同的方向。
例如,在射弹上,可以在启用对象后立即沿其前轴向其刚体施加冲击力。
例如:
public class CannonBall : MonoBehaviour
{
public float force = 10;
void Start()
{
GetComponent<Rigidbody>().AddForce(transform.forward * force, ForceMode.Impulse);
}
}
然后,在生成对象时,只需确保生成点也相对于它所附着的对象朝前,并且在创立射弹时,将旋转设置为匹配。
例如:
public class Cannon : MonoBehaviour
{
public GameObject cannonBall;
public Transform spawnPoint;
public AudioSource audioSource;
public AudioClip fireSound;
void FireCannon()
{
audioSource.PlayOneShot(fireSound);
Instantiate(cannonBall, spawnPoint.position, spawnPoint.rotation);
}
}
如果使用 Instantiate 创立射弹,那么很可能会在游戏运行时创立和销毁大量对象,这可能会导致性能问题。
这是因为实例化新目标和毁掉旧目标时产生的自动内存管理。因而,如果重复生成大量目标,那么重复使用旧目标而不是毁掉它们,然后在每次需要新目标时创立它们通常是一个好主意。
这是一个称为对象池的过程,可以帮助避免垃圾收集导致的性能下降,尤其是在低功率设备。
怎么在Unity中使用对象池重用对象
对象池是重用对象而不是反复销毁和实例化它的过程,这会产生垃圾并降低性能。
创立对象池的方法有很多种,但通常涉及维护与特定类型对象相关联的数据集合,例如数组或列表。
当不再需要旧对象时,它们会被添加到列表中,在那里它们会被关闭或重置而不是被销毁。
然后,当需要该类型的新对象时,可以从对象池中启用它,将在其中重新打开它,而不是实例化,从而节省性能。

创立对象池体系有许多不同的选项,许多开发人员会挑选简略地构建自己的。可是,从 Unity 2021 之后,有一个可用的内置对象池体系。
Unity内置对象池是如何工作的?
要使用 Unity 的对象池系统,需要使用 Unity 2021 或更高版本,并且需要将 UnityEngine.Pool 命名空间添加到访问池的任何脚本中。
例如:
using UnityEngine.Pool;
然后,将能够声明特定类型的新池,例如游戏对象:
ObjectPool objectPool = new ObjectPool<GameObject>();
执行此操作时,需要传递对最多四个函数的引用,这些函数将在对对象池发出不同请求时调用。
- Create – 首次实例化对象时调用
- Take – 从池中取出对象时调用
- Release – 当对象被释放回池时调用
- Destroy – 当一个对象被销毁而不是被返回时被调用
每次请求或返回池对象时都会调用 Take 和 Release 函数,即使对象是新创立的或无法放回池中,所以最好使用这些函数来启用或禁用这些部分需要重置的池对象。
如果池对象不可用或返回的对象不能放入池中,例如,因为池已满。
它们在 Take 和 Release 函数之前和之后被调用,这通常是放置 Instantiate 函数的地方,当池中没有对象时创立一个新对象,以及 Destroy 函数,用于删除不能被退回。
例如:
using UnityEngine;
using UnityEngine.Pool;
public class SpawnUsingPool : MonoBehaviour
{
public GameObject objectPrefab;
ObjectPool<GameObject> objectPool;
void Awake()
{
objectPool = new ObjectPool<GameObject>(OnObjectCreate, OnTake, OnRelease, OnObjectDestroy);
}
GameObject OnObjectCreate()
{
GameObject newObject = Instantiate(objectPrefab);
newObject.AddComponent<PoolObject>().myPool = objectPool;
return newObject;
}
void OnTake(GameObject poolObject)
{
poolObject.SetActive(true);
}
void OnRelease(GameObject poolObject)
{
poolObject.SetActive(false);
}
void OnObjectDestroy(GameObject poolObject)
{
Destroy(poolObject);
}
}
在此示例中,当创立一个新对象时,会向其中添加一个池对象组件,并设置对原始对象池的引用。
为什么会这样?
这是为了让池中的对象能够返回自身,例如,如果它死亡或移动超出范围,它可以将自己释放回它来自的池中,就像任何其他对象通常可以摧毁自己一样。
例如:
using UnityEngine;
using UnityEngine.Pool;
public class PoolObject : MonoBehaviour
{
public ObjectPool<GameObject> myPool;
public void DestroyPoolObject()
{
myPool.Release(gameObject);
}
}
如何用参数实例化对象
使用对象池创立对象时,并没有真正产生新对象,而是在重用旧对象。这意味着需要将对象重置为其原始状态,然后才能再次使用它。
同样,在创立新对象时,可能希望将数据传递给其脚本以自定义它们或以特定方式初始化它们。
那我们该怎么做?
通常,除了 Instantiate 函数的重载方法之外,没有内置方法可以在创立对象时设置对象的参数。
这是有道理的,因为无论如何,游戏中的每个对象对于项目来说都是独一无二的。但是,可以通过几种不同的方式在 Unity 中设置新的或重复使用的对象。
例如,可以在创立对象时将对象的组件添加到目标中,或许运用 Get Component 找到它们,这两种方法都会供给对目标脚本的引用,然后允许修正它们的值。
或许,可以从现已连接到系统和组件的初始化脚本中管理所有目标的设置,或许期望在创立它时对其进行调整。
或许,假如运用目标池创立对象,则每个脚本都可以运用其 On Enable 和 On Disable 函数,在打开或封闭对象时主动调用,来管理它们自己的初始化和重置任务。
但是,没有一个正确的答案,最适合自己的方法将取决于需要对目标和游戏结构进行管理的内容。