unity八股

原文参考:Unity八股总结 - loudmute的文章 - 知乎

C#内存管理值类型和引用类型

引用类型:string、数组、(class)类

值类型:其他数据类型,结构体

1747985397976

区别:

值类型:在相互赋值的时候把内容拷贝给对方,一个变另一个不会变

引用类型:两者指向同一个值,一个变另一个也变

namespace 值类型和引用类型;

class Program
{
    static void Main(string[] args)
    {
        //值类型
        int a = 1;
        //引用类型
        int[] arr = new int[] {1,2,3,4 };

        //赋值给另一个变量
        int b = a;
        int[] arr2 = arr;
        Console.WriteLine("a={0},b={1}\narr[0]={2},arr2[0]={3}",a,b,arr[0],arr2[0]);
  
        //修改新的变量
        b = 2;
        arr2[0] = 99;
        Console.WriteLine("a={0},b={1}\narr[0]={2},arr2[0]={3}",a,b,arr[0],arr2[0]);
    }
}

WHY?

值类型存储在栈空间——系统分配,自动回收,小而快

引用类型存储在堆空间——手动申请释放,大而慢

1745215518430

值类型每次申明相当于开了一个栈空间,赋值的时候互不影响。

引用类型申明的时候开的栈空间存放的是一个指针(地址),指向一块堆内存,赋值的时候其实赋的是地址。

习题

1745215752082

  • 10
  • 20
  • "123"

string——特殊的引用类型

每次重新赋值的时候在堆内存重新分配空间,地址也会重新分配

        #region string——特殊的引用类型
        string str1 = "123";
        string str2 = str1;//这一步两个变量指向的地址相同

        str2 = "456";//str2重新赋值,地址改变
        Console.WriteLine("str1={0},str2={1}",str1,str2);
        #endregion

习题

        #region 习题
        int[] arr3 = new int[]{1};
        int[] arr4 = arr3;
        int[] arr5 = arr3;
        arr4[0] = 99;//arr4修改单个元素,地址不变
        arr5 = new int[5];//arr5重新赋值,地址改变,指向新的堆内存
        Console.WriteLine("arr3[0]={0},arr4[0]={1},arr5[0]={2}",arr3[0],arr4[0],arr5[0]);
        #endregion

总结

只要是整体重新赋值(像new int[])地址就会改变

而单独改一个元素,地址不会改变

Unity MonoBehaviour

类的继承关系图

1747986348241

System.Object和UnityEngine.Object

System.Object是C#中所用引用类型的基类

UnityEngine.Object是Unity中所有引用类型的基类,任何 UnityEngine.Object的类型或者其子类类型的变量都可以在属性面板上作为下拉对象进行选择。

GameObject

Scene中的一切都是 GameObject,我们写的大部分"组件类型的脚本"是依附于GameObject而存在的。

Prefab是序列化之后的 GameObject。

GameObject存在是否"激活"的状态, 通过 SetActive来设置, 通过 activeSelfactiveInHierarchy<span> </span>来查询(只有都为 true时, 对象才是真正的"激活")。

只有处于激活状态的 GameObject身上的脚本的生命周期才能生效

GameObject提供组件的添加和获取, 移除组件需要使用 Object.DestroyXXX

Component

Component是组件类的基类, 组件代表所有可以附加到 GameObject的对象

Component提供了 transform/GetComponentXXX等属性和接口, 在内部其实是使用其附加的 GameObject来调用。

Behaviour

Component的基础之上, 添加了"启用"和"禁用"的能力. 只有启用(enabled=true)了的 Behaviour才能进行更新.

常用的继承于 Behaviour的类: AudioSource, Camera, Animation, 当然, 最常用的还是 MonoBehaviour

启用与禁用按钮

Rigidbody之类的类, 继承于 Component, 在属性面板上就没有禁用按钮:

1747987047912

MonoBehaviour

八股:

MonoBehavior的作用体现在三个方面:1)生命周期管理;2)组件化架构;3)丰富的引擎继承功能

  1. 生命周期管理: 提供Awake、Start、Update等回调函数,是我们可以精确控制脚本在不同阶段的初始化、更新和销毁逻辑
  2. 组件化架构: 每个MonoBehaviour脚本都是一个独立的功能模块,可以灵活地挂载到GameObject上,这是一种遵循单一职责原则的设计,使得游戏对象的功能组合更加灵活
  3. 丰富的引擎继承功能: 提够了提供了系统协程、物理回调、消息传递能功能

相比于 Behaviour, MonoBehaviour主要增加了一些方法和很多"消息", 也就是生命周期回调。

参考链接:https://blog.csdn.net/woodengm/article/details/126472371

MonoBehaviour增加的方法

  • print, 效果与 Debug.Log相同
  • 增加的实例方法主要是"延迟调用"和"协程"方面⬇️

延迟调用

  • public void Invoke(string methodName, float time):

    • 延迟 time时间调用 methodName方法, 当 time=0时, 将在下一帧进行调用.
  • public void InvokeRepeating(string methodName, float time, float repeatRate):

    • 延迟 time时间调用 methodName方法,并在之后的每 repeatRate时间重复调用, 当 time=0时, 将在下一帧进行调用, 当 repeatRate=0时, 不会重复调用.
  • public bool IsInvoking(string methodName):

    • methodName方法是否存在 InvokeXXX调用
  • public void CancelInvoke(string methodName):

    • 取消对象上所有 methodName方法的 Invoke调用.
  • public void CancelInvoke():

    • 取消对象上所有 Invoke方法调用.

协程

协程启动
  • public Coroutine StartCoroutine(IEnumerator routine):
    • 将一个迭代器以协程方式执行, 这是Unity最常用的形式 .
  • public Coroutine StartCoroutine(string methodName):
    • 将指定方法当做协程调用.
  • public Coroutine StartCoroutine(string methodName, [DefaultValue("null")] object value):
    • 将指定方法当做协程调用, 另外传递一个引用类型参数.

启动协程后都会返回一个 Coroutine<span> </span>对象, 我们可以将其记录下来作为唯一性的判断或者中途临时关闭协程使用。

关闭协程

默认情况下, 协程执行完毕, 或者脚本所挂载的游戏对象被销毁后, 协程会自动关闭,也可以用以下接口进行手动关闭:

与上面启用协程的三个方法相对应:

  • public void StopCoroutine(Coroutine routine):
    • 通过启动协程返回的 Coroutine对象来关闭协程.
  • public void StopCoroutine(IEnumerator routine):
    • 通过用于启动协程的同一个迭代器对象来关闭协程.
  • public void StopCoroutine(string methodName):
    • 通过方法名来关闭协程, 注意这里, 如果多次通过方法名来启动协程, 每个协程都是独立的, 但是通过方法名来停止协程, 会将所有以方法名启动的协程关闭.

停止该脚本对象上该脚本启用的所有协程

  • public void StopAllCoroutines():
    • 停止脚本对象上所有协程, 只会对脚本启动的协程起作用, 也就是说不影响同一个游戏对象上, 由其他脚本启动的协程.

手动关闭协程时, 其真正的关闭点是迭代器的下一次调用:

private IEnumerator Test()
{
    int i = 0;
    while(i <= 10)
    {
        Debug.Log($"Test {Time.frameCount}");
        yield return new WaitForSeconds(1); // 这里

        i++;
        if (i >= 1)
        {
            StopAllCoroutines(); // 这里停止之后, 需要在下一次循环的[第7行]才真正关闭
            Debug.Log($"Finish {Time.frameCount}");
        }
    }

    Debug.Log($"Finish {Time.frameCount}"); // 自动关闭才执行到这里
}

异步任务

可以直接使用.net提供的异步任务机制(async/await/Task)来实现协程或者延迟调用

总结

几个Object

  • System.Object是C#世界中的对象,类型标识object
    • “创建对象(构造函数)”
    • “判等(Equal)”
    • “GetHashCode”
    • “GetType”
    • “ToString”
  • UnityEngine.Object是Unity世界中的对象,类型标识Object
    • “名称”
    • “对象的创建(构造函数), 查找和销毁”
    • “获取对象实例id”
    • “重载操作符(==, !=, bool)”
  • UnityEngine.GameObject是Unity场景中的实体对象,类型标识GameObject
    • “创建游戏对象(构造函数)”
    • “在世界中的定位描述(Transform)”
    • “激活与否的状态(active, 在世界中是否可见, 可用)”
    • “查找游戏对象”
    • “添加和获取组件”
    • “向身上挂载的MonoBehaviour发送消息(调用函数)”

继承关系, 抽象的程度不同, 一层层增加了适应自身所在世界的属性和函数.

几个Component

  • 组件Component在UnityEngine.Object的基础之上, 额外提供了:
    • "附加"到GameObject上的能力. 也就是说, Unity的场景由一个个游戏对象组成, 而游戏对象又包含一个个Component.
  • Behaviour在Component的基础之上, 额外提供了:
    • "是否可用"的勾选. 也就是说每个Behaviour可以附加到游戏对象上, 而且可以设置可用与否.
  • MonoBehaviour在Behaviour的基础之上, 额外提供了:
    • "延迟函数调用"和"协程执行函数"的能力, 更重要的是提供了生命周期函数, 配合Unity内置的Component来完成各种需求的开发.

Unity 生命周期管理

游戏对象的生命周期包括四个阶段:1)初始化;2)游戏循环;3)非活跃期;4)销毁

初始化:Awake, OnEnable, Start

Awake()

  • 对象刚被创建时立即调用(即使脚本未启用)
  • 适合初始化变量、获取组件引用

OnEnable()

  • 脚本或对象激活时调用
  • 适合注册事件监听、恢复游戏状态

Start()

  • 在所有 Awake() 调用完成之后、第一帧 Update() 之前,进行 组件之间依赖关系的初始化
  • 适合其他组件完成初始化后的设置,基本就是用来初始化参数

游戏循环:FixedUpdate, Update, LateUpdate

FixedUpdate

默认值:

Time.fixedDeltaTime = 0.02f; // 默认每 0.02 秒调用一次(即每秒50次)

可在Start()中初始化自定该固定时间间隔

void Start()
{
    Time.fixedDeltaTime = 0.01f; // 设置为每秒调用100次
}
  • 固定时间间隔调用,不受帧率波动影响,更稳定地处理物理逻辑
  • 适合物理计算,如Rigidbody、Collider,力的施加、刚体运动控制等

Update()

  • 每帧调用一次(不固定时间间隔)
  • 适合游戏逻辑、输入处理

LateUpdate()

  • 在所有Update之后调用
  • 适合摄像机跟随等需要最后调整的逻辑

非活跃期:OnDisable

OnDisable()

  • 脚本/对象被禁用时调用
  • 适合取消事件监听、保存临时状态

销毁:OnDestroy,OnApplicationQuit

OnDestroy()

  • 对象被销毁前调用
  • 适合释放资源、清理引用

OnApplicationQuit()

  • 游戏退出时所有对象都会收到
  • 适合保存游戏最终状态

总结

  • Awake vs Start: Awake是更早的初始化(无论启用否),Start是启用后、第一帧前的最后初始化。就像“天生自带的能力” 和 “开始工作前的最后准备”。
  • FixedUpdate vs Update: FixedUpdate物理时钟驱动,稳定间隔;Update渲染帧驱动,间隔不稳定。物理计算放FixedUpdate!
  • OnEnable vs OnDisable: 它们是配对的。在对象频繁激活/禁用时(如对象池),这两者比 Awake/StartOnDestroy更常用。
  • LateUpdate的作用: 当某个行为必须在所有其他关键逻辑计算之后才发生时用(如摄像机跟踪的主角位置,必须在主角移动计算完成后才跟踪)。

Unity 协程

协程是 运行在单线程内的可暂停函数 ,通过 yield指令在代码中插入多个断点,让执行流可以在这个断点暂停和恢复。

其核心特点包括:

1)单线程执行:所有协程在调用它的主线程上执行

2)主动让出:通过 yield指令主动转让控制权

3)极低开销:上下文切换只需保存少量寄存器的值

4)无并行性:多个协程交替执行,而不是同时运行

进程、线程和协程

进程是操作系统分配和调度资源的基本单位,拥有独立内存空间,切换开销大;线程是进程中的执行流,共享进程资源,由操作系统调度,能实现并行但需处理线程同步;协程是运行在单线程内的可暂停函数,通过 yield指令实现交替执行,无并行能力但切换成本极低;

打个比方:进程是单个公司,线程是公司内多个部门同时工作,协程就是同部门多个员工轮岗;资源隔离性递减,切换成本递减

为什么需要协程

协程时处理时序逻辑的利器,通过分段执行的机制,解决了以下核心痛点:

1)时序控制:用同步代码实现流程,避免回调地狱

2)性能优化:通过 yield return nullWaitForSecond 实现分帧操作,将密集计算分摊到多帧执行,避免单帧卡顿

3)引擎整合:原生支持Unity的生命周期,直接调用UnityAPI,无需考虑多线程问题

实现原理

Unity协程的实现包括两个部分:1)编译器生成状态机;2)Unity引擎的协程调度

编译器生成状态机

我们编写一个协程代码如下时

IEnumerator MyCoroutine() {
    Debug.Log("A");
    yield return new WaitForSeconds(1);
    Debug.Log("B");
}

C#会将其编译为如下的状态机类,所谓状态机类时编译后的隐藏类,通过状态变量和分段执行实现协程的暂停/恢复功能,MoveNext()中的逻辑:1)执行当前代码语句;2)执行 yield指令后的代码;3)变量更新;代码如下

class <MyCoroutine> : IEnumerator {
    private int _state; //状态变量
    private object _current; //yield指令暂停的分割点
  
    bool MoveNext() {
        switch(_state) {
            case 0: // 对应yield之前的代码
                Debug.Log("A");
                _current = new WaitForSeconds(1);
                _state = 1;
                return true;
            case 1: // 对应第一个yield之后的代码
                Debug.Log("B");
                _state = -1; // 结束标记
                return false;
        }
        return false;
    }
}

Unity引擎的协程调度

Unity引擎底层维护了一个活跃协程列表,1)每帧末尾检查所有活跃协程;2)通过 MoveNext()推进协程到下一个 yield点;3)自动移除已完成的协程,伪代码逻辑如下

class CoroutineScheduler {
    List<IEnumerator> _activeCoroutines;
  
    void Update() {
        for(int i=0; i<_activeCoroutines.Count; i++) {
            var coroutine = _activeCoroutines[i];
            // 检查yield条件是否满足
            if(IsYieldConditionMet(coroutine.Current)) { 
                if(!coroutine.MoveNext()) { // 推进状态机
                    _activeCoroutines.RemoveAt(i--);
                }
            }
        }
    }
  
    bool IsYieldConditionMet(object yieldInstruction) {
        if(yieldInstruction == null) return true; // yield return null
        if(yieldInstruction is WaitForSeconds wfs) 
            return Time.time >= wfs._resumeTime;
        // 其他yield类型判断...
    }
}

Unity Delegate(委托)

参考链接:【C# 教程】一次搞懂什么是 Delegate 委托

delegate是C#中类型安全的函数指针,允许将方法作为参数传递或动态调用,本质是对方法的引用和封装

当几个方法中只有某个部分不同,其他部分相同,这个不同的部分就可以用deligate()来代替呼叫对应的方法,然后在方法的传参里面加入delegate参数。

将“做什么”和“谁来做”分离,通过方法抽象实现:

1)解耦,模块间无需相互引用;

2)扩展性,新增功能无需修改原有代码;

3)复用性,统一委托可绑定不同方法

一个简单的应用:

1747990142880

这部分还需要学习,留个坑。。。


碰撞检测

Unity的碰撞检测(Collision Detection)是物理系统的核心功能,用于判断游戏对象之间的接触或交叉,并触发相应事件。

Collider、Rigidoby、Trigger

Collider决定碰撞形状 ,在此基础上Rigdbody和Trigger二选一

  • Rigidbody决定物理引擎开关,是需要添加的一个组件,为物体添加质量、重力等物理属性,使其受物理引擎驱动(移动、旋转、受力)
  • Trigger决定事件检测开关,在Collider中勾选,关闭物理碰撞,仅保留事件检测

碰撞事件与触发事件

碰撞事件(物理响应):双方均有 Collider+至少一方有 Rigidbody

触发事件(无物理响应):双方均有 Collider+至少一方勾选 Is Trgger

碰撞检测的实现

1)基础代码

public class Bullet : MonoBehaviour {
    void OnCollisionEnter(Collision collision) {
        if (collision.gameObject.CompareTag("Enemy")) {
            Destroy(collision.gameObject); // 击中敌人
        }
    }
}

2)射线检测(Raycast)

RaycastHit hit;
if (Physics.Raycast(transform.position, transform.forward, out hit, 10f)) {
    Debug.Log("击中:" + hit.collider.name);
}

3)形状检测(Overlap)

Collider[] hits = Physics.OverlapSphere(transform.position, 5f);
foreach (var hit in hits) {
    // 处理检测结果
}

UGUI

UGUI的作用主要在于两方面:1)显示 2)交互

显示:我们在屏幕中看到的UI的图形和布局;包括:1)图形的渲染,从3维到2维再到显示在屏幕上 2)UI元素控件布局,尺寸调整

交互:收到一件事情,让谁去干这件事,怎么干这件事;包括:1)输入 2)射线检测 3)控件事件交互

显示和交互一共包含的五个步骤,就是我们UGUI的核心类继承关系图,如下图,分别有五个模块:

1)输入模块BaseInputModule;

2)射线检测模块BaseRayCaster;

3)交互模块Selectable;

4)布局模块LayoutGroup;

5)图形模块Graphic

1749126848828

仅仅有了UGUI核心的五个类模块还不行,我们需要让他们修调运作。

在场景中创建Canvas时,还会自动创建一个EventSystem

所以当我们创建UI时,会有两个大的实体 :Canvas和EventSystem;

UGUI的两个大作用1)显示 2)交互

EventSystem就负责管理"交互"

Canvas和CanvasUpdateRegistry(一个静态服务类)就负责 "显示";

EventySystemCanvasCanvasUpdateRegitry,组织调度了UGUI的五个类模块,使其协同运作,组成了UGUI显示和交互的循环往复,这五个模块的顺序如下

1749126887900

EventSystem

EventSystem是一个实体,负责管理UI的交互,作用包括:

1)输入处理,将输入处理转换作为逻辑事件;

2)射线检测,通过GraphicRaycaster确定命中的UI元素;

3)状态管理,控制Seclectable的交互状态(如按钮按下或禁用状态);

每个场景只需要一个EventSystem实体

CanvasUpdateRegistry

CanvasUpdateRegistry是一个静态状态类,负责UI的更新,作用包括:

1)脏标记管理,记录需要更新的UI元素;

2)批量更新,在帧末统一处理所有更新请求;

3)优先级控制,确保计算布局再更新图形

Canvas

Canvas是一个实体,是UI元素的物理承载和渲染终端,作用包括:

1)渲染空间定义,决定UI显示在屏幕空间/世界空间;

2)合批绘制,将相同材质的UI在同一批绘制;

3)渲染控制,通过CanvasRender将网格数据提交给GPU

posted @ 2025-05-23 16:55  EanoJiang  阅读(139)  评论(0)    收藏  举报