让Struct在检查器显示
脚本序列化官方文档:
Script serialization - Unity 手册
在Struct上面加[Serializable]
继承MonoBehaviour的类的单例模式
和普通C#单例的区别是如果instance是空,不是直接new,而是新建一个物体,或找一个物体,AddComponent。
从这抄的:
Unity重要框架之单例模式框架 - 知乎 (zhihu.com)
CustomEditor显示的控件修改后运行时无效的问题
给一个脚本编写CustomEditor,用OnInspectorGUI()显示一些字段,在检视器修改字段的值,运行时还是用脚本里的初始值。
Debug DrawRay()看不到射线
原因:Gizmos没开。
Screen.width/Screen.height得到0
打印屏幕的长宽比:
输出是0:
原因:这两个值是int,要计算它们的比需要转换成float。
在脚本里判断一个引用的对象是在Scene里的实例还是预制体
脚本里开放的对象引用在Inspector里显示为接口,可以把Scene里的物体实例拖进去,也可以把Assets里的预制体拖进去,但是预制体不实例化是不能用的。脚本需要能判断它是实例还预制体。
判断gameObject.scene.name==null,Assets里的预制体这个值是Null,场景里的是场景的名字。
Invoke()
只能调用没有参数的方法,即使有默认值的参数也不行。
UnityEvent
UnityEvent引用在Inspector里是这样的:
可以在这个列表里添加某对象的某方法,列表里的方法叫“监听器”(Listener)。手拖进去的叫持久性监听器。takeAction.AddListener()可以在脚本里添加监听,叫非持久性监听器,不显示在Inspector里。
执行takeAction.Invoke()会执行这个列表里的所有方法。
背包系统关于List Remove()的报错
这个错误是测试背包系统,拿起物品时出现的,是偶发的。点击可拾取物品里的物品时,会从背包的可拾取物品List里删除相应的元素,此时有概率报错:ObjectDisposedException: SerializedProperty itemsInReach.Array.data[3] has disappeared!
但是可拾取物品的List的元素被正常删掉了:
拿起最后一个物品时有概率报错:InvalidOperationException: The operation is not possible when moved past all properties (Next returned false)。此时最后一个物品在可拾取物品List里的元素不会被删掉,它指向的物品已经删掉了,所以这个元素变成一个空引用。
新发现:测试的时候如果Inspector没有看着背包脚本或背包脚本折叠起来就从来不会报这个错。我又看到报错是关于UnityEditor的,应该是脚本的接口更新显示相关的报错。不是我的脚本的错误,所以这个报错我不打算管了。
定义一个脚本后报错The namespace '<global namespace.' already contains a definition for 'xxx'
脚本里的类和脚本重名就报错,不重名反而没问题。
原因:我已经定义了这个名字的类,但是我忘了。
写了ContextMenu,但是Inspector脚本的菜单里没有
原因:加ContextMenu的方法都不能有参数!否则不出现!
低耦合高内聚思想
就是一个模块只能通过接口方法被其他模块调用,其内部的数据结构不能被外部访问。
判断低耦合高内聚的方法:
对模块内的一个数据结构Find All References:
只在这个模块出现过。下图也没有完全实现高内聚。
NotSupportedException: Specified method is not supported.
这个报错可能对应不止一种具体错误。。
我出问题的那一行是这样:
经过检查是因为这个Resources.Load()路径的资源不存在。但是报的这个错我完全看不懂。我决定不在静态类里用字符串记录预制体路径了,还是往检查器里拖。
Destroy()和DestroyImmediate()
Destroy()是把物体放在一个缓冲区,下一帧删除,所以只能在Play Mode使用,在编辑状态要使用DestroyImmediate()。因为这个区别,删除所有子物体时GetChild()的效果不一样,Play Mode在一个方法里执行Destroy()前后GetChild()返回的值没有变化,可以用GetChild(i),
编辑模式执行DestroyImmediate()后后面的子物体向前进一位,循环删除应该用GetChild(0).
充分利用动画状态机的逻辑
这个问题在射击游戏笔记里说过,一些代码的执行时机其实刚好就是动画状态机的一些状态切换时。
比如人物跑步、换弹、趴下、站起时不允许瞄准、射击,与其在玩家输入这些动作时判断,其实这些刚好就是人物离开持枪状态的去向,直接写在持枪状态的OnStateExit()即可。
再比如更新枪和子弹的信息,需要执行的时机有很多:从身上取枪时、交换枪时、拿起手里枪对应的弹匣时、换弹时、射击时、放下手里的枪时、放下手里枪的弹匣时、改变自动方式时。其中除了射击、改变自动方式,其他要更新的时机都不在持枪状态,那么就可以写在持枪状态的OnStateEnter()。
用lambda表达式给按钮传递带参数的回调方法onClick.AddListener(()=>...)
有个坑,就是在执行这个回调的时候才去读这个lambda表达式返回的值,那个时候参数可能比执行onClick.AddListener时已经变了。看起来按按钮时就去执行回调方法了,其实系统去onClickAddListener那一行计算参数的值了。
关于Unity.VisualScripting
据我目前所知,它允许调用如xxx.AddComponent(),xxx是任意组件。AddComponent()本来是GameObject的方法。
raycastHit.transform返回的物体到底™是什么?
一开始我以为RaycastHit.transform是命中的碰撞体的根节点,如果给人物的chest加碰撞体,射线命中了这个碰撞体,Debug.Log(hit.transform)输出的是这个人物的名字
RaycastHit.collider是命中的碰撞体,Debug.Log(hit.collider)输出的是碰撞到的部位的名字。
2024.4.29今天又发现_hit.transform打印的是身体部位的名称。(后来调查发现是因为当时在使用布娃娃,身体部位挂有刚体)
2024.7.27今天又发现_hit.transform打印的是人物的名称。(这是因为骨骼没挂刚体)
2024.8.15
今天发现_hit物体的父级如果没有刚体,_hit.transform就是它自己,如果有刚体,就是它父级。这™是什么逻辑。
结论:_hit.transform返回的是它挂有刚体的最低级父级。
rigidBody.velocity和animator.velocity的关系
二者共同决定了物体的速度,二者没什么关系。使用animator的Root Motion驱动角色移动时,刚体的velocity是0,除了角色下坡时,刚体的重力起作用,会有一点向下的速度;角色上坡被地面顶向上时,刚体并没有向上的速度。
动画事件方法和有参数方法的矛盾
动画事件方法不能有参数,但是想让一个方法既当动画事件又被其他地方调用,且其他地方有参数会很方便。
可以写一个无参的方法作动画事件,它调用有参的方法,参数写死。
void GrabRifleAnimEvent(){//动画事件里要用这个方法,没法传自定义类型参数
MoveGunInHand(rifleScript);
}
void GrabPistolAnimEvent(){
MoveGunInHand(pistolScript);
}
void MoveGunInHand(Item item){
item.transform.SetParent(rightHand);
item.transform.localPosition=Vector3.zero;
item.transform.localEulerAngles=gunEulerInHand;
}
反射用到的方法总结
得到Type
typeof(类名);
实例.GetType();
Type.GetType("命名空间.类名");
得到公共成员
MemberInfo memberInfos[]=type.GetMembers();
得到构造函数
得到所有:
ConstructorInfo[] constructorInfos=type.GetConstructors();
得到无参:
ConstructorInfo constructorInfo=type.GetConstructor(new Type[0]);
MyClass myClass=constructorInfo.Invoke(null) as MyClass;
得到有参:
ConstructorInfo constructorInfo2=type.GetConstructor(new Type[]{typeof(参数类型)});
myClass=constructorInfo2.Invoke(new object[]{2}) as MyClass;
Activator快速实例化
myClass=Activator.CreateInstance(type) as MyClass;
得到公共字段
FieldInfo fieldInfo=type.GetField(string 字段名);
公共字段读写
fieldInfo.GetValue(实例);
fieldInfo.SetValue(实例,数据);
得到公共方法
得到所有
MethodInfo[] methodInfos=type.GetMethods();
按方法名得到:
MethodInfo methodInfo=type.GetMethod(string 方法名,new Type[]{typeof(参数1类型),...});
执行公共方法
实例方法
methodInfo.Invoke(实例,new object[]{参数1,...});
静态方法
methodInfo.Invoke(null,new object[]{参数1,...});
特性相关方法
查询有没有特性:
type.IsDefined(typeof(自定义特性类型),bool 是否查找继承链)
得到特性类:
type.GetCustomAttributes(bool 是否查找继承链)
异步加载场景在Hierarchy出现好几个场景名(Is loading)
代码:
public Slider loadSceneProgress;
void Start()
{
DontDestroyOnLoad(gameObject);
// DontDestroyOnLoad(loadSceneProgress.gameObject);
}
public string sceneName;
AsyncOperation asyncOperation=null;
public void StartLoadAsync(){
asyncOperation=SceneManager.LoadSceneAsync(sceneName);
StartCoroutine(MyLoadSceneAsync(null));
}
IEnumerator MyLoadSceneAsync(UnityAction Preset){
while(!asyncOperation.isDone){
loadSceneProgress.value=asyncOperation.progress;
Debug.Log(asyncOperation.progress);
yield return null;
}
loadSceneProgress.value=1;
Preset();
}
void OnTriggerEnter(){
Instance.StartLoadAsync();
}
}
原因:异步加载是通过OnTriggerEnter()触发的,加载中每一帧都执行一次OnTriggerEnter()。
缓冲池
看的教程里缓冲池包含一个字典,键是字符串,值是存放缓冲物体的父物体。
Dictionary<string,GameObject> bufferDict=new Dictionary<string,GameObject>();
觉得这样写字符串容易打错,不如用枚举。
延迟放入缓冲池使用协程,yield return new WaitForSeconds()后面没有执行
public IEnumerator EnpoolLater(GameObject instance,BufferType bufferType,float delay){
yield return new WaitForSeconds(delay);
if(bufferType==BufferType.impact){
Debug.Log("击中效果进入");
}
Enpool(instance,bufferType);
}
原因:放入的是击中效果,由子弹触发,但是子弹在执行Enpool()前被销毁了。又把子弹的销毁改成不活动,也不能执行延迟入池。
所以延迟入池的代码还是要放在被入池物体的OnEnable()里,不要让其他物体触发。其他物体可能提前就被销毁或失活了。
射击游戏弹头缓冲池的问题
射击游戏的弹头、击中效果、弹壳都使用缓冲池存储,出现了击中效果不在瞄准位置的问题。
[RequireComponent(typeof(CapsuleCollider))]
[RequireComponent(typeof(Rigidbody))]
public class MyBullet : MonoBehaviour
{
public LayerMask bulletLayerMask;
int groundLayer=8;
public WeaponData weaponDamageData;
Vector3 lastFramePosition;
float lifeTime=1;
void Start(){
}
void OnEnable(){
lastFramePosition = transform.position;
StartCoroutine(BufferPoolBase.Instance.EnpoolLater(gameObject,BufferPoolBase.BufferType.bullet,lifeTime));
}
RaycastHit raycastHit;
BodyTrigger bodyTrigger;
ImpactEffectRecorder myImpactEffect;
void Update(){
if(Physics.Linecast(lastFramePosition,transform.position,out raycastHit,bulletLayerMask,QueryTriggerInteraction.UseGlobal)){
if(raycastHit.collider.gameObject.layer==groundLayer){//打到地
if(raycastHit.transform.TryGetComponent(out myImpactEffect)){
GameObject effectInstance;
effectInstance=BufferPoolBase.Instance.Depool(myImpactEffect.impactEffectPrefab.gameObject,BufferPoolBase.BufferType.impact);//缓冲池出池
effectInstance.transform.position=raycastHit.point;
effectInstance.transform.LookAt(raycastHit.point+raycastHit.normal);
}
else{
Debug.Log(raycastHit.transform.name+"没有击中效果!");
}
}
else{
if(raycastHit.collider.TryGetComponent(out bodyTrigger)){
bodyTrigger.GetHurt(weaponDamageData);
}
}
BufferPoolBase.Instance.Enpool(gameObject,BufferPoolBase.BufferType.bullet);
}
}
}
弹头脚本有一个Vector3记录上一帧的位置,每一帧的位置和上一帧的位置使用Physics.Linecast()检测击中。在OnEnable()里记录了一次当前位置,但是这个代码是在把弹头放到枪口之前的,导致击中检测的连线是从上一次子弹入池的位置到枪口。
解决方法:给Depool()加一个参数Vector3 position,先设置位置再激活。理论上所有物体都应该这样,但是其他物体影响不大,弹头必须这样。修改后的Depool():
Dictionary<BufferType,Transform> bufferDict=new Dictionary<BufferType,Transform>();
//可能的情况包括没缓冲池、有缓冲池没物体(一般不会有,但理论上可能)、有缓冲池有物体
public GameObject Depool(GameObject prefab,BufferType bufferType,Vector3 position){
GameObject instance;
if(bufferDict.ContainsKey(bufferType)){//缓冲池已建立
if(bufferDict[bufferType].childCount>0){//缓冲池里有物体
instance=bufferDict[bufferType].GetChild(0).gameObject;//取出
instance.transform.SetParent(null);//解绑
}
else{//有缓冲池没物体(曾经放入过物体,又拿出了)
instance=GameObject.Instantiate(prefab);
}
}
else{//没有缓冲池
Transform bufferTransform=new GameObject(bufferType.ToString()).transform;
bufferDict.Add(bufferType, bufferTransform);
instance=GameObject.Instantiate(prefab);
}
instance.transform.position = position;
instance.SetActive(true);//激活
return instance;
}
这样写第一个弹头还是有问题,第一个弹头的OnEnable()在Instantiate()时执行,在设置位置之前,导致记录的上是帧位置是(0,0,0)。所以又在需要Instantiate()的参数里加上了position和一个Quaternion.identity。
总结:缓冲池的严谨写法是在SetActive()之前就设置好位置,如果是实例化,则在Instantiate()参数里加上位置。对大部分物体这可能只是锦上添花,但是对弹头实体,需要记录上一帧位置,通过Physics.Linecast()判断击中时,必须这样写。
NullReferenceException: SerializedObject of SerializedProperty has been Disposed.
背景:写了两个带[ExecuteAlways]的脚本,开始运行后无限报错,停止运行也报错。把一个脚本删掉,执行,不报错,再撤销删除脚本,再执行,也不报错。
DontDestroyOnLoad的单例物体在返回一个场景时怎么防止出现两个
这是一个跨场景的单例,可以在Start()里判断:
1.单例是空,把自己写入;
2.单例非空&&单例不是自己,把自己销毁;
3.它不能继承泛型单例基类,因为判断单例为空不能使用Instance,否则就像其他类调用它一样触发“要么新建要么报错”的自动操作,需要使用instance,那么这个私有instance必须写在自己里;
public class MySceneManager : MonoBehaviour
{
private static MySceneManager instance;
public static MySceneManager Instance{
get{
if(!instance){
#if UNITY_EDITOR
Debug.Log($"场景里没有MySceneManager实例!");
if (UnityEditor.EditorApplication.isPlaying)
{
UnityEditor.EditorApplication.isPlaying = false;
}
#endif
}
return instance;
}
}
void Start()
{
if(!instance){
instance=this;
}
else if(Instance!=this){
Destroy(gameObject);
}
DontDestroyOnLoad(gameObject);
}
}
异步加载场景从第二个场景回第一个场景后IEnumerator的一些代码没有执行,且新场景的动画没有播放,物理引擎无效
原因:从游戏场景加载回来是经过了暂停界面,执行了Time.timeScale=0;没复原。
为什么只有匿名函数、lambda表达式会出现闭包
匿名函数、lambda表达式支持在一个函数内定义,在这个函数外执行。而C#虽然允许函数内定义函数,却不允许函数内定义,函数外执行。委托装匿名函数、lambda表达式是个例外。
玩家人物对界面的修改代码应该放在哪里
游戏界面有武器信息、交互动作等信息。玩家人物在某些情况下会修改这些信息,修改的代码可以防止人物脚本里(游戏里不管是不是玩家人物都有人物脚本),人物脚本知道该修改信息的时间,但不知道这个人物是不是玩家需要判断一下;也可以放在某个管理器单例里,这样不用判断某个人物是不是玩家,但又不知道该修改信息的时间,只能在Update()里一直监视。