本文将持续间断更新
本文主要面向初级程序员,为了方便Unity开发,有些快捷键的方式和一些通用性的技巧tips等会在这篇博客内持续更新,欢迎点赞收藏
快捷键
- Ctrl + S ; 快捷保存!闲着没事就来两下!
- Ctrl+Shift+F ; 在hierarchy窗口中选中任意对象,在scene窗口中按此快捷键,可将对象坐标设置为此时scene窗口坐标。
- Alt+Mouse Left ; 在hierarchy窗口中折叠或展开对象时,按住alt再按左侧小三角,可使其所有子对象折叠或展开。
重要、有用,必学的函数
Lerp
使用变量插值:在编写脚本时,为了提高代码的可读性和灵活性,建议使用变量插值,例如 transform.position = Vector3.Lerp(transform.position, targetPosition, speed * Time.deltaTime);
这种方式,能够实现更平滑的过渡效果。
https://docs.unity3d.com/ScriptReference/30_search.html?q=Lerp.
开发时的原则
尽量减少GameObject的数量
尽可能地合并一些GameObject,可以大大减少Draw Call的次数,从而提高性能。
预制体重用
预制体是非常有用的,可以让我们在场景中快速地实例化对象,并且在多个场景中复用,更改一个prefab,即可应用更改到所有prefab的实例。
还可以有Prefab Variants(https://docs.unity3d.com/Manual/PrefabVariants.html),如同面向对象的继承一样,可以实现更改“树”prefab,应用更改到“苹果树”prefab和“梨子树”prefab这样的功能。
对象池
对象池是一种重复使用已经创建过的对象的技术,这样可以避免在创建对象时的开销,提高性能。比如子弹,如果每颗子弹单独Instantiate和destroy,开销会很大,转用对象池就能很省资源,下面是例子:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class ObjectPool : MonoBehaviour {
public GameObject bulletPrefab;
public int poolSize = 20;
private List<GameObject> bullets = new List<GameObject>();
//创建了一个Bullet的对象池,通过实例化bulletPrefab来填充池子。
void Start () {
for (int i = 0; i < poolSize; i++) {
GameObject bullet = Instantiate (bulletPrefab);
bullet.SetActive (false);
bullets.Add (bullet);
}
}
//GetBullet函数用于获取一个Bullet对象,
//它首先检查池子中是否有空闲的对象,
//如果有,就返回其中一个;否则,就实例化一个新的Bullet对象。
public GameObject GetBullet () {
for (int i = 0; i < bullets.Count; i++) {
if (!bullets[i].activeInHierarchy) {
return bullets[i];
}
}
GameObject bullet = Instantiate (bulletPrefab);
bullet.SetActive (false);
bullets.Add (bullet);
return bullet;
}
//ReturnBullet函数用于将Bullet对象返回池子。
//当我们使用完一个Bullet对象时,可以将其传递给ReturnBullet函数
//,以便将其返回到池子中,而不是销毁它。
public void ReturnBullet (GameObject bullet) {
bullet.SetActive (false);
}
}
在实际使用中,我们可以在需要创建Bullet对象时,使用ObjectPool的GetBullet函数获取一个对象,使用完毕后,使用ObjectPool的ReturnBullet函数将对象返回到池子中。这样,我们就可以避免频繁地创建和销毁Bullet对象,从而提高应用程序的性能。
使用Profiler进行性能分析
Profiler是Unity的一个内置工具,可以帮助开发者识别应用程序的性能瓶颈,以及优化应用程序的性能。可以通过Profiler判断当前程序的运行瓶颈在哪,我是通过unity官方教程了解到的这个工具,这个确实简单易懂,而且入门简单,能清楚的看到哪行到哪行用了多少ms。可以搜搜使用方法,我觉得非常有用。
使用协程来延迟执行代码
协程(Coroutine)是Unity中一种用于实现多任务并行处理的机制。协程可以在执行过程中暂停并等待某些条件满足后再继续执行,从而可以实现一些非常有用的功能,例如延时执行、动画效果、处理异步任务等。在谷歌搜Invoke和协程的区别的时候,大家都建议用协程,下面是一个使用协程实现延时执行的例子:
using UnityEngine;
using System.Collections;
public class CoroutineExample : MonoBehaviour
{
void Start()
{
StartCoroutine(DelayedFunction(2.0f));
}
IEnumerator DelayedFunction(float delay)
{
Debug.Log("Delay start");
yield return new WaitForSeconds(delay);
Debug.Log("Delay end");
}
}
另外开发中经常有生成器这种东西,我一般开个协程,start里调用一下,下面是我开发中的一个例子:
using System.Collections;
using System.Collections.Generic;
using System.Security.Cryptography;
using UnityEngine;
public class ResidentManager : MonoBehaviour
{
[SerializeField] GameObject[] residentsPrefabs;
int totalResidentNumber;
private void Awake()
{
//变量初始化
}
// Start is called before the first frame update
void Start()
{
//一些在其他脚本内awake中初始化的变量需要在start中初始化,
//因为Start方法会在所有对象的Awake方法调用完毕后执行
totalResidentNumber = GameManager.INSTANCE.totalResidentNumber;
StartCoroutine(SpawnResident());
}
// Update is called once per frame
void Update()
{
}
//协程,生成resident
IEnumerator SpawnResident()
{
for (int i = 0; i < totalResidentNumber; i++)
{
int randomIndex = Random.Range(0, residentsPrefabs.Length);
GameObject resident = Instantiate(residentsPrefabs[randomIndex], transform.position, Quaternion.identity);
resident.transform.parent = transform;
yield return new WaitForSeconds(0.1f);
}
}
}
条件编译
可以帮助我们根据不同的平台和编译条件编写不同的代码,从而提高应用程序的可移植性。用条件编译就不用换一次平台新建一次工程或者手动根据目标平台注释代码了,直接代码全写里面,区别就是编译的时候,根据build选项里的目标平台会自动确定需要编译哪部分代码,未被编译的代码没有任何性能损失。
一些只有在开发时才会用到的函数可以用UNITY_EDITOR来预编译,下面是我开发的一个VR游戏的例子,只有在调试的时候才会在editor和window下运行,所以可以这样写:
下面是一个使用条件编译实现平台相关功能的例子:
using UnityEngine;
using System.Collections;
public class PlatformExample : MonoBehaviour
{
#if UNITY_EDITOR
void Start()
{
Debug.Log("Unity Editor");
}
#elif UNITY_ANDROID
void Start()
{
Debug.Log("Android");
}
#elif UNITY_IOS
void Start()
{
Debug.Log("iOS");
}
#endif
}
一眼就能看明白,最重要的是方便,不用注释掉其他平台的代码,毕竟unity也是个适合跨平台的引擎。
其他的条件编译内容和平台列表参照官网:
https://docs.unity3d.com/Manual/PlatformDependentCompilation.html
重构你的代码!
脚本中函数位置规划
为了提高脚本的可读性,可以按照以下方式规划各个函数的位置:
-
首先,建议将所有的公共成员变量(例如,public变量和public属性)放在脚本的最上面,这样其他开发者就可以很方便地查看脚本中可见的成员变量。
-
接下来,建议将脚本的生命周期函数(例如,Start和Update)放在公共成员变量之后。这些函数通常用于初始化脚本和每帧更新脚本的状态,所以将它们放在公共成员变量之后更加合理。
-
紧接着,建议将其他重载函数、条件编译函数、公共函数和私有函数按照函数的逻辑顺序进行分组,并将各个函数组之间留出一定的空白行进行分割。这样做可以提高代码的可读性,使得开发者更容易理解代码的逻辑和结构。
-
如果有辅助计算函数,建议将它们放在调用它们的函数之后,并将它们命名为较短的名称,以突出它们的作用。
例如,一个可能的函数排列顺序如下:
public class ExampleScript : MonoBehaviour {
// 公共成员变量
public int someValue;
// 生命周期函数
void Start() {
// ...
}
void Update() {
// ...
}
// 其他重载函数和条件编译函数
void OnTriggerEnter(Collider other) {
// ...
}
#if UNITY_EDITOR
void OnDrawGizmos() {
// ...
}
#endif
// 公共函数
public void DoSomething() {
// ...
}
// 私有函数
private void DoAnotherThing() {
// ...
}
// 辅助计算函数
private void CalculateSomething() {
// ...
}
// 调用辅助计算函数的函数
private void PerformCalculations() {
CalculateSomething();
// ...
}
}
上述代码中,按照公共成员变量、生命周期函数、其他重载函数和条件编译函数、公共函数、私有函数以及辅助计算函数的顺序进行排列,增加了代码的可读性,使得其他开发者更容易理解代码的逻辑和结构。