延迟函数
- 定义:延迟函数是MonoBehaviour基类提供的可以延时执行的函数
- 特点:
- 可以自定义要执行的函数和延迟时间
- 继承MonoBehaviour后即可使用
- 通过反射机制根据函数名查找并执行对应方法
延迟函数的使用
cs
//Invoke()
//参数一:函数名 字符串
//参数二:延迟时间 秒为单位
Invoke("DelayFunc", 1);
private void DelayFunc()
{
print("延时执行函数");
}
//注意:
//延时函数第一个参数传入的是函数名字符串
//延时函数无法传入参数,需包裹一层无参函数
//函数名必须是该脚本上申明的函数
如何包装有参函数
csInvoke("DelayDoSomething", 1); private void DelayDoSomething() { print("延时执行的函数"); TestFun(2); } private void TestFun(int i) { print("传入参数" + i); }
延迟重复函数
其注意事项与延迟函数一致
延时函数第一个参数传入的是函数名字符串
延时函数无法传入参数,需包裹一层无参函数
函数名必须是该脚本上申明的函数
cs
//InvokeRepeating()
//参数一:函数名 字符串
//参数二:第一次执行的延迟时间 秒为单位
//参数三:之后每次执行的间隔时间 秒为单位
//延迟5s执行第一次,后续每隔2s重复执行一次
InvokeRepeating("DelayFunc", 5, 2);
private void DelayFunc()
{
print("延时重复执行函数");
}
延迟函数练习题

延迟函数一般放在Start函数中,如果放在Update函数中会一直启用该函数就会导致混乱。
计时器
csvoid Start() { //第一次在0s瞬间执行该函数,后面每一次过一秒执行DelayFun InvokeRepeating("DelayFun", 0, 1); DelayFun2(); } private void DelayFun() { print(time + "秒"); ++time; } private void DelayFun2() { print(time + "秒"); ++time; Invoke("DelayFun2", 1); }输出结果如下:
延迟摧毁
csvoid Start() { //通过Destroy来进行延迟销毁 Destroy(this.gameObject, 5); Invoke("DelayDes", 5); } 0 个引用 private void DelayDes() { Destroy(this.gameObject); }
取消延迟函数
cs
取消该脚本上的所有延时函数
CancelInvoke();
取消指定函数名的延迟函数
只要取消了指定延迟,无论函数开启了多少次延迟执行,都会统一取消
CancelInvoke("DelayFunc");
判断是否存在延迟函数
if( IsInvoking() )
{
print("存在延迟函数");
}
if( IsInvoking("DelayFunc") )
{
print("存在函数名为 Func 的延迟函数");
}
延迟函数的失活
- 对象或脚本失活无法停止延时函数执行
- 销毁组件或者对象才会停止或者代码停止
多线程
Unity中多线程的使用不算常见,只是面试需要了解部分内容
- 支持情况:Unity确实支持多线程,可以创建并运行新线程
- 关键限制:
- 新线程无法访问Unity相关对象的内容(如Transform、GameObject等)
- 尝试访问会抛出"get_transform can only be called from the main thread"异常
- 新线程无法访问Unity相关对象的内容(如Transform、GameObject等)
- 注意事项:
- 必须手动关闭创建的多线程(在OnDestroy中使用t.Abort())
- Unity大部分API都不能在非主线程中调用
cs
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
public class MultiThreadTest : MonoBehaviour
{
// 声明线程对象
private Thread t;
// 申明一个公共内存容器(队列),用于线程间通信
private Queue<Vector3> queue = new Queue<Vector3>();
// 可选:另一个队列(演示多个共享容器)
private Queue<Vector3> queue2 = new Queue<Vector3>();
void Start()
{
t = new Thread(Test);
t.Start();
}
// 后台线程执行的方法
private void Test()
{
while (true)
{
Thread.Sleep(1000); // 模拟耗时操作,每秒执行一次
// 相当于模拟复杂算法算出了一个结果,然后放入公共容器中
queue.Enqueue(new Vector3(1, 2, 3));
}
}
// 当对象销毁时,终止线程并清理资源
private void OnDestroy()
{
if (t != null && t.IsAlive)
{
t.Abort(); // 强制终止线程(不推荐,但简单)
// 更安全的方式是使用标志位控制循环,避免 Abort
}
t = null;
}
// 主线程中定期处理队列中的数据(例如在 Update 中)
void Update()
{
if (queue.Count > 0)
{
Vector3 result = queue.Dequeue();
Debug.Log($"从队列中取出结果: {result}");
}
}
}
协程
协同程序,简称协程。它是一种"假"的多线程技术,本质上并不是真正的多线程。
它的主要作用是将代码分时执行,从而避免卡住主线程。简单理解,就是把那些可能会让主线程卡顿的耗时逻辑,分时分步地执行。
主要使用场景包括:
- 异步加载文件
- 异步下载文件
- 场景异步加载
- 批量创建时防止卡顿
协程与线程的关系

协程的使用
如下所示,返回值类型IEnumerator,所以协程函数中必须要返回某种数据:即yield return......
cs
关键点一:协程程序(协程)函数返回值必须是 IEnumerator 或者继承它的类型
IEnumerator MyCoroutine(int i, string str)
{
print(i);
// 关键点二:协程函数当中必须使用 yield return 进行返回
yield return new WaitForSeconds(5f);
print(str);
}
银鸟工作室------协程
https://blog.csdn.net/2303_80204192/article/details/156720077
开启协程
cs
// 第二步:开启协程函数
// 协程函数是不能够直接这样去执行!!!!!
// 这样执行没有任何效果
// MyCoroutine(1, "123");
// 常用开启方式
第一种:
IEnumerator ie = MyCoroutine(1, "123");
StartCoroutine(ie);
第二种:
StartCoroutine(MyCoroutine(1, "123"));
这里可以开启多个相同的协程
协程的关闭
cs
Coroutine c1 = StartCoroutine(MyCoroutine(1, "123"));
Coroutine c2 = StartCoroutine(MyCoroutine(1, "123"));
Coroutine c3 = StartCoroutine(MyCoroutine(1, "123"));
// 第三步:关闭协程
// 关闭所有协程
// StopAllCoroutines();
// 关闭指定协程
StopCoroutine(c1);
yield return相关
cs
下一帧执行
yield return 数字;
yield return null;
等待指定秒后执行
yield return new WaitForSeconds(秒);
等待下一个固定物理帧更新时执行
yield return new WaitForFixedUpdate();
等待摄像机和GUI渲染完成后执行
yield return new WaitForEndOfFrame();
一些特殊类型的对象 比如异步加载相关函数返回的对象
之后讲解 异步加载资源 异步加载场景 网络加载时再讲解
一般在Update和LateUpdate之间执行
跳出协程
yield break;
yield return new WaitForSeconds(5f);
当延迟五秒的这个条件满足了,然后在之后这个满足条件过后,
update中所有函数执行完过后,然后再在lateupdate将要执行之前去执行的
跳出协程
yield break后面的协程不会被执行
协程的失活

协程的使用原理
- 组成结构: 协同程序可以分成两部分
- 1.协程函数本体
- 2.协程调度器
- **函数本体特性:**协程本体就是一个能够中间暂停返回的函数
- **调度器功能:**协程调度器是Unity内部实现的,会在对应的时机帮助我们继续执行协程函数
- **实现范围:**Unity只实现了协程调度部分,协程的本体本质上就是一个C#的迭代器方法

练习题------协程的使用

cs
public class Corotine : MonoBehaviour
{
private int time = 0;
void Start()
{
StartCoroutine(settime());
}
public IEnumerator settime()
{
while (true)
{
print($"第{time}秒");
time++;
yield return new WaitForSeconds(1);
}
}
}
1秒等待结束后,协程会从yield return语句之后继续执行,也就是循环体的结束,然后由于是while(true)循环,会再次进入循环体。
开始协程放在Start函数中------不可能每帧都执行开始协程
cs
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
StartCoroutine(CreateCube(10000));
}
}
IEnumerator CreateCube(int num)
{
for (int i = 0; i < num; i++)
{
GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
obj.transform.position = new Vector3(Random.Range(-100, 100), Random.Range(-100, 100), Random.Range(-100, 100));
if (num % 1000 == 0)
yield return null;
}
}
创建100000个物体,如果一次性在一帧中创建,会发生卡顿。但如果分批次处理------每生成一千个等待一帧再生成一千个,就缓解卡顿的情况
特殊文件夹

此处代码输出结果是当前游戏项目的Asset文件夹
Resource文件夹
因为打包后会加密,所以一般不直接获取
描述:存放在运行时需通过Resources.Load动态加载的资源,但使用 Resources文件夹中的资源会增加构建体积,因为这些资源会被包含在构建文件中,即使在运行时并不一定用到这些资源,且使用Resources文件夹的资源会被打包到游戏的所有平台版本中,因此要谨慎使用。
cs
#region 知识点二 Resources 资源文件夹
//路径获取:
//一般不获取
//只能使用Resources相关API进行加载
//如果硬要获取 可以用工程路径拼接
print(Application.dataPath + "/Resources");
//注意:
//需要我们自己将创建
//作用:
//资源文件夹
//1-1.需要通过Resources相关API动态加载的资源需要放在其中
//1-2.该文件夹下所有文件都会被打包出去
//1-3.打包时Unity会对其压缩加密
//1-4.该文件夹打包后只读 只能通过Resources相关API加载
输出结果如下:正好是Resources文件夹的路径
StreamingAssets 流动资源文件夹
- 不能使用Application.dataPath + "/StreamingAssets"拼接路径
- 平台差异:在不同平台下打包后路径会变化,拼接方式会导致路径错误
描述:存储配置文件、外部数据或其他需要在运行时直接访问的资源,且在运行时不需要修改。这些文件会被原样复制到构建目录中,可以通过绝对路径访问。
cs
#region 知识点三 StreamingAssets 流动资源文件夹
//路径获取:
print(Application.streamingAssetsPath);
//注意:
//需要我们自己将创建
//作用:
//流文件夹
//2-1.打包出去不会被压缩加密,可以任由我们摆布
//2-2.移动平台只读,PC平台可读可写
//2-3.可以放入一些需要自定义动态加载的初始资源
persistentDataPath 持久数据文件夹
- 自动创建:不需要手动创建该文件夹,Unity会自动生成
- 平台差异性:在不同平台下路径不同,无法在Unity编辑器中直接查看
- 存储位置:存储在设备本地(如PC的C盘用户目录或手机存储中),与用户名相关
cs
#region 知识点四 persistentDataPath 持久数据文件夹
//路径获取:
print(Application.persistentDataPath);
//注意:
//不需要我们自己将创建
//作用:
//固定数据文件夹
//3-1.所有平台都可读可写
//3-2.一般用于放置动态下载或者动态创建的文件,游戏中创建或者获取的文件都放在其中
Plugins 插件文件夹
描述:存放插件或本地化的插件库。这里的插件通常包括DLL文件、原生插件等,用于扩展Unity的功能或与外部系统集成。插件通常包括平台特定的代码(如Android的.jar文件或iOS的.a文件),这些文件在构建时会被正确地处理。
使用场景:集成了一个第三方的原生插件库,如一个用于图像处理的DLL文件。在 Assets/Plugins/文件夹中放置这个DLL文件,这样Unity可以在构建过程中正确处理和链接这个插件。
cs
#region 知识点五 Plugins 插件文件夹
//路径获取:
//一般不获取
//注意:
//需要我们自己将创建
//作用:
//插件文件夹
//不同平台的插件相关文件放在其中
//比如IOS和Android平台
Editor 编辑器文件夹
描述:存放自定义编辑器脚本、工具或编辑器扩展,这些内容只在Unity编辑器中有效,不会被包含在最终构建中。如可以创建自定义Inspector、编辑器窗口等功能来增强编辑器的功能。
cs
#region 知识点六 Editor 编辑器文件夹
//路径获取:
//一般不获取
//如果硬要获取 可以用工程路径拼接
print(Application.dataPath + "/Editor");
//注意:
//需要我们自己将创建
//作用:
//编辑器文件夹
//5-1.开发Unity编辑器时,编辑器相关脚本放在该文件夹中
//5-2.该文件夹中内容不会被打包出去
默认资源文件夹 Standard Assets
- 创建方式:需要手动创建,命名为"Standard Assets"
- 典型内容:
- Unity官方提供的默认资源
- 从Asset Store下载的标准资源包
cs
#region 知识点七 默认资源文件夹 Standard Assets
//路径获取:
//一般不获取
//注意:
//需要我们自己将创建
//作用:
//默认资源文件夹
//一般Unity自带资源都放在这个文件夹下
//代码和资源优先被编译
#endregion
资源加载
为什么需要进行资源加载,是避免直接将资源模型内容,直接进行拖拽,导致管理混乱。通过代码进行统一管理,自动化配置
加载预设体
cs
// 加载预设体资源(未实例化)
GameObject prefab = Resources.Load<GameObject>("Cube");
// 实例化到场景中
Instantiate(prefab)
(1)加载的预设体只是配置数据,需通过Instantiate生成实例
(2)假设Cube.prefab位于Resources/Prefabs/Cube,则路径为"Prefabs/Cube" (Resource文件夹在Resource类中不直接写入路径中)
| 概念 | 定义 | 关键操作 |
|---|---|---|
| 加载资源 (Load) | 从存储介质(磁盘、内存、网络)读取资源数据到内存 | Resources.Load、AssetBundle.LoadAsset、Addressables.LoadAsset |
| 生成/实例化 (Instantiate) | 在内存中根据资源数据创建可运行的游戏对象 | GameObject.Instantiate、Object.Instantiate |
音频使用
cs
//加载资源
AudioClip clip = Resources.Load<AudioClip>("Music/BkMusic");
//播放音频
audioSource.clip = clip;
audioSource.Play();
加载文本
cs
TextAsset textAsset = Resources.Load<TextAsset>("Txt/Text");
Debug.Log(textAsset.text); // 获取文本内容
//文本内容
print(ta.text);
//字节数据组
print(ta.bytes);
(1)支持txt/.xml/.json等文本格式
(2)通过text属性获取字符串,bytes获取二进制数据
加载图片
cs
Texture texture = Resources.Load<Texture>("Picture/4");
// GUI显示示例
void OnGUI() {
GUI.DrawTexture(new Rect(0,0,100,100), texture);
}
文件同名怎么办
● 问题:Resources.Load加载同名资源时无法准确识别
● 解决方案:
○ 使用指定类型加载API
○ 加载指定名字的所有资源
● 推荐做法:避免资源同名,保持资源命名唯一性
cs
// 指定类型加载------同名资源类型不同,一个Text,一个Texture
Texture tex = Resources.Load("Picture/4", typeof(Texture)) as Texture
// 加载所有同名资源------用if函数判断是否是指定类型
Object[] all = Resources.LoadAll("Picture/4");
foreach(Object obj in all){
if(obj is Texture2D) {
// 使用Texture2D资源
}
使用泛型加载如上资源类型
cs
// 泛型方法(推荐)
T Resources.Load<T>(string path) where T : Object;
优势:
无需类型转换(省去as操作)
直接返回指定泛型类型的结果
TextAsset ta2 = Resources.Load<TextureAsset>("Tex/TestJPG");
print(ta2.text);
tex = Resources.Load<Texture>("Tex/TestJPG");
异步加载
如上都是同步加载中,如果我们加载过大的资源,可能会造成程序卡顿。
卡顿的原因是:从硬盘将数据读取到内存的过程需要进行计算,而资源越大,耗时越长,从而导致掉帧或卡顿。
Resources 异步加载的原理:Unity 在内部开启一个独立线程来执行资源加载操作,不会阻塞主线程,因此不会造成游戏画面卡顿,能有效保障运行流畅性。
- 加载特性:
- 异步加载不能立即得到资源,至少需要等待一帧
完成事件
异步加载不能马上得到资源,所以要先判断是否为空
**这里比较疑惑:**加载资源时,明明使用了泛型,却依旧使用as
LoadOver()方法接收的是一个AsyncOperation类型参数 ,而AsyncOperation是一个基类,它不知道具体是哪种异步操作(比如资源加载、场景加载、协程等)。
- 所以在
LoadOver()里,rq实际上是一个AsyncOperation,丢失了具体的子类信息。(rq as ResourceRequest)将AsyncOperation转换为ResourceRequest,以便访问.asset属性。因为
.asset的类型是Object,所以tex想得到图片还需要as Texture
代码剖析

流程原理

题目练习------创建一个异步加载的资源管理器

cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public class ResourceMgr
{
private static ResourceMgr instance=new ResourceMgr();
public static ResourceMgr Instance=> instance;
private ResourceMgr() { }
public void LoadResoure<T>(string name, UnityAction<T>callback) where T : Object
{
ResourceRequest rq = Resources.LoadAsync<T>(name);
rq.completed += (AsyncOperation op) =>
{
// 安全转换:op 一定是 ResourceRequest(AsyncOperation是ResourceRequest父类)
ResourceRequest request = op as ResourceRequest;
T asset = request.asset as T;
// 安全调用回调(避免 callback 为 null 导致异常)
callback?.Invoke(asset);
};
}
}
cs
public class Lesson18Test : MonoBehaviour
{
private Texture tex; // 存储加载的纹理
void Start()
{
ResourceMgr.Instance.LoadResoure<Texture>("Tex/TestJPG", (obj) =>
{
tex = obj; // 将加载的纹理赋值给成员变量
Debug.Log("加载纹理成功");
});
}
private void OnGUI()
{
if (tex != null)
GUI.DrawTexture(new Rect(0, 0, 100, 100), tex); // 绘制纹理
}
}
协程
为什么协程也能异步获取------因为返回值数据类型的基类是一致的

通过协程加载资源

判断资源是否加载结束

两种异步加载的使用优劣

资源卸载
- 缓存机制:Resources加载一次资源后,该资源会一直存放在内存中作为缓存
- 重复加载特性:
- 第二次加载时发现缓存中存在该资源会直接取出使用
- 多次重复加载不会浪费内存
- 性能消耗:每次加载都会去查找取出,始终伴随一些性能消耗
cs
void Update()
{
if (Input.GetKeyDown(KeyCode.Alpha1))
{
print("加载资源");
tex = Resources.Load<Texture>("Tex/TestJPG");
}
if (Input.GetKeyDown(KeyCode.Alpha2))
{
print("卸载资源");
Resources.UnloadAsset(tex);
tex = null;
}
}
检测是否其效果:
可以Ctrl+7打开Profiler界面------然后按键查看Textrue的变化
卸载未使用的资源
cs
//2. 卸载未使用的资源
//注意:
//一般在过场景时和GC一起使用
Resources.UnloadUnusedAssets();
GC.Collect();
异步切换场景
通常我们进行场景加载的时候,我们会使用如下代码
cs
SceneManager.LoadScene();
但是如果此时场景内资源特别多,这个时候我们就会卡住
那么在游戏中,我们此时就不会立即跳转,而是使用进度条来告诉玩家已经加载了多少内容
本质上就是在不影响当前画面的前提下,加载场景
通过事件异步加载场景
cs
#region 知识点一 场景异步切换
//1.通过事件回调函数 异步加载
AsyncOperation ao = SceneManager.LoadSceneAsync("Lesson20Test");
//当场景异步加载结束后 就会自动调用该事件函数 我们如果希望在加载结束后 做一些事情 那么久可以在该函数中
//lamda表达式
ao.completed += (a) =>
{
print("加载结束");
};
//使用方法函数
ao.completed += LoadOver;
练习题------写一个场景管理器

为什么不推荐无回调切换
cs时间线 → ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 【带回调的版本】 调用 LoadSceneAsync ──加载中──┬──回调执行──→ 确定完成 ↑ 你在这里"知道"完成了 【你的版本(无回调)】 调用 LoadSceneAsync ──加载中──┬──??──→ 场景变了 ↑ 你不知道具体是哪一帧完成的无回调版本就会出现如下情况:
csvoid Start() { SceneMgr.Instance.LoadScene("Game"); // 下一行执行时,场景可能还没切换! Debug.Log("这行立即打印,场景还是旧的"); // ❌ 无法在这里安全地访问新场景的对象 // GameObject.Find("NewSceneObject"); // 可能找不到! }
cs
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.SceneManagement;
public class SceneMgr
{
private static SceneMgr instance = new SceneMgr(); // 单例实例
public static SceneMgr Instance => instance; // 公共访问点
private SceneMgr() { } // 私有构造函数,防止外部实例化
public void LoadScene(string name, UnityAction action)
{
AsyncOperation ao = SceneManager.LoadSceneAsync(name);
ao.completed += (a) => action(); // 加载完成后执行回调
}
}
cs
void Start()
{
// 场景切换前显示加载界面
SceneMgr.Instance.LoadScene("Game", () => {
Debug.Log("场景切换完成!");
// 可以在这里:关闭加载界面、初始化玩家、播放音乐等
});
}
协程异步加载场景
cs
StartCoroutine(LoadScene("MyScene"));
IEnumerator LoadScene(string name)
{
AsyncOperation ao = SceneManager.LoadSceneAsync(name);
print("异步加载过程中 打印的信息"); // ✅ 立即执行
yield return ao; // ✅ 协程在此挂起,等待加载完成
print("异步加载结束后 打印的信息"); // ✅ 加载完成后才执行
}
在异步加载场景过程中更新进度条
第一种:
第一种不太推荐
cs
//比如 我们可以在异步加载过程中 去更新进度条
//第一种 就是利用 场景异步加载 的进度 去更新 但是 不是特别准确 一般也不会直接用
while(!ao.isDone)
{
print(ao.progress);
yield return null;
}
第二种:
不要盲目相信 Unity 的自动进度值,而是根据游戏自身的加载流程,手动设计进度条的增长逻辑
-
资源可能早已加载完成
- 比如通过
Resources.LoadAsync()或Addressables提前加载了怪物、模型等。 - 它们已经在内存中,只是还没有被实例化(Instantiate)或放置到场景中。
- 比如通过
-
进度条反映的是"游戏就绪度",而非"文件读取进度"
- 你关心的不是"硬盘读了多少字节",而是:
- 场景结构是否建好?
- 怪物是否生成完毕?
- UI 是否初始化完成?
- 音效系统是否准备就绪?
- 这些才是玩家感知的"加载完成"。
- 你关心的不是"硬盘读了多少字节",而是:
cs
//第二种 就是根据你游戏的规则 自己定义 进度条变化的条件
yield return ao;
//场景加载结束 更新20%进度
//接着去加载场景中的其它信息
//比如
//动态加载怪物
//这时 进度条 再更新20%
//动态加载 场景模型
//这时 就认为 加载结束了 进度条顶满
两种异步加载方式的优劣
- 前者是加载结束啊,才能够去毁掉。
- 后者是在这个异步加载过程中,我就可以去做一些处理
根据需求选择即可

LineRender
- 组件功能: Unity提供的专门用于画线的组件,可在场景中绘制线段
- 主要用途:
- 绘制攻击范围
- 武器红外线
- 辅助功能
- 其他画线需求(如圆形、方形或不规则线段)
- 本质理解: 通过设置多个点坐标,将这些点连接起来形成线段
主要功能略讲


顶点圆角

材质设置

几种编辑模式
都不太常用,一般使用代码布置点位

代码控制LineRender
cs
#region 知识点三 LineRenderer代码相关
//动态添加一个线段
GameObject line = new GameObject();
line.name = "Line";
LineRenderer lineRenderer = line.AddComponent<LineRenderer>();
//首尾相连
lineRenderer.loop = true;
//开始结束宽
lineRenderer.startWidth = 0.02f;
lineRenderer.endWidth = 0.02f;
//开始结束颜色
lineRenderer.startColor = Color.white;
lineRenderer.endColor = Color.red;
//接着就设置 对应每个点的位置
//第一种是直接数组一次性声明
lineRenderer.SetPositions(new Vector3[] {
new Vector3(0,0,0),
new Vector3(0,0,5),
new Vector3(5,0,5)
});
//第二种是索引声明
lineRenderer.SetPosition(3, new Vector3(5, 0, 0));
//是否使用世界坐标系
//决定了 是否随对象移动而移动
lineRenderer.useWorldSpace = false;
//让线段受光影影响 会接受光数据 进行着色器计算
lineRenderer.generateLightingData = true;
练习题
范围检测
碰撞相关的概念重温
碰撞产生的必要条件
至少有一个物体必须挂载刚体(Rigidbody)组件 。
刚体是 Unity 物理系统的核心,只有带有刚体的物体才能参与物理模拟(如受重力影响、产生碰撞响应等)。
两个物体都必须拥有碰撞器(Collider)组件 。
碰撞器定义了物体的物理形状(如 Box Collider、Sphere Collider 等),用于检测与其他物体的接触。
碰撞与触发的区别
碰撞(Collision)
会产生真实的物理效果,例如反弹、停止运动、施加力等。适用于需要物理交互的场景,如角色撞击墙壁、车辆相撞等。
触发(Trigger)
不会产生物理效果,但可以通过脚本监听进入、停留或离开事件。只需将任意一方的碰撞器勾选
Is Trigger属性即可启用。常用于逻辑判断,如拾取道具、区域检测、自动开门等。
而范围检测的基本概念:
- 定义: 检测特定区域内是否存在符合条件物体的方法
- 特点: 不需要实际物理碰撞即可判断物体位置关系
- 必备条件 :想要被范围检测到的对象,必须具备碰撞器(Collider)!
- 注意点 :
- 范围检测相关API是瞬时的,只有当执行该代码时才进行一次范围检测。
- 范围检测相关API并不会真正产生一个碰撞器,它仅仅是进行碰撞判断计算
Unity Physics 类提供了一系列范围检测的API。它们通常分为两类:
直接返回Collider[]数组的方法(有GC开销): 例如 Physics.OverlapBox,每次调用会创建一个新的数组。
**非分配式方法(NonAlloc,无GC开销):**例如 Physics.OverlapBoxNonAlloc,需要传入一个预先分配好的Collider[]数组来存储结果,推荐在频繁调用时使用。
检测的层级相关事项
LayerMask.NameToLayer("Layer1")
- 这个函数返回的是 图层(Layer)的索引编号(整数)。
如果该图层索引是5,相当于把1左移5位来表示第5层

盒状范围检测
检测该范围内的对应层级的碰撞物体

cs
//范围检测API
//1.盒状范围检测
//参数一:立方体中心点
//参数二:立方体三边大小
//参数三:立方体角度
//参数四:检测指定层级(不填检测所有层)
//参数五:是否忽略触发器 UseGlobal-使用全局设置 Collide-检测触发器 Ignore-忽略触发器 不填使用UseGlobal
//返回值:在该范围内的触发器(得到了对象触发器就可以得到对象的所有信息)
print(LayerMask.NameToLayer("UI"));
Collider[] colliders = Physics.OverlapBox(
Vector3.zero,
Vector3.one,
Quaternion.AngleAxis(45, Vector3.up),
1 << LayerMask.NameToLayer("UI") |
1 << LayerMask.NameToLayer("Default"),
QueryTriggerInteraction.UseGlobal);
for (int i = 0; i < colliders.Length; i++)
{
print(colliders[i].gameObject);
}
球形范围检测
cs
//2.球形范围检测
//参数一:中心点
//参数二:球半径
//参数三:检测指定层级(不填检测所有层)
//参数四:是否忽略触发器 UseGlobal-使用全局设置 Collide-检测触发器 Ignore-忽略触发器 不填使用UseGlobal
//返回值:在该范围内的触发器(得到了对象触发器就可以得到对象的所有信息)
Physics.OverlapSphere(Vector3.zero, 5, 1 << LayerMask.NameToLayer("Default"));
胶囊体范围检测
cs
//3.胶囊范围检测
//参数一:半圆一中心点
//参数二:半圆二中心点
//参数三:半圆半径
//参数四:检测指定层级(不填检测所有层)
//参数五:是否忽略触发器 UseGlobal-使用全局设置 Collide-检测触发器 Ignore-忽略触发器 不填使用UseGlobal
//返回值:在该范围内的触发器(得到了对象触发器就可以得到对象的所有信息)
Physics.OverlapCapsule(Vector3.zero, Vector3.up, 1,
1 << LayerMask.NameToLayer("UI"),
QueryTriggerInteraction.UseGlobal);
射线检测
射线检测(Raycasting)是游戏开发中一种常用的碰撞检测技术,核心原理是发射一条虚拟的"射线"(有起点和方向的无限长直线),检测这条射线是否与场景中的碰撞体(Collider)相交,并获取相交的详细信息。
射线生成
cs
#region 知识点二 射线对象
//1.3D世界中的射线
//假设有一条起点为坐标(1,0,0),方向为世界坐标z轴正方向的射线
//注意:
//理解参数含义
//参数一:起点
//参数二:方向(一定记住 不是两点决定射线方向,第二个参数 直接就代表方向向量)
//目前只是申明了一个射线对象 对于我们来说 没有任何的用处
Ray r = new Ray(Vector3.right, Vector3.forward);
//Ray中的参数
print(r.origin); //起点
print(r.direction); //方向
//2.摄像机发射出的射线
//得到一条从屏幕位置作为起点
//摄像机视口方向为 方向的射线
Ray r2 = Camera.main.ScreenPointToRay(Input.mousePosition);
【屏幕发出的射线】
根据玩家在屏幕上点击的位置,发射一条射线
单独的射线,对于我们来说没有意义,它一定是 和别的模块配合使用的
射线检测1------是否碰撞到对象
只能检测到是否有碰撞对象
cs
//进行射线检测 如果碰撞到对象 返回true
//参数一:射线
//参数二:检测的最大距离 超出这个距离不检测
//参数三:检测指定层级(不填检测所有层)
//参数四:是否忽略触发器 UseGlobal-使用全局设置 Collide-检测触发器 Ignore-忽略触发器 不填使用UseGlobal
//返回值:bool 当碰撞到对象时 返回 true 没有 返回false
if (Physics.Raycast(r3, 1000, 1 << LayerMask.NameToLayer("Monster"), QueryTriggerInteraction.UseGlobal))
{
print("碰撞到了对象");
}
射线检测2------交互单个物体信息
cs
//2.获取交互的单个物体信息
//物体信息类 RaycastHit
RaycastHit hitInfo;
//参数一:射线
//参数二:RaycastHit是结构体 是值类型 Unity会通过out 关键在 在函数内部处理后 得到碰撞数据后返回到该参数中
//参数三:距离
//参数四:检测指定层级(不填检测所有层)
//参数五:是否忽略触发器 UseGlobal-使用全局设置 Collide-检测触发器 Ignore-忽略触发器 不填使用UseGlobal
if (Physics.Raycast(r3, out hitInfo, 1000, 1 << LayerMask.NameToLayer("Monster"), QueryTriggerInteraction.UseGlobal))
{
print("碰撞到了物体 得到了信息");
//碰撞器信息
print("碰撞到物体的名字" + hitInfo.collider.gameObject.name);
//碰撞到的点
print(hitInfo.point);
//法线信息
print(hitInfo.normal);
//得到碰撞到对象的位置
print(hitInfo.transform.position);
//得到碰撞到对象 离自己的距离
print(hitInfo.distance);
}
//RaycastHit 该类 对于我们的意义
//它不仅可以得到我们碰撞到的对象信息
//还可以得到一些 碰撞的点 距离 法线等等的信息
该函数还有一种重载:
cs
//还有一种重载 不用传入 射线 直接传入起点 和 方向 也可以用于判断
if (Physics.Raycast(Vector3.zero, Vector3.forward, out hitInfo, 1000, 1 << LayerMask.NameToLayer("Monster"), QueryTriggerInteraction.UseGlobal))
{
// ...
}
射线检测3------检测多个物体

cs
//可以得到碰撞到的多个对象
//如果没有 就是容量为0的数组
//参数一:射线
//参数二:距离
//参数三:检测指定层级(不填检测所有层)
//参数四:是否忽略触发器 UseGlobal-使用全局设置 Collide-检测触发器 Ignore-忽略触发器 不填使用UseGlobal
RaycastHit[] hits = Physics.RaycastAll(r3, 1000, 1 << LayerMask.NameToLayer("Monster"), QueryTriggerInteraction.UseGlobal);
for (int i = 0; i < hits.Length; i++)
{
print("碰到的所有物体 名字分别是" + hits[i].collider.gameObject.name);
}
//还有一种重载 不用传入 射线 直接传入起点 和 方向 也可以用于判断
//之前的参数一射线 通过两个点传入
hits = Physics.RaycastAll(Vector3.zero, Vector3.forward, 1000, 1 << LayerMask.NameToLayer("Monster"), QueryTriggerInteraction.UseGlobal);
//还有一种函数 返回的碰撞的数量 通过out得到数据
if (Physics.RaycastNonAlloc(r3, hits, 1000, 1 << LayerMask.NameToLayer("Monster"), QueryTriggerInteraction.UseGlobal))
{
// ...
}












