Unity脚本笔记

让Struct在检查器显示

脚本序列化官方文档:

Script serialization - Unity 手册

在Struct上面加[Serializable]

892abe3ce0364a979ed9e6a03e846c99.png

5bc28e54e1504ce0a9c23aa9e3b462b4.png

继承MonoBehaviour的类的单例模式

和普通C#单例的区别是如果instance是空,不是直接new,而是新建一个物体,或找一个物体,AddComponent。

从这抄的:

Unity重要框架之单例模式框架 - 知乎 (zhihu.com)

6c9e5ff6611a4429aee534226973b10c.png

CustomEditor显示的控件修改后运行时无效的问题

给一个脚本编写CustomEditor,用OnInspectorGUI()显示一些字段,在检视器修改字段的值,运行时还是用脚本里的初始值。

271984c11a74476790ab7fd1b75ca988.pngb1be9768cd5f41b09c01357386fda680.png

Debug DrawRay()看不到射线

原因:Gizmos没开。

d71ecad6cd7841d6aba26f1b2cba0ec3.png

Screen.width/Screen.height得到0

打印屏幕的长宽比:

27ef5bc60e796a9a0a254240460753ef.png

输出是0:

f5be68eec112f20aaf8debce8a23a581.png

原因:这两个值是int,要计算它们的比需要转换成float。

在脚本里判断一个引用的对象是在Scene里的实例还是预制体

脚本里开放的对象引用在Inspector里显示为接口,可以把Scene里的物体实例拖进去,也可以把Assets里的预制体拖进去,但是预制体不实例化是不能用的。脚本需要能判断它是实例还预制体。

37f92665713251f7c3db6a9ee2a999ca.png01088edd5e8e27b3da27af357a5e2fb2.pngff8574772c71b7caaf20d267910e3361.png

判断gameObject.scene.name==null,Assets里的预制体这个值是Null,场景里的是场景的名字。

81162ac4c0c842c8972f4068d59acfa5.png

Invoke()

只能调用没有参数的方法,即使有默认值的参数也不行。

UnityEvent

UnityEvent引用在Inspector里是这样的:

10a6bdbec9e0eb792ac10991fbac38ef.png

可以在这个列表里添加某对象的某方法,列表里的方法叫“监听器”(Listener)。手拖进去的叫持久性监听器。takeAction.AddListener()可以在脚本里添加监听,叫非持久性监听器,不显示在Inspector里。

执行takeAction.Invoke()会执行这个列表里的所有方法。

70196e12553944d2bb28bee9a5f1c245.png

背包系统关于List Remove()的报错

这个错误是测试背包系统,拿起物品时出现的,是偶发的。点击可拾取物品里的物品时,会从背包的可拾取物品List里删除相应的元素,此时有概率报错:ObjectDisposedException: SerializedProperty itemsInReach.Array.data[3] has disappeared!

4695549196c02845b7f08453b275125c.png

c96b1c4e40e29ba9fb0ad0a305eecb86.png

b7ae75923275890f8e05098c9a6085dc.png

但是可拾取物品的List的元素被正常删掉了:

9690d80dbeb7da6cfd5a06af5edc8a8a.png

拿起最后一个物品时有概率报错:InvalidOperationException: The operation is not possible when moved past all properties (Next returned false)。此时最后一个物品在可拾取物品List里的元素不会被删掉,它指向的物品已经删掉了,所以这个元素变成一个空引用。

011a560a0c659593f76580c5090f1bcb.pngeb5a74493e7ff89885b487882f007355.png

新发现:测试的时候如果Inspector没有看着背包脚本或背包脚本折叠起来就从来不会报这个错。我又看到报错是关于UnityEditor的,应该是脚本的接口更新显示相关的报错。不是我的脚本的错误,所以这个报错我不打算管了。

2dfcc89759548ebe53522be0e6c6790a.png

定义一个脚本后报错The namespace '<global namespace.' already contains a definition for 'xxx'

脚本里的类和脚本重名就报错,不重名反而没问题。

b1ffabb5088eb60eb60e22452de4161c.png

原因:我已经定义了这个名字的类,但是我忘了。

写了ContextMenu,但是Inspector脚本的菜单里没有

942b855197e36d385c0f49296fb0337e.png

原因:加ContextMenu的方法都不能有参数!否则不出现!

低耦合高内聚思想

就是一个模块只能通过接口方法被其他模块调用,其内部的数据结构不能被外部访问。

04ae26a5f6a149548d3605da882471d8.png

判断低耦合高内聚的方法:

对模块内的一个数据结构Find All References:

8cd159331f2c474fb862c391dba52727.png

只在这个模块出现过。下图也没有完全实现高内聚。

0c876db6194141ca83573e3720c4dfe5.png

NotSupportedException: Specified method is not supported.

这个报错可能对应不止一种具体错误。。

36877967a87e468f99d7171c1b2f0613.png

我出问题的那一行是这样:

2240e9d2b5fe4879868c50397ee45588.png

经过检查是因为这个Resources.Load()路径的资源不存在。但是报的这个错我完全看不懂。我决定不在静态类里用字符串记录预制体路径了,还是往检查器里拖。

Destroy()和DestroyImmediate()

Destroy()是把物体放在一个缓冲区,下一帧删除,所以只能在Play Mode使用,在编辑状态要使用DestroyImmediate()。因为这个区别,删除所有子物体时GetChild()的效果不一样,Play Mode在一个方法里执行Destroy()前后GetChild()返回的值没有变化,可以用GetChild(i),

bce42b68109b4bed9d21c1211f844036.png

编辑模式执行DestroyImmediate()后后面的子物体向前进一位,循环删除应该用GetChild(0).

154743bad3794b859b9eff006d05bff0.png

充分利用动画状态机的逻辑

这个问题在射击游戏笔记里说过,一些代码的执行时机其实刚好就是动画状态机的一些状态切换时。

比如人物跑步、换弹、趴下、站起时不允许瞄准、射击,与其在玩家输入这些动作时判断,其实这些刚好就是人物离开持枪状态的去向,直接写在持枪状态的OnStateExit()即可。

再比如更新枪和子弹的信息,需要执行的时机有很多:从身上取枪时、交换枪时、拿起手里枪对应的弹匣时、换弹时、射击时、放下手里的枪时、放下手里枪的弹匣时、改变自动方式时。其中除了射击、改变自动方式,其他要更新的时机都不在持枪状态,那么就可以写在持枪状态的OnStateEnter()。

2616977e2e48446cbc0faaf72c57613c.png

用lambda表达式给按钮传递带参数的回调方法onClick.AddListener(()=>...)

有个坑,就是在执行这个回调的时候才去读这个lambda表达式返回的值,那个时候参数可能比执行onClick.AddListener时已经变了。看起来按按钮时就去执行回调方法了,其实系统去onClickAddListener那一行计算参数的值了。

93e2eba361714d76b1e0cc0cd21cfce9.png

关于Unity.VisualScripting

cb4550d9f3b24468bd88039c88da6a85.png

据我目前所知,它允许调用如xxx.AddComponent(),xxx是任意组件。AddComponent()本来是GameObject的方法。

raycastHit.transform返回的物体到底™是什么?

一开始我以为RaycastHit.transform是命中的碰撞体的根节点,如果给人物的chest加碰撞体,射线命中了这个碰撞体,Debug.Log(hit.transform)输出的是这个人物的名字
RaycastHit.collider是命中的碰撞体,Debug.Log(hit.collider)输出的是碰撞到的部位的名字。

5c026a66e2889ba62088bfa0656ef62a.png

37a4334aea33ccc7d9558a8dbf6e3901.png

943579fe701dd8e05b6dfa830585087b.png

2024.4.29今天又发现_hit.transform打印的是身体部位的名称。(后来调查发现是因为当时在使用布娃娃,身体部位挂有刚体)

2024.7.27今天又发现_hit.transform打印的是人物的名称。(这是因为骨骼没挂刚体)

2024.8.15

今天发现_hit物体的父级如果没有刚体,_hit.transform就是它自己,如果有刚体,就是它父级。这™是什么逻辑。

845eb1fea33441858a8700748d48ffd2.pnga6a2cd96247f471882bbe94110c5bc7f.png

结论:_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()里一直监视。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值