Unity3d笔记
- 当变量重命名后,已序列化保存的值会丢失,如果希望继续保留其数值,可使用FormerlySerializedAs,如下代码所示:
[UnityEngine.Serialization.FormerlySerializedAs("hp")] public int newHp = 20;
-
Unity有个隐藏的彩蛋:void Main(){},它的调用在Awake() OnEnable()之后Start()之前。
- unity对文件名以点开头的文件视而不见
- 当用enum枚举值作为Dictionary的key,在访问字典时则会产生GC。枚举是值类型的它被当作为整形使用,而Dictionary每次判断key时都会调用
Object.getHashCode(Object),
枚举会被调用到基类引用类型Object的该方法从而导致装箱产生GC,其他如int等值类型因自己的getHashCode
不会调用到Object上的方法则作为key不会有GC,建议在需要用到enum作为key时先强转为int再将int作为key,或者在Dictionary的构造函数内传入自定义的IEqualityComparer:
1 public class MyEnumComparer : IEqualityComparer<MyEnum> { 2 public bool Equals(MyEnum x, MyEnum y) { 3 return x == y; 4 } 5 public int GetHashCode(MyEnum x) { 6 return (int)x; 7 } 8 }
-
C#属性(Property)想要显示在Inspector上且set get能被正常执行:https://github.com/LMNRY/SetProperty
[SerializeField, SetProperty ("Number")] private float number; public float Number { get { return number; } private set { number = Mathf.Clamp01(value); } }
- 当你在Unity中编辑场景,突然死机时,可以在项目文件目录中找到Temp文件夹,双击文件夹,找到_Backupscenes文件夹,把后缀为.backup的文件后缀改为.unity,然后拖进Unity的Project界面里面,这样就可以还原死机前场景最后情况。
- 通过Debug.Log获取执行此语句物件:在脚本的Debug.Log语句中加入gameObject,即Debug.Log("Test", gameObject); 脚本运行时点击Console界面中的输出语句,就能在Hierarchy界面中看到哪个物件执行了这个脚本。
- Debug.Break()可以在任何地方暂停游戏,调试小技巧。
- 性能调试时将你想观察的那部分代码放入
Profiler.BeginSample ("aaa");
Profiler.EndSample ();
代码之间就可指定查看该部分代码的开销。 - 当修改了Prefab并想将该改动应用到所有的物体上:
若是在Project视图中直接修改的该prefab则一定要记得执行File->Save Project,unity并不会将改动直接保存到磁盘。
若是在Hierarchy视图中修改的该prefab,必须点击Apply,若同时还需要保存场景时,则尽量记住先执行Save Project操作再执行Save Scene,不然你场景中的prefab很可能跟它本身的prefab永久丢失关联。 - 屏幕坐标与鼠标位置:
屏幕坐标系以左上角为原点(0, 0),右下角为(Screen.Width, Screen.Height)。
Input.mousePosition鼠标位置以屏幕左下角为原点(0, 0),屏幕右上角为(Screen.Width, Screen.Height)。 - Awake会在物体初始化之后被调用,无论脚本本身是否被启用(是否被禁用,在Inspector视图上脚本前面的复选框是否被勾上,实际上该复选框只有脚本在存在Start(), Update(), FixedUpdate(), and OnGUI()至少一个时才会显示);OnEnable、Start则是在脚本被启动的情况下被调用,其中OnEnable会在脚本或者物体每次被重新启用时都会被再次调用。
- 父物体与子物体Awake、OnEnable、Start函数执行顺序:
首先同一脚本中该三个函数的执行顺序是:Awake->OnEnable->Start,然后:
* 若父子物体都是在场景中已经存在的:子物体的Awake、OnEnable全执行完毕后才会执行到父物体的Awake、OnEnable;当所有父与子物体的Awake与OnEnable全都执行完毕后才开始执行子物体的Start再执行父物体的Start。
* 若父子物体先不存在于场景中,而是通过Instantiate()动态产生的:则父与子的调用顺序与上面相反,父物体的Awake、OnEnable全执行完毕后才会执行到子物体的Awake、OnEnable;当所有父与子物体的Awake与OnEnable全都执行完毕后才开始执行父物体的Start再执行子物体的Start。
该结论是实测多次观察出来的,但unity官方没未明确说明Awake OnEnable Start存在固定顺序,故不排除有时不表现为以上所说顺序而是随机顺序,不应该依赖这些执行顺序而应尽量避免初始化的先后的要求。 - 调用Instantiate()方法动态添加GameObject时,新GameObject的Awake、OnEnable都调用结束后Instantiate()才会返回。
- 一般在新建类时会产生空的Update函数。如果代码不需要用到该函数,应该该函数进行删除。另外,尽量不要在Update函数内执行Find、FindObjectOfType、FindGameObjectsWithTag这些寻找物体的函数,面应该尽量在Start或Awake函数中执行。
- 每个脚本中实现Update()回调函数,这是一般做法,更优的做法是只有一个Manger脚本含有Update函数,遍历(Array性能优于List)调用其他脚本的OnUpdate(或其他命名)函数。Unity要维护调用的Update函数越多开销越大,详细测试见:https://blogs.unity3d.com/cn/2015/12/23/1k-update-calls/ 从评论中了解到,unity正在开发全新的一套消息机制,喜闻乐见。
另一方面,有人想到用Coroutines协程代替Update,测试证明Update比协程要快5倍,协程内部至少要处理move next和current两个调用 - 引用一个游戏对象的逻辑,可以在最开始的地方定义它。例如:
1 private Transform myTransform; 2 private Rigidbody myRigidbody; 3 void Start() 4 { 5 myTransform = transform; 6 myRigidbody = rigidbody; 7 }
- 尽量减少使用临时变量,特别是在Update等实时调用的函数中。
- 捕捉Android返回与Home键:
1 //返回键 2 if(Application.platform == Runtimeplatform.Android 3 && Input.GetKeyDown(KeyCode.Escape)) 4 { 5 //... 6 } 7 8 //Home键 9 if(Application.platform == Runtimeplatform.Android 10 && Input.GetKeyDown(KeyCode.Home)) 11 { 12 //... 13 }
- 直接打开app store,同理根据链接不同也可打开Mail或浏览器等:
void OnRateButtonClick() { #if UNITY_ANDROID Application.OpenURL("market://details?id=YOUR_APP_ID"); #elif UNITY_IPHONE Application.OpenURL("itms-apps://itunes.apple.com/app/idYOUR_APP_ID"); #endif }
- 获取Android手机分辨率:
1 private float androidDensity = 1.0f; 2 void GetDensity() 3 { 4 //#if UNITY_ANDROID 5 if (Application.platform == RuntimePlatform.Android) 6 { 7 AndroidJavaClass player = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); 8 AndroidJavaObject activity = player.GetStatic<AndroidJavaObject>("currentActivity"); 9 AndroidJavaObject wm = activity.Call<AndroidJavaObject>("getWindowManager"); 10 if (wm != null) 11 { 12 AndroidJavaObject display = wm.Call<AndroidJavaObject>("getDefaultDisplay"); 13 if (display != null) 14 { 15 AndroidJavaObject displayMetrics = new AndroidJavaObject("android.util.DisplayMetrics"); 16 display.Call("getMetrics", displayMetrics); 17 androidDensity = displayMetrics.Get<float>("density"); 18 } 19 } 20 } 21 //#endif 22 }
- Time.timeScale设置为0时Update()仍然会继续执行,FixedUpdate()会停止调用,注意:此时协程WaitForSeconds和Invoke调用都会停止!
Time.timeScale会影响Time.deltaTime和Time.time,不会影响Time.realtimeSinceStartup, Time.unscaledDeltaTime, Time.unscaledTime;
Time.time/timeScale = unscaledTime,
Time.deltaTime / timeScale = unscaledDeltaTime;
在Update中使用Time.deltaTime,就可以使用timeScale改变运动速率
在Update中使用Time.unscaledDeltaTime,timeScale就不会影响运动速率
在后台运行(暂停)和卡顿(如加载大场景时进入场景那一刻)期间,Time.deltaTime和Time.time不会继续计时,Time.realtimeSinceStartup, Time.unscaledDeltaTime, Time.unscaledTime会继续计时,此时Time.realtimeSinceStartup更精准会在游戏恢复时立马得到正常时间,Time.unscaledDeltaTime, Time.unscaledTime则可能要等好几帧之后才会得到正常的时间(即暂停期间经过的时间过几帧才加上来)。
当手机调整了设备时间后Time.realtimeSinceStartup也同样会受影响。 - 获取当前animator播放的state时长:anim.GetCurrentAnimatorStateInfo(0).length。然而该函数需要等动画播放后才能正确获取,即使在协程中等待一帧再调用该函数得到的时间也可能是错误的,应该调用anim.Update(0)后再去获取:
m_animator.Play(animName, -1, 0); m_animator.Update(0); int length = m_animator.GetCurrentAnimatorStateInfo(0).length;
若是在5.x版本中则可以通过指定的状态名字获取其时长:anim.runtimeAnimatorController.animationClips.First (x => x.name == "AnimationName").length;
- Animator.Play(string stateName, int layer = -1, float normalizedTime = float.NegativeInfinity):
layer:经测试0和-1是一样的。
normalizedTime:
0-1为从开始到结束之间某时间点开始播放;
负无穷时如果play的是当前状态则继续播放当前状态不做任何改变,否则跳到从头开始播目标状态;
为负数时,时间会保持增长,从负数开始至0期间为暂停的,从0之后开始正常播放;
为超过1的数时则会以小数部分的时间(等同于0-1的参数)点开始播放,忽略整数部分(实际上整数部分代表重复播放了多少遍)。 - 简单实现animator反方向播放动画:animator.speed=-1;
- 发ios包的注意点:
-
在用unity4.6发iOS包的时候,发现导出的xcode工程出错;其实原因是xcode太新了,为7.2, 而unity还只支持7.1,而两者兼容性不好,导致了很多error的出现。所以,发ios包时要注意xcode的版本和unity是否相符,不然会花太多的时间在上面的,切记!
-
图形化调试:
Unity中图形化调试主要4种
Debug.Draw
Gizmos.Draw
Graphic.DrawMesh
GL
只需在Scene窗口显示的调试图像
一直显示的 OnDrawGizmos + Gizmos.Draw
选中显示的 OnDrawGizmosSelected + Gizmos.Draw
脚本控制的 Update + Debug.Draw
需要在实际设备屏幕显示的调试图像
Update+Graphic.DrawMesh
OnRenderObject+GL
Graphic.DrawMesh和Debug.Draw 调用一致,都是在Update系里
Graphic.DrawMesh和GL 显示类似,都在各个窗口显示,并且可以设置材质。
详见:http://blog.sina.com.cn/s/blog_471132920101gxzf.html -
Unity提供了命令行的接口,可以通过Shell调用。进而可以一键打包,一键打AssetBundle,接着上传SVN等操作。文档:http://docs.unity3d.com/Manual/CommandLineArguments.html
- 调试执行时间:
1 Stopwatch sp = new Stopwatch (); 2 sp.Start (); 3 DoSomething (); 4 sp.Stop (); 5 Debug.Log (string.Format ("Elapsed:{0} ms", (float)sp.ElapsedTicks/ Stopwatch.Frequency * 1000f));
- 写Unity编辑器控件时如何获得Unity内置的图标:Texture tex = (Texture)EditorGUIUtility.Load("PlayButton On");或
EditorGUIUtility.IconContent("PlayButton On")这样就可以获得一个蓝色的播放图标。所有的图标名字集合请转至http://www.xuanyusong.com/archives/3777
- 您可以通过使用[MenuItem(“CONTEXT / ...”)添加自定义项目到上下文菜单,即使是内置的类如:[MenuItem("CONTEXT/Image/TestFunc", false, 10000)]
-
写一个继承自AssetPostporocessor的脚本可在Unity导入或更新各种资源之前或之后增加自定义处理,如更改图片或音频的格式、转换xlsx配置档为bytes文件等等。
- 在使用缓冲池等需要修改对象的父节点时,正确的顺序应该是首先禁用对象,然后将其父对象重置为对象池,而不是先修改父对象再禁用对象,这样会造成不必要的污染。
- ??和?.两个操作符表达式对于继承自UnityEngine.Object的类通常是得不到正确结果的,它是纯C#的null检查,它会绕过Unity内部自定义的==null检查,请谨慎使用或避免使用在内置组件中。
unity为了开发者方便使用调试获得更多错误细节信息,在对C++封装C#时增加了很多额外处理,如果不做这些操作,则用户看到的就是简单的空引用异常的错误,对错误原因和修改方式知之较少。
如GameObject等在C#仅仅是对引擎内部原生C++ Object的封装,C#的内存管理是GC自动处理,C++则是是仅当切换场景或手动调用UnityEngine.Object.Destroy()时清理,故存在C#引用还存在而底层原生代码已被销毁的情况。故unity重载了==操作符可以判断原生Object是否被destoryed,也因此==null的操作比你想像中更费,它要判断处理的事比较复杂:private Transform m_CachedTransform public Transform transform { get { if (m_CachedTransform == null) m_CachedTransform = InternalGetTransform(); return m_CachedTransform; } }
以上代码即会看不出对transform进行缓存有多少性能提升,因==null的操作本身就比较费。
对object进行判空有两种意图,是确保已进行赋值还是检查引用的底层引擎对象生命周期,以下代码意义不明:
var go = gameObject ?? CreateNewGameObject();
当你是想判断实际引用的物体是否被destory需要显示调用==操作符:
var go = gameObject != null ? gameObject : CreateNewGameObject(); // Or use the implicit bool conversion operators for the same check go = gameObject ? gameObject : CreateNewGameObject();
当你想确保变量已被初始化赋了正确的值需要显示调用object.ReferenceEquals()(对null的检查该调用已被编译器优化,且速度快于自定义的==操作符):
return !object.ReferenceEquals(gameObject, null) ? gameObject : CreateNewGameObject();
详情:https://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/
实际代码实践中发现仅如BoxCollider等内置组件不可使用??和?.表达式,自定义类及UGUI相关组件是没问题的,可能unity5或之后某版本unity已优化。 - LinkedList、List当自定义结构体struct做链表节点,必须实现IEquatable<T>、IComparable<T>接口,否则Remove、Cotains、Find、FindLast每次都有GC产生.
-
var json = JsonUtility.ToJson(unityComponent); JsonUtility.FromJsonOverwrite(json, unityOtherComponent);
json工具可以实现一些奇技淫巧,如有个自定义的文本组件继承自TextMeshProUGUI组件,需要将现有UI上的旧组件替换为新的组件,但因两个组件不能在一个节点上共存很难复制原属性字段值,代码挨个赋值太累,便可用以上代码将旧组件的全部配置合并到新组件上(编辑器下new Preset(Object)应该也可以复制组件属性并随后应用到其他组件上)。
或者做小工具想用一个脚本修改任意组件时可用以上代码保存任意的修改再分情况还原到指定组件上。(多语言要不同配置或很多子类要自定义时)