文档章节

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

FreeBlues
 FreeBlues
发布于 2016/06/14 22:48
字数 2131
阅读 124
收藏 2

用 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(协程)

© 著作权归作者所有

FreeBlues
粉丝 99
博文 280
码字总数 493678
作品 0
其它
程序员
私信 提问
从零开始写一个武侠冒险游戏-5-使用协程

从零开始写一个武侠冒险游戏-5-使用协程 ---- 用协程实现控制权灵活切换 概述 因为我们的地图类是可以自己控制大小的, 在无意中用了一个比较大的数字 后, 结果花了挺长时间来生成地图, 而在这...

FreeBlues
2016/06/15
130
0
用 Lua 的协程 coroutine 在 Codea 上实现一个多任务调度类

用 Lua 的协程 coroutine 在 Codea 上实现一个多任务调度类 概述 问题描述 在 中调试程序时发现一个问题: 如果在 中执行了比较耗时的语句, 比如地图生成, 资源下载等操作, 那么在该这些操作没...

FreeBlues
2016/06/15
68
0
ngx_lua_API 指令详解(五)coroutine.create,coroutine.resume,coroutine.yield 等集合指令介绍

ngx_lua 模块(原理实现) 1、每个worker(工作进程)创建一个Lua VM,worker内所有协程共享VM; 2、将Nginx I/O原语封装后注入 Lua VM,允许Lua代码直接访问; 3、每个外部请求都由一个Lua...

tinywan1227
2017/07/19
0
0
从零开始写一个武侠冒险游戏-0-开发框架Codea简介

从零开始写一个武侠冒险游戏-0-开发框架Codea简介 概述 本游戏全程使用一款运行于 上的开发工具类 -- 来开发, 是一款 + 的开发工具, 它既是一个: - 也是一个: - 还是一个: - 更是一个: - , 可...

FreeBlues
2016/06/21
106
0
利用Lua协程实现Future模式

Future模式: 参见http://www.cnblogs.com/zhiranok/archive/2011/03/26/FuturePattern.html 使用future的好处是即利用了异步的并行能力,又保证主逻辑串行执行,保持简单。 2. Lua 协程 si...

follitude
2016/09/17
43
0

没有更多内容

加载失败,请刷新页面

加载更多

PostgreSQL 11.3 locking

rudi
今天
5
0
Mybatis Plus sql注入器

一、继承AbstractMethod /** * @author beth * @data 2019-10-23 20:39 */public class DeleteAllMethod extends AbstractMethod { @Override public MappedStatement injectMap......

一个yuanbeth
今天
10
1
一次写shell脚本的经历记录——特殊字符惹的祸

本文首发于微信公众号“我的小碗汤”,扫码文末二维码即可关注,欢迎一起交流! redis在容器化的过程中,涉及到纵向扩pod实例cpu、内存以及redis实例的maxmemory值,statefulset管理的pod需要...

码农实战
今天
4
0
为什么阿里巴巴Java开发手册中不建议在循环体中使用+进行字符串拼接?

之前在阅读《阿里巴巴Java开发手册》时,发现有一条是关于循环体中字符串拼接的建议,具体内容如下: 那么我们首先来用例子来看看在循环体中用 + 或者用 StringBuilder 进行字符串拼接的效率...

武培轩
今天
8
0
队列-链式(c/c++实现)

队列是在线性表功能稍作修改形成的,在生活中排队是不能插队的吧,先排队先得到对待,慢来得排在最后面,这样来就形成了”先进先出“的队列。作用就是通过伟大的程序员来实现算法解决现实生活...

白客C
今天
80
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部