Unity的协程和Lua的协程

本文分享Unity的协程和Lua的协程

协程(Coroutine)在我们游戏开发中有着比较重要的地位.

协程全名协同程序, 和线程不同, 协程是依附于主线程执行的, 相当于在主线程上夺取了一定的执行时间片.

也可以理解为在主线程的调用之外开辟了一个单独的调用栈, 并在协程消亡之前能保存内部信息和与其它协程共享公用信息.

协程实质是运行在主线程, 代码仍然是同步执行, 只是在某些点被挂起然后重新进行主线程其它代码的执行, 之后重新进入协程时可以从离开点继续执行. 这点和递归有些类似.

协程的核心概念有挂起恢复, 挂起即执行到某个位置后停止, 直到被外部恢复.

同一时间只能最多有一个协程在运行, 其它协程需要被挂起.

协程有主要有以下用途:

  • 异步逻辑的同步写法
  • 递归代码的非递归写法

Unity和Lua在协程概念的实现上有一些区别, 下面简单和大家分享.

Unity中的协程

Unity使用了C#的迭代器yield关键字来实现协程.

实现原理如下:

  • 方法返回迭代器(IEnumerator)

  • 使用yield关键字每次往迭代器中添加一个可迭代对象(null或者继承于YieldInstruction类)

  • yield break或者执行完毕方法后结束协程

  • 通过方法StartCoroutineStopCoroutine来启动或者关闭协程

  • 这里的可迭代对象会加入到Update, FixedUpdate等生命周期中执行

void Start()
{
	var cor = StartCoroutine(Example());
	StopCoroutine(cor);
}

IEnumerator Example()
{
    print(Time.time);
    yield return new WaitForSeconds(5);
    print(Time.time);
}

与yield结合使用的可迭代对象

在yield调用时, 可以返回一些可用的迭代对象执行预定义的行为, 当然, 也可以自定义对象.

除了返回null之外, 其它的类都是继承于YieldInstruction类, 也可以自定义类继承于CustomYieldInstruction来定义自己的协程逻辑, 对这两个类我们会在另外的文章中单独分享, 本文只是简单介绍并与Lua的协程对比.

yield return null

当返回null时, Unity会在下一帧的Update和LateUpdate之间恢复协程的运行.

如下代码会在每帧打印"Test":

IEnumerator Example()
{
	while(true)
	{
		Debug.Log("Test")
		yield return null;
	}
}

当然, 也可以返回数字, 如yield return 0; yield return 1, 但是不管返回null还是数字, 都只是延迟一帧的时间, 即一个"yield return null或者数字"调用延迟一帧, 如果需要延迟多帧需要多次调用.

yield return new WaitForEndOfFrame()

顾名思义, 在本帧末尾调用.

即渲染所有摄像机和GUI之后, 在屏幕上显示该帧之前调用.

有时需要将本帧准备显示的内容截图保存时, 这个调用很方便.

yield return new WaitForFixedUpdate()

顾名思义, 在下一个FixedUpdate调用.

yield return new WaitForSeconds(time)

从当前帧的末尾开始, 等待一定时间再继续执行.

这个时间受Time.timeScale的影响, 可能会与实际的时间不一致.

还要注意的是, 这个调用是从当前帧的末尾开始, 如果当前帧执行时间过程, 那么实际等待的时间也会与设定的时间不一致.

yield return new WaitForSecondsRealtime(time)

与WaitForSeconds一样, 只不过使用未缩放的时间(Time.unscaledTime), 这样可以尽可能保证时间的准确性.

其它几个继承于CustomYieldInstruction的类我们在另外的文章中分享.


Lua中的协程

现在很多大型项目会使用C#与Lua结合的方式进行开发, 而Lua中也有协程的概念, 下面我们来分享一下.

Lua中的协程使用专门的包(Package)来处理, 即"coroutine".

如下所示:

local cor = coroutine.create(function()
    print("2222")
    local result = coroutine.yield()
    print(result)

    print("4444")
end)

print("1111")
coroutine.resume(cor)

print("33333")
coroutine.resume(cor, "#####")

print("55555")
coroutine.resume(cor)

-- 会打印:
-- 1111
-- 2222
-- 3333
-- #####
-- 4444
-- 55555

使用coroutine.create创建一个协程, 并使用coroutine.resume来启动, 直到遇到coroutine.yield()就挂起协程, 等待外部激活.

可以通过coroutine.resume(cor, "xxxx")来给协程传递变量, 这样可以在协程内部做一些选择逻辑, 比如彻底停止协程的运行: return.

其他接口

其他接口, 如coroutine.wrap可以在创建的时候立即启动协程, coroutine.status可以查看协程的状态等, 有兴趣的同学可以自行学习.

与Unity协程的异同

相同点

  • 因为都是协程, 所以核心的概念, 即都有挂起恢复的操作
  • 挂起的操作是一样的, 都是使用"yield"的概念(即让出执行权限的意思)
  • 都是可以在主线程和协程之间传递数据

不同点

  • 主要的不同点是, Unity的协程的恢复操作是由Unity来控制, 只是给大家提供了一些预设的行为和提供自定义行为的接口
  • 而Lua由于没有主循环, 协程的恢复需要在主线程中手动恢复
  • Lua没有提供一些预设的行为, 使用比较灵活, 但缺失了方便性, 一些常用行为, 如延迟等需要配合Unity的Update之类的接口来实现

实例分享

下面分享协程的一些用途实例.

1. 异步代码的同步写法

在游戏开发中, 异步代码的出现频率是很高的, 比如请求服务器消息, 异步加载资源等.

在请求服务器消息时, 经常需要消息的顺序调用, 但因为发消息是异步过程, 只能通过在上一个消息的回调里处理下一个消息的请求.

使用协程可以将这种代码近似写成同步的.

如下所示(Lua):

NetMgr.SendMsg("msg1", function() 
	NetMgr.SendMsg("msg2", function() 
        NetMgr.SendMsg("msg3", function() 
            NetMgr.SendMsg("msg4", function() 
    			do xxxxxxxxxxxxxxxxxxxx return end
            end)
		end)
    end)
end)
-------------------------------------------------------------------
coroutine.wrap(function() 
	NetMgr.SendMsg("msg1", function() 
		coroutine.resume()
	end)
	
	coroutine.yield()
    NetMgr.SendMsg("msg2", function() 
		coroutine.resume()
	end)
	
    coroutine.yield()
    NetMgr.SendMsg("msg3", function() 
		coroutine.resume()
	end)
	
    coroutine.yield()
    NetMgr.SendMsg("msg4", function() 
		coroutine.resume()
	end)
	
    coroutine.yield()
    do xxxxxxxxxxxxxxxxxxxx return end
end)

异步切换场景, 代码如下(C#):

private void OnClick()
{
	StarCoroutine(LoadScene(sceneName));
}

private IEnumerator LoadScene(sceneName)
{
	var result = SceneManagement.SceneManager.LoadSceneAsync(sceneName);
	while(not result.isDone)
	{
		yield return null;
	}
	
	Debug.Log("创建切换成功!");
}

2. 递归代码的非递归写法

有一些逻辑需要使用递归, 比如最常见的斐波那契数列.

如下所示(C#):

public IEnumerable Fib(int total)
{
	int pre = 0, cur = 1;
	for (int count = 0; count < total; ++count)
	{
		yield return pre;
		
		int newCur = pre + cur;
		pre = cur;
		cur = newCur;
	}
}

void Main()
{
	foreach(var itor in Fib(10))
	{
		Console.WriteLine(itor.ToString());
	}
}

猜你喜欢

转载自blog.csdn.net/woodengm/article/details/119255321#comments_27849981