前言:

最近在摸索用XLua 在 Unity 中进行全Lua 开发。然后就遇到了协程的问题。我想在 Lua 侧开启一个 Unity 的协程,该怎么做呢?

一开始我先去翻 XLua 的文档,我记得之前我看文档是看到过协程介绍的。但是快速翻了一遍后没找到(后来又找到了,本文最后会进行讲解)。

然后就开始了自己的摸索。

什么是协程?

协程 是 协同程序 的简写。有过 Unity 或者 Lua 基础的朋友应该都已经很清楚了,可以跳过这一段。下面我做一下简单解释。

协程 与 多线程很像,它们都有 挂起、运行、死亡 等一系列状态,不同点在于 多线程是独立于主线程的,同一时间可以有多个多线程在运行;而 协程则是都只能存在于主线程中,同一时间最多只能有一个协程在运行。

那么 协程 跟 子程序(或者说 函数/方法) 又有什么区别呢?它们不都是在主线程中被执行吗?

区别就在于,协程可以被挂起,从而实现一种 异步 策略。

例如:

我们要加载一个非常大的文件,如果我们直接在主线程中执行加载文件的方法,那么主线程运行到加载文件时,就会一直去加载这个文件,直到文件被全部加载完毕之后,再去执行下面的代码逻辑。

而如果我们把加载文件的逻辑放入到协程中去执行,那么我们就可以让主线程执行到加载文件时,只加载 0.1s 的时间,然后就把这个协程挂起,去执行下面的程序。然后主线程下次又来到加载文件这个协程时,我们再恢复协程,然后再加载 0.1s 的时间,再去执行下面的程序。

这样,我们就可以在 加载文件 的同时 也 执行着主线程中的所有代码。

简单来讲,使用协程我们就可以做到那个有名的猜想:当一个人快速地交换着执行两个任务,那么从外界来看,他就像是在同时执行着两个任务一样。

Unity 中的协程

注意是Unity中的协程,而不是C#的协程,因为C#原生是没有协程这一东西的,只是Unity封装出了协程。

尽管 协程 的产生 要比 线程早,但是在 线程产生后,线程就淘汰了协程,或者说用 线程 可以实现 协程,因此 C#、Java 等语言就不直接支持协程。但之后又有一些语言拾起了协程,例如 Lua 和 Python

关于 Unity 中的协程,我要说的其实非常简单:使用 StartCoroutine(IEnumerator cor) 就可以开启一个协程,当然该方法是 MonoBehaviour 类的方法,因此只能在 Mono 或者 Mono 子类中进行调用,传入的参数是迭代器类型(IEnumerator)参数

Lua中的协程

关于 Lua 中的协程,笔者前几天才草草看了遍《Lua 程序设计》,下面直接举个栗子来解释吧,没说到的在下面具体操作时会解释:一段关于 Lua 协程的代码

如上图,我们通过 coroutine.create() 创建出了一个协程对象。该方法传入的是一个方法,作为协程对象的方法体

创建出的协程默认是在挂起状态,我们可以使用 coroutine.resume() 来将协程转为运行状态,只需要把协程对象作为参数传进去即可。我们的 stepCor 方法就是将 cor 协程对象转为运行状态。

当我们第一次执行 stepCor 方法是,cor协程转为运行状态,执行其方法体,然后我们就会遇到方法体的第一行:

coroutine.yield(num)

yield 会使协程挂起,并把 yield 传入的参数 输出给 调用 resume 的方法。即,此时 cor 协程会被挂起,并且 coroutine.resume(cor, 10) 会输出 10,只是这里我们没有接收。

既然因为 yield,cor 协程被挂起了,那么我们要恢复执行 cor 就要再调用 stepCor,这次调用 stepCor 时,cor 协程会接着它上次被挂起的地方继续往下执行,即执行 num = num + 1,此时 num = 11,然后因为协程没有被挂起,继续往下执行,执行 coroutine.yield(num),那么 cor 就被挂起,并把 num 返回给 coroutine.resume(cor)

下次调用 stepCor,cor 协程就会执行 print(num),然后执行 coroutine.yield(num) 又将自己挂起,并把 num 返回给 coroutine.resume(cor)

...

这就是 Lua 协程的基本操作。

Unity 协程 与 Lua 协程的互通

这里,默认大家对XLua 有基本的了解,太基础的API就不说了。

首先是把 Lua 脚本 与 Mono 脚本进行关联,这一个在 XLua 的示例中有模板,即XLua 提供的 LuaBehaviour 类。其把 Lua脚本的 self 设置为 Mono 类的对象,来把 Lua 脚本 与 Mono 脚本进行关联。里面的更多细节大家可以去看文档和源码,这里不做赘述。

LuaBehaviour 的 Awak 方法

那么,通过 XLua 来开启一个协程我们就可以直接这样子:self:StartCoroutine(cor) 来开启

注意,使用的是 冒号(:) 调用 而非 点号(.) 调用,因为冒号调用是Lua调用对象方法的书写,而点号调用则常用来调用类的方法

那么关键就在于传入 StartCoroutine 的参数 cor 应该是什么

根据定义 StartCoroutine(IEnumerator cor) 我们需要传入一个 迭代器类型 的参数。一开始我认为凭借 XLua 强大的 C#-Lua 类型互通功能,我只要创建出一个表,该表中有 IEnumerator 所定义的东西,那么直接把这个表对象传入即可。于是开始在 Lua 中实现 IEnumerator:

我们看一下 IEnumerator 的定义:IEnumerator 的定义

首先有一个属性 Current,其表示迭代器当前迭代到的元素

然后是接口方法 MoveNext,其表示迭代,即将 Current 更新为下一个迭代元素。如果迭代器可以迭代,则返回 true,如果 迭代器迭代元素已经全部被迭代完毕了,则返回 false

再然后是接口方法 Reset,它会重置我们的迭代器。

那么在Lua中实现迭代器,我们就可以使用 协程 来进行。在 《Lua 程序设计》中有专门一节讲解使用协程实现迭代器,只是书中的迭代器并不能和 C# 迭代器接口中的定义一一对应起来,那么我们就来让它们对应起来。

首先,我们希望 Lua 侧实现的迭代器是可以创建对象的,那么就先使用最基本的 Lua 面向对象写法:Lua 面向对象构造函数模板

这样,我们 local IEnumerator = require('LuaIEnumerator') 后,就可以通过 IEnumerator:new() 来创建对象了。

至于为什么这样可以构造对象,属于 Lua 基础知识,这里不做赘述。

接下来我们来思考 MoveNext() 的实现:

在 C# 中,我们通常这样定义迭代器:C# 定义迭代器

对于上面的代码所定义的迭代器:

第一次调用 MoveNext() 迭代器的 Current 会变为 yield return 后的 1,返回 true

第二次调用 Current 会变为 2,返回 true

第三次调用 Current 变为 3, 返回 true

第四次调用 Current 变为 4, 返回 true

第五次调用,因为没有 yield return 了,所有迭代元素已经被迭代完了,返回 false

可以发现,这与 Lua 的 协程是类似的。

我们可以在 Lua 迭代器对象创建时,传入一个 迭代器方法体,方法体内用 coroutine.yield(some) 来表示 yield return some

然后在 MoveNext 中,使用 coroutine.resume(cor) 来进行迭代

那么 Reset 方法,其实就与 new 方法差不多了,我们重新根据用户一开始传入的方法体 重新构造一个 协程即可(coroutine.create(func))

具体实现:

这里需要注意 Lua 的 coroutine.resume(cor) 方法,会返回两个值,第一个是布尔值 表示当前协程开启是否成功,如果协程中的代码都执行完了,调用 resume 该值就会为 false,第二个值才是我们 coroutine.yield(some) 返回出来的值,因此我们需要定义两个变量来接收,第二个变量直接用 Current 来接收即可。

还有就是 三个点(...) 是 Lua 中的可选参数,因为我们在定义时无法确定用户传入的方法体 会传入哪些参数。

且可选参数不能直接传入到一个方法的调用中,如果要传入调用方法,则必须把可选参数“解包”,对于不同 Lua 版本,解压方法不同。Lua 5.1 直接使用 unpack 之后版本使用 table.unpack

为什么要用 param = {...} 来把可选参数再封装一层呢?这也是 Lua 的基础知识了。因为用户可能什么参数都不传,此时可选参数为 nil,如果我们用 param = {...} 封装一层,此时 param 仍是一个表,只是表里什么都没有,这会避免 unpack 报错

但这样还存在一些小问题,这里我们的 MoveNext() 返回的是 code,而 code 会在 协程代码都被执行完毕之后,在执行一次协程时 才会为 false,因此用 code 来作为标识 不太准确,并且resume 返回 false 时 是会抛出错误的(尽管如果我们不接收这个错误,该错误就不会显示,且也不影响程序运行)。我们可以定义一个 表 move_end,用该表 作为唯一标识,在协程代码执行到最后时,我们返回一个这个表,那么就可以判定 self.Current == move_end 来判定当前是否迭代完毕。

代码:

还可以进一步简化,我们可以使用 coroutine.warp(func) 来创建 “函数式协程”,这也是 Lua 协程创建更为常用的方法。使用 coroutine.warp 创建协程,其返回值是一个 方法,我们可以直接调用这个方法,来间接调用 coroutine.resume 且无需关心 resume 返回的 code 值

代码:

这样就基本没什么问题了。

大家可能会有这样的疑问:不是说要在 Lua 侧 定义 IEnumerator 吗? 但是 Lua 侧这里的 IEnumerator 比 C# 的还多了 func、cor 这两个东西呀。

但其实,在C#里,IEnumerator 是个接口,我们可以把我们在 Lua 侧定义的 IEnumerator 理解为 继承了 C# IEnumerator 接口的类,这样,多了一些东西也就没什么了。

接下来,展示一下使用方法:

这样就行了吗?答案是否定的。因为上面的实现是基于 “我认为XLua 强大的类型互通功能,可以自动地把我们定义的表转换为 IEnumerator 对象”。而实际上,XLua 并不能直接的实现这样的转换。

不过无所谓的,反正是开源的,直接改源码。

进行自定义 Lua 迭代器 与 C# 迭代器类型的互通

实际上也不算改源码,XLua 为我们了提供了 自定义 互通类型 的方法,该方法就是修改器其RawObject 类。RawObject 所在目录

RawObject

可以看到,RawObject 默认已有一些类型。

我们可以在 Lua 中,使用 CS.XLua.Cast.Int32(o) 来把 o “强转” 为 Int32 类型。

同样的道理,我们可以类似的在这里面声明 IEnumerator 类:对照着已有的默认属性,添加我们的 IEnumerator 类

这样,我们就可以在 Lua 侧,把我们的对象转换为 System.Collections.IEnumerator 类型啦

至于它们具体是怎么转换的,XLua 已经帮我们解决。

使用代码:

但,实际上此时会报错:System.Collections.IEnumerator must need CSharpCallLua

也就是说,我们需要给 System.Collections.IEnumerator 加上 CSharpCallLua 特性,对于源码这种我们无法直接给写上特性的,可以使用 XLua 提供的 动态添加标签的方法。

这里,我直接使用了 XLua 示例中给的一个 动态添加特性的示例脚本,里面给出了较为适用的动态打标签方法。具体细节大家可以去看里面的代码,这里就不赘述了。ExampleConfig 所在目录

因为我是打算 全Lua 编程的,所以把 全Lua 编程部分取消注释

这里我使用最简单的打标签方式,直接在这个类中声明静态的列表:

至此,Unity 协程 与 Lua 协程的互通就完毕了。

最后

在做的过程中,又找了找资料,翻了下示例(本来我下的XLua 是没有全部示例的),发现其实 XLua 内部有返回 IEnumerator 对象的方法,我们只需要调用这个方法。然后再像我后面一样,设置类型转换,和打 CSharpCallLua 标签即可。

那么 XLua 内置的这个方法就在它的 util.lua 脚本中:util 脚本目录

util 脚本的一部分

其中  cs_generator 该方法就可以返回一个 IEnumerator 对象,我们只需要传入方法体即可。

可以看到,这里的实现,与我的实现是一样的,只是我更习惯于先声明一个类的脚本,然后用创建对象的方式,来返回 IEnumerator 而不是直接整合在一个方法中。

因此,这里的使用也是完全一样的。

使用代码:

lua协程 unity_XLua 之 Lua 协程 与 Unity 协程互通相关推荐

  1. Unity协程(Coroutine)原理深入剖析再续

    Unity协程(Coroutine)原理深入剖析再续 By D.S.Qiu 尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com 前面已经介绍过对协程(Coroutine ...

  2. 【Unity】Unity协程(Coroutine)的原理与应用

    文章目录 前言 一.什么是协程 二.应用场景 1.异步加载资源 2.将一个复杂程序分帧执行 3.定时器 三.协程的使用 注意事项 四.Unity协程的底层原理 1. 协程本体:C#的迭代器函数 2. ...

  3. unity协程实现多个动画连播

    unity协程实现多个动画连播 unity协程实现多个动画连播 协程的理解 协程实现多个动画连播 unity协程实现多个动画连播 协程的理解 协程不是进程,也不是线程,它就是一个函数,一个特殊的函数- ...

  4. Unity协程实现分析以及Lua协程与Unity协程的混合使用

    1,节选翻译一篇stackoverflow关于Unity协程实现的讨论 - The big clues are in the C# version. Firstly, note that the re ...

  5. lua学习01:c调用lua、lua调用c、lua的协程、lua的常见API、lua读取配置文件总结

    文章目录 1.c通过虚拟机和虚拟栈调用lua test-vm.c test-vm.lua 打印效果 2.lua通过虚拟机和虚拟栈调用c lua-tbl.c test-tbl.lua 3.lua的协程 ...

  6. Unity 协程Coroutine综合测试

    Unity 协程Coroutine综合测试 1 using UnityEngine; 2 using System.Collections; 3 using System.Text; 4 5 publ ...

  7. go 怎么等待所有的协程完成_Go语言入门必知教程-协程

    Golang中的并发性是指函数独立运行的能力.Goroutines就是能够并发运行的函数,也叫协程,它们是Golang提供作为并发处理操作的方法. 使用go语句创建协程goroutines 要将函数作 ...

  8. Unity 协程原理探究与实现

    目录 一.介绍 二.迭代器 三.原理 四.总结 一.介绍 协程Coroutine在Unity中一直扮演者重要的角色.可以实现简单的计时器.将耗时的操作拆分成几个步骤分散在每一帧去运行等等,用起来很是方 ...

  9. python 协程池gevent.pool_进程池\线程池,协程,gevent

    目录 1. 进程池与线程池 2. 协程 3. gevent 4. 单线程下实现并发的套接字通信 首先写一个基于多线程的套接字 服务端: from socket import * from thread ...

最新文章

  1. 面试官:InnoDB中一棵B+树可以存放多少行数据?
  2. PHP stomp 连接判断,php实现通过stomp协议连接ActiveMQ操作示例
  3. VUE3模板ref引用子组件或者子组件的方法
  4. axios_的其他方式发送请求_使用axios.request .get .delete .post .put 等方法发送请求---axios工作笔记005
  5. linux安装Linux下软件的安装与卸载方法
  6. win10电脑桌面html,手把手教你美化win10电脑桌面的小技巧
  7. Chrome扩展程序开发文档(中文译文)
  8. 本工具仅仅交流之用,把黑群晖洗白用,如果对此感兴趣请支持正版,请勿用于违法,作者不承担法律和相关连带责任,工具内有详细sn算号器,可供使用还有教程
  9. python俄罗斯方块思路_python实现俄罗斯方块小游戏
  10. windows操作系统32位与64位的含义
  11. 《当程序员的那些狗日日子》(四十)繁杂的需求
  12. 从头开始实现Java多人联机游戏(飞机大战)源码粘贴即用
  13. SequoiaDB巨杉数据库-JDBC驱动
  14. 众安运维监控平台,构建devops一体化监控和运维体系
  15. 云端敏捷部署单节点MySQl与Redis服务(以Ubuntu为例)
  16. 攻防世界web高手进阶区(一)
  17. 吴恩达老师,被曝靠「教书」实现首个IPO上市,Coursera估值50亿美元
  18. pip及openpyxl安装
  19. python 树莓派摄像头_Python实现树莓派摄像头持续录像并传送到主机的步骤
  20. 建造者(Builder)模式

热门文章

  1. 什么是三层架构以及三成架构的好处
  2. C语言文件操作系统实验
  3. 【致敬未来的攻城狮计划】— 连续打卡第十天:FSP固件库开发及FSP配置详解。
  4. python threading中的join和setDaemon方法
  5. mysql行转列sql函数_SQL之行转列Pivot函数
  6. echarts--设置图表背景网格线
  7. XShell命令查看进程
  8. Dubbo原理和机制详解(非常全面)
  9. numpy.eye()函数
  10. 香港科大商学院获评全球最佳商学院之一!