下载安卓APP箭头
箭头给我发消息

客服QQ:3315713922
论坛 >编程语言 >用 Lua 的协程 coroutine 控制 Codea 屏幕刷新速度

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

九城学院发布于 2016-06-22 15:05查看:1473

  在 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() 中, 这里需要稍作修改, 因为 Codea 的 draw() 天然地就是一个大循环, 所以我们可以考虑把 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来试验, 则显示 FPS 为 30左右, 使用 1/2 来试验, 则 FPS 为 2 左右, 看来这个尝试成功了!

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



收藏(0)0
查看评分情况

全部评分

此主贴暂时没有点赞评分

总计:0

回复分享

共有0条评论

    • IT宅男
    • mr jack
    • Mr ken
    • Mright
    • cappuccino
    • YUI
    • 课课家运营团队
    • 课课家技术团队1
    • 酸酸~甜甜
    • 选择版块:

    • 标题:

    • 内容

    • 验证码:

    • 标题:

    • 内容

    • 选择版块:

    移动帖子x

    移动到: