用 Lua 的协程 coroutine 控制 Codea 屏幕刷新速度

用 Lua 的协程 coroutine 控制 Codea 屏幕刷新速度

概述

Codea 中, 函数 draw() 缺省每秒执行 60 次, 我们希望能修改一下它的刷新速度, 于是想到了 Lua 的一个特性:协程 coroutine, 希望试着用它来控制程序执行的节奏, 不过目前对于协程还不太了解, 那就一边看教程, 一边试验好了.

Codea 运行机制

我们知道, Codea 的运行机制是这样的:

  • setup() 只在程序启动时执行一次
  • draw() 在程序执行完 setup() 后反复循环执行, 每秒执行 60
  • touched()draw() 类似, 也是反复循环执行

简单说, 就是类似于这样的一个程序结构:

setup()

while true do
	...
	draw()
	touched(touch)
	...
end

协程 coroutine 的简单介绍

Lua 所支持的协程全称被称作协同式多线程collaborative multithreading)。Lua为每个 coroutine 提供一个独立的运行线路。然而和多线程不同的地方就是,coroutine 只有在显式调用 yield 函数后才被挂起,再调用 resume 函数后恢复运行, 同一时间内只有一个协程正在运行。

Lua 将它的协程函数都放进了 coroutine 这个表里,其中主要的函数如下:

表格图

协程 coroutine 的使用示例

新建协程 coroutine.create()

使用 coroutine.create(f) 可以为指定函数 f 新建一个协程 co, 代码如下:

-- 先定义一个函数 f
function f ()
	print(os.time())
end

-- 为这个函数新建一个协程
co = coroutine.create(f)

通常协程的例子都是直接在 coroutine.create() 中使用一个匿名函数作为参数, 我们这里为了更容易理解, 专门定义了一个函数 f.

  • 为一个函数新建协程的意义就在于我们可以通过协程来调用函数.

为什么要通过协程来调用函数呢? 因为如果我们直接调用函数, 那么从函数开始运行的那一刻起, 我们就只能被动地等待函数里的语句完全执行完后返回, 否则是没办法让函数在运行中暂停/恢复, 而如果是通过协程来调用的函数, 那么我们不仅可以让函数暂停在它内部的任意一条语句处, 还可以让函数随时从这个位置恢复运行.

也就是说, 通过为一个函数新建协程, 我们对函数的控制粒度从函数级别精细到了语句级别.

协程状态 coroutine.status()

我们可以用 coroutine.status(co) 来查看当前协程 co 的状态

> coroutine.status(co)
suspended
>

看来新建的协程默认是被设置为 挂起-suspended 状态的, 需要手动恢复.

恢复协程 coroutine.resume()

执行 coroutine.resume(co), 代码如下:

> coroutine.resume(co)
1465905122
true
> 

我们再查看一下协程的状态:

> coroutine.status(co)
dead
>

显示已经死掉了, 也就是说函数 f 已经执行完了.

挂起协程 coroutine.yield()

有人就问了, 这个例子一下子就执行完了, 协程只是在最初被挂起了一次, 我们如何去手动控制它的挂起/恢复呢? 其实这个例子有些太简单, 没有很好地模拟出适合协程发挥作用的使用场景来, 设想一下, 我们有一个函数执行起来要花很多时间, 如果不使用协程的话, 我们就只能傻傻地等待它执行完.

用了协程, 我们就可以在这个函数执行一段时间后, 执行一次 coroutine.yield() 让它暂停, 那么现在问题来了, 运行控制权如何转移? 这个函数执行了一半了, 控制权还在这个函数那里, 办法很简单, 就是把 coroutine.yield() 语句放在这个函数里边(当然, 我们也可以把它放在函数外面, 不过那是另外一个使用场景).

我们先把函数 f 改写成一个需要执行很长时间的函数, 然后把 coroutine.yield() 放在循环体中, 也就是让 f 每执行一次循环就自动挂起:

function f ()
	local k = 0
	for i=1,10000000 do
		k = k + i
		print(i)
		coroutine.yield()
	end
end

看看执行结果:

> co = coroutine.create(f)
> coroutine.status(co)
suspended
> coroutine.resume(co)                                                                                                                                                     2
true
> coroutine.status(co)
suspended
> coroutine.resume(co)
3
true
> coroutine.status(co)
suspended
> coroutine.resume(co)
4
true
> 

综合使用

很好, 完美地实现了我们的意图, 但是实际使用中我们肯定不会让程序这么频繁地 暂停/恢复, 一般会设置一个运行时间判断, 比如说执行 1 秒钟后暂停一次协程, 下面是改写后的代码:

time = os.time()
timeTick = 1

function f ()
	local k = 0
	for i=1,10000000 do
		k = k + i
		print(i)
		-- 如果运行时间超过 1 秒, 则暂停
		if (os.time() - time >= timeTick) then
			time = os.time()
			coroutine.yield()
		end
	end
end

co = coroutine.create(f)
coroutine.status(co)
coroutine.resume(co)

代码写好了, 但是运行起来表现有些不太对劲, 刚运行起来还正常, 但之后开始手动输入 coroutine.resume(co) 恢复时感觉还是跟之前的一样, 每个循环暂停一下, 认真分析才发现是因为我们手动输入的时间肯定要大于 1 秒, 所以每次都会暂停.

看来我们还需要修改一下代码, 那就再增加一个函数来负责自动按下恢复键, 然后把段代码放到一个无限循环中, 代码如下:

time = os.time()
timeTick = 1

function f ()
	local k = 0
	for i=1,10000000 do
		k = k + i
		-- print(i)
		-- 如果运行时间超过 timeTick 秒, 则暂停
		if (os.time() - time >= timeTick) then
			local str = string.format("Calc is %f%%", 100*i/10000000)
			print(str)
			time = os.time()
			coroutine.yield()
		end
	end
end

co = coroutine.create(f)

function autoResume()
	while true do
		coroutine.status(co)
		coroutine.resume(co)
	end
end

autoResume()

鉴于 os.time() 函数最小单位只能是 1 秒, 虽然使用 1 秒作为时间片有助于我们清楚地看到暂停/恢复 的过程, 但是如果我们想设置更小单位的时间片它就无能为力了, 所以后续改为使用 os.clock() 来计时, 它可以精确到毫秒级, 当然也可以设置为 1 秒, 把我们的时间片设置为 0.1, 代码如下:

time = os.clock()
timeTick = 0.1
print("timeTick is: ".. timeTick)

function f ()
	local k = 0
	for i=1,10000000 do
		k = k + i
		-- print(i)
		-- 如果运行时间超过 timeTick 秒, 则暂停
		if (os.clock() - time >= timeTick) then
			local str = string.format("Calc is %f%%", 100*i/10000000)
			print(str)
			time = os.clock()
			coroutine.yield()
		end
	end
end

co = coroutine.create(f)

function autoResume()
	while true do
		coroutine.status(co)
		coroutine.resume(co)
	end
end

autoResume()

执行记录如下:

Lua 5.3.2  Copyright (C) 1994-2015 Lua.org, PUC-Rio
timeTick is: 0.1
Calc is 0.556250%
Calc is 1.113390%
Calc is 1.671610%
Calc is 2.229500%
Calc is 2.787610%
Calc is 3.344670%
Calc is 3.902120%
Calc is 4.459460%
Calc is 5.017040%
...

好了, 关于协程, 我们已经基本了解了, 接下来就要想办法把它放到 Codea 里去了.

协程 coroutine 跟 Codea 代码框架的结合

上面那个例程中, 设置的时间片越小, 程序的控制权切换得越频繁, 这一点恰好可以用来设置 Codea 的屏幕刷新速度.

首先把那些只运行一次的函数和语句放到 setup() 中, 其次把那些需要反复执行的函数和语句放到 draw() 中, 这里需要稍作修改, 因为 Codeadraw() 天然地就是一个大循环, 所以我们可以考虑把 autoResume() 函数中的循环去掉, 把它的循环体放到 draw() 中就行了, 代码如下:

function setup()
	time = os.clock()
	timeTick = 1/2
	print("timeTick is: ".. timeTick)
	
	co = coroutine.create(f)
end

function draw()
	background(0)
	autoResume()
	sysInfo()
end

function f ()
	local k = 0
	for i=1,10000000 do
		k = k + i
		-- print(i)
		-- 如果运行时间超过 timeTick 秒, 则暂停
		if (os.clock() - time >= timeTick) then
			local str = string.format("Calc is %f%%", 100*i/10000000)
			--print(str)
			time = os.clock()
			coroutine.yield()
		end
	end
end

function autoResume()
	coroutine.status(co)
	coroutine.resume(co)
end

-- 显示FPS和内存使用情况
function sysInfo()
    pushStyle()
    -- fill(0,0,0,105)
    -- rect(650,740,220,30)
    fill(255, 255, 255, 255)
    -- 根据 DeltaTime 计算 fps, 根据 collectgarbage("count") 计算内存占用
    local fps = math.floor(1/DeltaTime)
    local mem = math.floor(collectgarbage("count"))
    text("FPS: "..fps.."    Mem:"..mem.." KB",650,740)
    popStyle()
end

这样我们就可以通过修改时间片 timeTick 的值来控制 draw() 函数的刷新速度了, 默认情况下 draw()1/60 秒刷新一次, 所以我们可以使用 1/60来试验, 这时显示的 FPS 应该是 60 左右, 使用 1/30来试验, 则显示 FPS30 左右, 使用 1/2 来试验, 则 FPS2 左右, 看来这个尝试成功了!

后续我们要在这个基础上搞一些更有趣的代码出来.

参考

快速掌握Lua 5.3 —— Coroutines
【深入Lua】理解Lua中最强大的特性-coroutine(协程)

转载于:https://my.oschina.net/freeblues/blog/691518

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值