LOADING

加载过慢请开启缓存 浏览器默认开启

whisper3zzzの窝

LuaDepth

Depth 2025/4/25

协程

线程:抢占式多任务机制,是一个相对独立的、可调度的执行单元,是系统独立调度和分配CPU的基本单位。它由操作系统来决定执行哪个任务,在运行过程中需要调度,休眠挂起,上下文切换等系统开销,而且最关键还要使用同步机制保证多线程的运行结果正确

协程:协作式多任务机制,协程之间通过函数调用来完成一个既定的任务。它由程序自己决定执行哪个任务,只涉及到控制权的交换(通过resume-yield),同一时刻只有一个协程在运行,而且无法外部停止。通俗来说,协程就是可以用同步的方式,写出异步的代码

协程(Coroutine)拥有4种状态:

  • 运行(running)如果在协程的函数中调用status,传入协程自身的句柄,那么执行到这里的时候才会返回运行状态
  • 挂起(suspended)调用了yeild或还没开始运行,那么就是挂起状态
  • 正常(normal)如果协程A重启协程B时,协程A处于的状态为正常状态
  • 停止(dead)如果一个协程发生错误结束,或正常终止。那么就处于死亡状态(不可以再重启)

Lua的协程是一种非对称式协程,又或叫半协程,因为它提供了两种传递程序控制权的操作:1. 重启调用协程,通过coroutine.resume实现;2. 挂起协程并将程序控制权返回给协程的调用者,即通过coroutine.yield实现。对称式协程,只有一种传递程序控制权的操作,即将控制权直接传递给指定的协程

协程(Coroutine)具有两个非常重要的特性:1. 私有数据在协程间断式运行期间一直有效;2. 协程每次yield后让出控制权,下次被resume后从停止点开始继续执行

闭包

把函数原型换成类,把闭包换成对象。一切就都可以解释了,无论什么时候,把一个函数原型赋值给一个变量时,Lua都会将这个函数原型实例化为一个闭包对象,无论这个闭包对象是否真的用到了所谓的上值表。当在Lua中调用函数或者传递函数对象时,都是在传递闭包对象,而非传递函数原型。
所以说白了,闭包就是函数原型的一个实例
所以它为什么叫闭包呢?就是因为当实例化一个函数原型为一个闭包时,这个函数原型已经捕获到了自己需要所有的上值对象,此后不会再进行任何其他捕获行为。它形成了一个自洽的体系,有自己内部的常量表,上值表,变量表。
这就是Lua的闭包机制,一种用到了OOP思想的机制。

  1. Upvalues是在函数闭包生成的时候(运行到function时)绑定的
  2. Upvalues在闭包还没关闭前(即函数返回前),是对栈的引用,这样做的目的是可以在函数里修改对应的值从而修改Upvalues的值
  3. 闭包关闭后(即函数退出后),Upvalues不再是指针,而是值

userdata

userdata本身只是一个指针(light userdata),或一块受Lua管理的内存块(full userdata),它没有任何预定义行为,在Lua看来它就是一个值,你需要提供配套的C函数去操作它。一般可以用一个对应的元表来判断userdata的类型,以及用一些方法操作userdata的属性

Json

1. 高效的内存管理

  • 预分配内存策略: 使用 membuffer 结构管理缓冲区,提前分配足够空间 (membuffer_ensure_space),避免频繁重分配
  • 内存复用: 尽可能复用内存缓冲区,减少内存分配次数
  • 零拷贝优化: 直接在缓冲区构建结果,避免中间字符串拷贝

2. 优化的解析技术

  • 高效分派: 使用 switch-case 和查表法代替条件分支
  • 内联优化: 关键函数使用 inline,减少函数调用开销
  • 分支预测: 使用 likelyunlikely 宏优化 CPU 分支预测
  • 快速路径: 常见情况直接处理,避免通用路径的开销

3. 数值处理专项优化

  • 整数/浮点分离处理: 对整数和浮点数采用不同的处理路径
  • 延迟转换: 尽量保持整数形式,只在必要时转为浮点数
  • 定界检测: 通过 MAXBY10MAXLASTD 常量快速判断整数边界
  • 幂运算优化: 使用预计算的 powersOf10 表加速指数部分计算

4. 字符串处理优化

  • 转义表: 使用预构建的查表法处理字符转义 (char2escapeescape2char)
  • 直接缓冲区操作: 使用 _unsafe 系列函数在保证安全的情况下跳过边界检查
  • UTF8/16 高效编码: 针对 Unicode 编码进行了特殊优化

5. 智能表处理

  • 数组/对象智能判断: 根据数据特征自动识别是数组还是对象
  • 空表优化: 对空表进行特殊处理,可配置为数组或对象
  • 表遍历优化: 减少表遍历中的 Lua API 调用次数

6. 底层 Lua API 优化

  • 栈操作优化: 最小化 Lua 栈操作,减少入栈/出栈开销
  • 直接数据访问: 直接与 Lua 数据类型交互,避免不必要的类型转换
  • 预分配栈空间: 使用 luaL_checkstack 提前确保足够的栈空间

7. 其他技术优化

  • 基于事件回调: 使用事件回调机制分离解析和构建逻辑
  • 深度控制: 防止过深嵌套导致的栈溢出
  • 条件编译: 根据需要开启格式化或注释支持

这些优化使得 LuaExtend JSON 实现在各种场景下都能保持高性能,尤其在处理大型 JSON 数据或高频解析/序列化场景中优势更加明显。

List

如下面这样的代码:

local t = {}
local tinsert = table.insert
for i = 1, 1000 do
    tinsert(t, i)
end

上面的代码还可以再优化成这样:

local t = {}
for i = 1, 1000 do
    t[#t+1] = i
end

这减少了一次C API的调用,性能会快一点点。

看起来#t并不是立即返回数组长度,而是做了更耗时的操作,翻开Lua的源码,相关的函数是:

lua_Unsigned luaH_getn (Table *t) {
  unsigned int j = t->sizearray;
  if (j > 0 && ttisnil(&t->array[j - 1])) {
    /* there is a boundary in the array part: (binary) search for it */
    unsigned int i = 0;
    while (j - i > 1) {
      unsigned int m = (i+j)/2;
      if (ttisnil(&t->array[m - 1])) j = m;
      else i = m;
    }
    return i;
  }
  /* else must find a boundary in hash part */
  else if (isdummy(t)) {  /* hash part is empty? */
    return j;  /* that is easy... */
  }
  else {
       return unbound_search(t, j);
  }
}

问题就在if (j > 0 && ttisnil(&t->array[j - 1]))这一段,如果数组部分最后一个元素是nil,那么就用二分查找找到最后不是nil的元素,返回这个索引。

我们不断地往Table增加数据时,数组部分会动态地按2的幂扩充,比如现在数组长度是2,再加一个值,数组就动态扩充成4,后面再扩充成8,16…,多出来的槽位填充为nil。所以除非数组刚好满了,否则#t都会用二分查找来确定长度。时间复杂度一样子从O(1)变成了O(logN)。这样分析下来,甚至比Python慢也是可以理解了。

这个问题的根源,我认为是Lua哈希表和数组融合在一起,很多对数组的操作,也不得不考虑哈希部分;而且在rehash的时候,数据经常在数组和哈希表转移。这种融合在我看来一点好处都没有:增加了Table代码的复杂度,且很难针对数组的操作作出更优化的写法。比如没有办法提供数组的精确长度,只能通过二分查找找到最右边的非空元素。

Lua中,把Table当成数组操作,是会比其他语言承担更多的性能开销的。

  • 用户数据设计:使用 lua_newuserdata 创建 C 结构体,配合元表实现对象化

  • 引用系统:通过自定义引用表管理 Lua 值的存储,避免 GC 问题

  • 元表机制:每个模块定义专有元表和元方法,支持面向对象语法

  • 类型检查与转换:使用 luaL_check* 系列函数确保类型安全

  • 动态扩容:如列表/队列等动态数据结构实现自动扩容算法

  • 内存复用:尽可能复用内存,减少分配/释放次数

  • 安全释放:使用 __gc 元方法确保资源自动释放

  • 定制内存分配器:通过 _malloc/_realloc/_free 封装内存操作

序列化

1. 高效的序列化方案

  • 优化的二进制格式设计:使用 type+cookie 8位字节高效编码类型信息
  • 自适应数值编码:根据数值范围自动选择 int8/16/32/64,节省空间
  • 表结构优化:分离数组部分和哈希部分,减少序列化体积
  • 递归深度控制:通过 MAX_DEPTH 参数防止栈溢出
  • 可讨论点:与 JSON/MessagePack 相比的性能优势

2. 内存管理策略

  • 内存分配最小化:使用固定初始缓冲区(INIT_BUFF_SIZE)减少小对象分配
  • 智能扩容算法:缓冲区动态扩容时采用倍增策略,平衡性能与内存使用
  • 内存复用机制:尽可能重用已分配内存,避免频繁分配/释放
  • 可讨论点:在内存受限环境中的优化思路

3. 跨平台兼容性

  • 字节序处理:通过 nativeendian 智能检测并处理大小端差异
  • 整数跨平台表示:处理不同平台整数位宽差异
  • 可讨论点:如何确保数据在异构系统间安全传输

4. C/Lua 接口设计

  • 面向对象包装:使用元表机制提供自然的 OOP 接口
  • 引用系统:通过引用表机制安全管理 Lua 对象生命周期
  • 统一命名空间:所有模块使用 LuaExtendLib 前缀,避免命名冲突
  • 可讨论点:如何平衡接口易用性与底层性能

5. 性能优化技术

  • 内联关键函数:使用 inline 避免高频调用开销
  • 自定义内存分配:通过 _malloc/_realloc/_free 封装,便于优化和跟踪
  • 底层优化:如 JSON 解析器中的字符级优化和查表法处理
  • 算法优化:如快速排序、二分查找等高效算法实现
  • 可讨论点:性能瓶颈分析方法和优化策略
阅读全文

从IEnumerator到UnityCoroutine——Unity协程的背后原理

2024/11/29

在Unity中使用协程

在最开始做游戏的时候,我们经常会遇到这种需求:玩家死亡之后玩家应该逐渐消失而非瞬间消失。玩家死亡很简单,我们按照最开始初学者的思路,死亡直接调用Destroy(gameObject); 就行了,但是当gameobject被销毁之后,我们已经无法去更改其挂载的组件。所以实际上我们需要先执行消失的逻辑。

下面是消失一个错误的实现

  void Fade() 
{
    float alpha = 1.0f;
    while(alpha > 0)
    {
        alpha -= Time.deltaTime;
        Color c = renderer.material.color;
        c.a = alpha;
        renderer.material.color = c;
    }
}

上面代码的思路很简单,让所渲染对象的Alpha通道从1开始不断递减直至0。但是,函数被调用之后将运行到完成状态然后返回,函数内的一切逻辑都会在同一帧内完成。物体会瞬间消失,而不会淡出。

将函数改写为协程就可以修正这个错误。

IEnumerator Fade()
{
    float alpha = 1.0f;
    while(alpha > 0)
    {
        alpha -= Time.deltaTime;
        Color c = renderer.material.color;
        c.a = alpha;
        renderer.material.color = c;
        yield return null;//协程会在这里被暂停,直到下一帧被唤醒。
    }
}
//Fade不能直接调用,需要使用StartCoroutine(方法名(参数列表))的形式进行调用。
StartCoroutine(Fade());

这种以IEnumerator为返回值的函数,在C#里面被称之为迭代器函数,在Unity里面也可以被称为协程函数。 当协程函数运行到yield return null语句时,协程会被暂停,Unity继续执行其它逻辑,并在下一帧唤醒协程。

Unity官方对于协程的定义是这样的:

A coroutine is like a function that has the ability to pause execution and return control to Unity but then to continue where it left off on the following frame.
By default, a coroutine is resumed on the frame after it yields but it is also possible to introduce a time delay using WaitForSeconds

协程是一种特殊的函数,它可以主动的请求暂停自身并提交一个唤醒条件,Unity会在唤醒条件满足的时候去重新唤醒协程。

生命周期图Base64

MonoBehaviour.StartCoroutine()方法可以开启一个协程,这个协程会挂在该MonoBehaviour下。

MonoBehaviour生命周期的UpdateLateUpdate之间,会检查这个MonoBehaviour下挂载的所有协程,并唤醒其中满足唤醒条件的协程。

要想使用协程,只需要以IEnumerator为返回值,并且在函数体里面用yield return语句来暂停协程并提交一个唤醒条件。然后使用StartCoroutine来开启协程。

IEnumerator CoroutineA(int arg1, string arg2)
{
    Debug.Log("协程A被开启");
    yield return null;
    Debug.Log("暂停了一帧");
    yield return new WaitForSeconds(1.0f);
    Debug.Log("暂停了一秒");
    yield return StartCoroutine(CoroutineB(arg1, arg2));
    Debug.Log("CoroutineB运行结束后协程A才被唤醒");
    yield return new WaitForEndOfFrame();
    Debug.Log("在这一帧的最后,协程被唤醒");
    Debug.Log("协程A运行结束");
}
IEnumerator CoroutineB(int arg1, string arg2)
{
    Debug.Log($"协程B被开启了,可以传参数,arg1={arg1}, arg2={arg2}");
    yield return new WaitForSeconds(3.0f);
    Debug.Log("协程B运行结束");
}

注意:

  1. 协程是挂在MonoBehaviour上的,必须要通过一个MonoBehaviour才能开启协程。
  2. MonoBehaviourDisable的时候协程会继续执行,只有MonoBehaviour被销毁的时候协程才会被销毁。
  3. 本质上协程还是运行在主线程上的,协程更类似于Update()方法,Unity会每一帧去检测协程需不需要被唤醒。一旦你在协程中执行了一个耗时操作,很可能会堵塞主线程。

Unity协程的底层原理

协程分为两部分,协程与协程调度器:协程仅仅是一个能够中间暂停返回的函数,而协程调度是在MonoBehaviour的生命周期中实现的。 准确的说,Unity只实现了协程调度部分,而协程本身其实就是用了C#原生的”迭代器方法“。

IEnumerator是什么

public interface IEnumerator
{
    object Current { get; }//获取集合当前迭代位置的元素
    bool MoveNext();//将当前迭代位置推进到下一个位置,如果成功推进到下一个位置则返回true,否则已经推进到集合的末尾返回false
    void Reset();//将当前迭代位置设置为初始位置(该位置位于集合中第一个元素之前,所以当调用Reset方法后,再调用MoveNext方法,Current值则为集合的第一个元素)
}

枚举器接口提供了一种通用的遍历集合的方法,任意一个集合只要实现自己的IEnumerator,它的使用者就可以通过IEnumerator迭代集合中的元素,而不用针对不同的集合采用不同的迭代方式。

foreach只是C#提供的语法糖,本质上也是采用IEnumerator来遍历集合的。在编译时编译器会将的foreach循环进行下面的代码转换:

foreach (var item in collection)
{
   Console.WriteLine(item.ToString());
}
//转换后的代码
{
    var enumerator = collection.GetEnumerator();
    try
    {
        while (enumerator.MoveNext())  // 判断是否成功推进到下一个元素(可理解为集合中是否还有可供迭代的元素)
        {
            var item = enumerator.Current;
            Console.WriteLine(item.ToString());
        }
    } finally
    {
        // dispose of enumerator.
    }
}

yield

yield是C#的关键字,是快速定义迭代器的语法糖。只要是yield出现在其中的方法就会被编译器自动编译成一个迭代器,对于这样的函数可以称之为迭代器函数。迭代器函数的返回值就是自动生成的迭代器类的一个对象。

这个类它会记录当前迭代的状态,包括本地变量的值与迭代器在集合中的位置;它会把yield return x翻译成“将Current设为x,记录当前的状态,并暂停执行”这一系列操作。

//eg.Form Microsoft
var numbers = ProduceEvenNumbers(5);
Console.WriteLine("Caller: about to iterate.");
foreach (int i in numbers)
{
    Console.WriteLine($"Caller: {i}");
}

IEnumerable<int> ProduceEvenNumbers(int upto)
{
    Console.WriteLine("Iterator: start.");
    for (int i = 0; i <= upto; i += 2)
    {
        Console.WriteLine($"Iterator: about to yield {i}");
        yield return i;
        Console.WriteLine($"Iterator: yielded {i}");
    }
    Console.WriteLine("Iterator: end.");
}
// Output:
// Caller: about to iterate.
// Iterator: start.
// Iterator: about to yield 0
// Caller: 0
// Iterator: yielded 0
// Iterator: about to yield 2
// Caller: 2
// Iterator: yielded 2
// Iterator: about to yield 4
// Caller: 4
// Iterator: yielded 4
// Iterator: end.

Unity中的协程利用了迭代器的机制实现了**“执行一系列操作”而非“遍历一个集合”的功能。关键点在于:在这里一个协程中的“集合”**就是所有yield return的返回值,比如{null, new WaitForSeconds(5)}就是下方协程的集合。当我们在遍历这个集合时,所有真正有用的语句都会一并被执行。

IEnumerator MyCoroutine()
{
    //真正有用的语句
    yield return null;
    //真正有用的语句
    yield return new WaitForSeconds(5);
}

yield return语句可以返回一个值,表示迭代得到的当前元素,yield break语句可以用来终止迭代,表示当前没有可被迭代的元素了。

以能“停住”的地方为分界线,编译器会为不同分区的语句按照功能逻辑生成一个个对应的代码块。yield语句就是这条分界线,想要代码“停住”,就不执行后面语句对应的代码块,想要代码恢复,就接着执行后面语句对应的代码块。而调度上下文的保存,是通过将需要保存的变量都定义成成员变量来实现的。

协程本体:C#的迭代器函数

C#中的迭代器方法其实就是一个协程,你可以使用yield来暂停,使用MoveNext()来继续执行。 当一个方法的返回值写成了IEnumerator类型,他就会自动被解析成迭代器方法*(后文直接称之为协程)*,你调用此方法的时候不会真的运行,而是会返回一个迭代器,需要用MoveNext()来真正的运行。

static void Main(string[] args)
{
    IEnumerator it = Test();//仅仅返回一个指向Test的迭代器,不会真的执行。
    Console.ReadKey();
    it.MoveNext();//执行Test直到遇到第一个yield
    System.Console.WriteLine(it.Current);//输出1
    Console.ReadKey();
    it.MoveNext();//执行Test直到遇到第二个yield
    System.Console.WriteLine(it.Current);//输出2
    Console.ReadKey();
    it.MoveNext();//执行Test直到遇到第三个yield
    System.Console.WriteLine(it.Current);//输出test3
    Console.ReadKey();
}
static IEnumerator Test()
{
    System.Console.WriteLine("第一次执行");
    yield return 1;
    System.Console.WriteLine("第二次执行");
    yield return 2;
    System.Console.WriteLine("第三次执行");
    yield return "test3";
}
  • 执行Test()不会运行函数,会直接返回一个IEnumerator
  • 调用IEnumeratorMoveNext()成员,会执行协程直到遇到第一个yield return或者执行完毕。
  • 调用IEnumeratorCurrent成员,可以获得yield return后面接的返回值,该返回值可以是任何类型的对象

那么我们都知道yield return new WaitForSeconds(5)会停5秒,yield return null会停一帧,那只有写yield return null才会延迟到下一帧执行吗?

下面这段伪代码展示了StartCoroutine()的内部原理:

void MyStartCoroutine()
{
 Update//每一帧都执行
 {
        MoveNext();
        if(Current is WaitForSeconds)
        {
         Stop for x seconds;//具体逻辑可能就是我们写的一个简单的计时器,timer=time+=Time.dealtTime这样
        }
        else if(Current is WaitFixedUpdate)
        {
         Stop for fixedupdate;
        }
        else
         //···
 }
}

所以我们如果在yield return后面写一个new GameObject();编译器也不会报错,这些奇奇怪怪的返回类型根本在StartCoroutine()里面没有对应的情况,所以它不会执行特定的操作,依然默认间隔一帧,然后执行MoveNext(),甚至写new GameObject();,确实会把Current的值设置为新实例化一个GameObject,场景里面真的会出现新的GameObject。所以写yield return null只是为了不发生奇奇怪怪的事,并非它是特定的延迟一帧。

  1. IEnumerator中的yield return可以返回任意类型的对象,事实上它还有泛型版本IEnumerator<T>,泛型类型的迭代器中只能返回T类型的对象。Unity原生协程使用普通版本的IEnumerator

  2. 函数调用的本质是压栈,协程的唤醒也一样,调用IEnumerator.MoveNext()时会把协程方法体压入当前的函数调用栈中执行,运行到yield return后再弹栈。

协程调度:MonoBehaviour生命周期中实现

  1. C#层调用StartCoroutine方法,将IEnumerator对象(或者是用于创建IEnumerator对象的方法名字符串)传入C++层。
  2. 通过mono的反射功能,找到IEnumerator上的moveNextcurrent两个方法,然后创建出一个对应的Coroutine对象,把两个方法传递给这个Coroutine对象。【创建好之后这个Coroutine对象会保存在MonoBehaviour一个成员变量List中,这样使得MonoBehaviour具备StopCoroutine功能,StopCoroutine能够找到对应Coroutine并停止。】
  3. 调用这个Coroutine对象的Run方法
  4. Coroutine.Run中,然后调用一次MoveNext。①如果MoveNext返回false,表示Coroutine执行结束,进入清理流程;②如果返回true,表示Coroutine执行到了一句yield return处,这时就需要调用invocation(m_Current).Invoke取出yield return返回的对象monoWait,再根据monoWait的具体类型(nullWaitForSecondsWaitForFixedUpdate等),将Coroutine对象保存到DelayedCallManagercallback列表m_CallObjects中。
  5. 至此,Coroutine在当前帧的执行即结束。
  6. 之后游戏运行过程中,游戏主循环的PlayerLoop方法会在每帧的不同时间点以不同的modeMask调用DelayedCallManager.Update方法,Update方法中会遍历callback列表中的Coroutine对象,如果某个Coroutine对象的monoWait的执行条件满足,则将其从callback列表中取出,执行这个Coroutine对象的Run方法,回到之前的执行流程中。

可以模仿Unity自己实现一个简单的协程调度:

void YieldWaitForSeconds()
{
    //定义一个移除列表,当一个协程执行完毕或者唤醒条件的类型改变时,应该从当前协程列表中移除。
    List<WaitForSeconds> removeList = new List<WaitForSeconds>();
    foreach(IEnumerator w in m_WaitForSeconds) //遍历所有唤醒条件为WaitForSeconds的协程
    {
        if(Time.time >= w.beginTime() + w.interval) //检查是否满足了唤醒条件
        {
            //尝试唤醒协程,如果唤醒失败,则证明协程已经执行完毕
            if(it.MoveNext();)
            {
                //应用新的唤醒条件
                if(!(it.Current is WaitForSeconds))
                {
                    removeList.Add(it);
                       //在这里写一些代码,将it移到其它的协程队列里面去
                }
            }
            else 
            {
                removeList.Add(it);
            }
        }
    }
    m_WaitForSeconds.RemoveAll(removeList);
}

Unity协程只是个伪异步,内部的死循环依旧会导致应用卡死

Unity之外的协程

一个进程可以有多个线程,一个线程可以有多个协程

进程其实就是程序运行的实例:程序本身只是存储在外存上的冷冰冰的二进制流,计算机将这些二进制流读进内存并解析成指令和数据然后执行,程序便成为了进程。

每一个进程都独立拥有自己的指令和数据,所以称为资源分配的基本单位。其中数据又分布在内存的不同区域,一个运行中的进程所占有的内存大体可以分为四个区域:栈区、堆区、数据区、代码区。其中代码区存储指令,另外三个区存储数据。

线程是处理器调度和执行的基本单位,一个线程往往和一个函数调用栈绑定,一个进程有多个线程,每个线程拥有自己的函数调用栈,同时共用进程的堆区,数据区,代码区。操作系统会不停地在不同线程之间切换来营造出一个并行的效果,这个策略称为时间片轮转法。线程是并行的,多个线程可以同时运行,而多线程共享资源,所以访问全局变量的时候要加锁。

一切用户自己实现的,类似于线程的轮子,都可以称之为是协程。

协程有什么样的行为,完全由实现协程的程序员来决定。


参考与引用:

Unity协程的原理与应用

【迭代器模式】深入理解协程

阅读全文

xLua热更新

学习 2024/8/5

xLua的新功能-HotFix

优势:

开发时使用C#,打好hotfix标签,出现问题时再使用lua修复,大版本更新时将lua部分更新回C#。

劣势:

  • 热修复复杂脚本的难度:
    • 储备lua开发人员。
    • 制定C#编写规范。
    • 考虑C#和lua之间“翻译”的语言特性问题。
  • 更新频率:
    • 不适合高频维护项目。
阅读全文

如何定位Unity编辑器崩溃问题

2024/6/15

C:\Users\{username}\AppData\Local\Unity\Editor文件夹中,Editor.log文件是当前编辑器的控制台输出,Editor-prev.log是上一次编辑器的控制台输出,查看Editor-prev.log即可找到导致编辑器崩溃的报错。

阅读全文

Unity ML_Agents_Study

学习 2024/5/20

下载ML-Agents

到官方Github库下载Release21版本(后续教程均基于此版本),下载完成后解压到本地目录。

环境配置

1.安装anaconda,建议安装完成之后更新到最新版本,否则即使你的anaconda不安装在C盘,创建的环境也会在C盘,且无法修改,在安装过程中务必勾选将anaconda添加到环境变量,安装完成之后,可以在cmd中运行conda --version,如果成功输出版本号,则添加成功;如果发生报错,请手动配置环境变量,在控制面板\系统和安全\系统\高级系统设置\环境变量\用户变量\PATH 中添加 anaconda的安装目录的Scripts文件夹。

2.创建虚拟环境,这里有两种方法,一种是使用图形化界面anaconda Navigator ,另一种是使用anaconda Prompt。

使用图形化界面anaconda Navigator

启动anaconda Navigator

如图所示创建一个环境,python版本选择一个靠近3.10.12的版本,创建好之后,请切换python版本到3.10.12,参考下图

使用anaconda Prompt

启动anaconda Prompt,输入conda create -n UniMLA python=3.10.12

这样就创建了一个名为UniMLA的虚拟环境

3.Win+R,输入cmd,使用cd命令进入ML-Agents解压后的文件夹,比如

cd /d G:\ml-agents-release_21(使用/d是为了切换盘符,如果你的ML-Agents文件夹在系统盘,请忽略/d)

然后输入conda -activate UniMLA,启动你创建的虚拟环境,成功启动后,cmd新一行会变成(UniMLA) G:\ml-agents-release_21>

4.打开文件夹ml-agents-envs,找到文件setup.py,使用诸如VScode之类的软件打开,找到install_requires,把numpy后面的版本号改成1.23.5,保存并退出;打开文件夹ml-agents,找到文件setup.py,同样找到 install_requires,把numpy的那一行改成"numpy>=1.23.5,<1.24.0"

5.回到启用虚拟环境的cmd窗口,依次输入

pip3 install torch -f https://download.pytorch.org/whl/torch_stable.html
pip3 install -e ./ml-agents-envs
pip3 install -e ./ml-agents

等待安装完成,如果有报错,请按照提示处理,比如更新pip版本。

从第三步开始不使用cmd,使用anaconda Prompt过程一致

之后创建一个Unity项目(Release21版本推荐使用2022.3及以后版本),在packmanager里面选择从磁盘导入包找到\ml-agents-release_21\com.unity.ml-agents\package.json导入即可。

阅读全文
1 2 3 ... 4
avatar
whisper3zzz

Description
Unity&游戏开发&Computer Graphics