延迟函数/协同程序

延迟函数

  • 定义:延迟函数是MonoBehaviour基类提供的可以延时执行的函数
  • 特点:
    • 可以自定义要执行的函数和延迟时间
    • 继承MonoBehaviour后即可使用
    • 通过反射机制根据函数名查找并执行对应方法

延迟函数的使用

cs 复制代码
//Invoke()
//参数一:函数名 字符串
//参数二:延迟时间 秒为单位

Invoke("DelayFunc", 1);

private void DelayFunc()
{
print("延时执行函数");
}

//注意:
//延时函数第一个参数传入的是函数名字符串
//延时函数无法传入参数,需包裹一层无参函数
//函数名必须是该脚本上申明的函数

如何包装有参函数

cs 复制代码
Invoke("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函数中会一直启用该函数就会导致混乱。

计时器

cs 复制代码
void Start()
{  
    //第一次在0s瞬间执行该函数,后面每一次过一秒执行DelayFun
    InvokeRepeating("DelayFun", 0, 1);
    
    DelayFun2();
}

private void DelayFun()
{
    print(time + "秒");
    ++time;
}

private void DelayFun2()
{
    print(time + "秒");
    ++time;
    Invoke("DelayFun2", 1);
}

输出结果如下:

延迟摧毁

cs 复制代码
void 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"异常
  • 注意事项:
    • 必须手动关闭创建的多线程(在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.LoadAssetBundle.LoadAssetAddressables.LoadAsset
生成/实例化 (Instantiate) 在内存中根据资源数据创建可运行的游戏对象 GameObject.InstantiateObject.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 ──加载中──┬──??──→ 场景变了
                              ↑
                        你不知道具体是哪一帧完成的

无回调版本就会出现如下情况:

cs 复制代码
void 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 的自动进度值,而是根据游戏自身的加载流程,手动设计进度条的增长逻辑

  1. 资源可能早已加载完成

    • 比如通过 Resources.LoadAsync()Addressables 提前加载了怪物、模型等。
    • 它们已经在内存中,只是还没有被实例化(Instantiate)或放置到场景中
  2. 进度条反映的是"游戏就绪度",而非"文件读取进度"

    • 你关心的不是"硬盘读了多少字节",而是:
      • 场景结构是否建好?
      • 怪物是否生成完毕?
      • 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;

练习题

范围检测

碰撞相关的概念重温

碰撞产生的必要条件

  1. 至少有一个物体必须挂载刚体(Rigidbody)组件

    刚体是 Unity 物理系统的核心,只有带有刚体的物体才能参与物理模拟(如受重力影响、产生碰撞响应等)。

  2. 两个物体都必须拥有碰撞器(Collider)组件

    碰撞器定义了物体的物理形状(如 Box Collider、Sphere Collider 等),用于检测与其他物体的接触。

碰撞与触发的区别

  • 碰撞(Collision)

    会产生真实的物理效果,例如反弹、停止运动、施加力等。适用于需要物理交互的场景,如角色撞击墙壁、车辆相撞等。

  • 触发(Trigger)

    不会产生物理效果,但可以通过脚本监听进入、停留或离开事件。只需将任意一方的碰撞器勾选 Is Trigger 属性即可启用。常用于逻辑判断,如拾取道具、区域检测、自动开门等。

而范围检测的基本概念:

  • 定义: 检测特定区域内是否存在符合条件物体的方法
  • 特点: 不需要实际物理碰撞即可判断物体位置关系
  • 必备条件 :想要被范围检测到的对象,必须具备碰撞器(Collider)!
  • 注意点
    1. 范围检测相关API是瞬时的,只有当执行该代码时才进行一次范围检测。
    2. 范围检测相关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))
{
    // ...
}
相关推荐
魔力军1 小时前
Rust学习Day5:结构体介绍和使用
开发语言·学习·rust
maplewen.1 小时前
C++ 内存对齐
开发语言·c++
摇滚侠2 小时前
登录认证,验证码实现逻辑
java·intellij-idea
老毛肚2 小时前
java juc 01 进程与线程
java·开发语言
1candobetter2 小时前
JAVA后端开发——反射机制在Spring业务开发中的实际应用
java·开发语言·spring
野犬寒鸦2 小时前
WebSocket协同编辑:高性能Disruptor架构揭秘及项目中的实战应用
java·开发语言·数据库·redis·后端
kyle~2 小时前
ROS2----组件(Components)
开发语言·c++·机器人·ros2
橙露2 小时前
排序算法可视化:用 Java 实现冒泡、快排与归并排序的对比分析
java·python·排序算法
靠沿2 小时前
【优选算法】专题二——滑动窗口
java·数据结构·算法