Dorothy Routine

  回调函数大家一定不陌生,比如说我们使用一个计时器的功能,时间到了要做一件事,自然就会定义一个自己的回调函数,就像这样:

setTimeout(1000, function(){ ... });

  在这样回调式的写法里,如果我们要连续做很多件事,每件事之间都要等待一些时间,这样要怎么做呢:

setTimeout(1000, function()
{
	console.log("等待1s后做第一件事");
	setTimeout(2000, function()
	{
		console.log("再等待2s后做第二件事");
        setTimeout(3000, function()
		{
			console.log("再等待3s后做第三件事");
			setTimeout(4000, function()
			{
				console.log("再等待4s后做第四件事");
			}); 
		}); 
	}); 
});
// Wow, Such code, So Nasty

  如果各个事件之间有先后等待的关系的话,用回调式的写法很容易就进入这样的callback hell。我们来看看Dorothy中的写法吧:

oRoutine(once(function()
	wait(seconds(1))
    print("等待1s后做第一件事")
	wait(seconds(2))
    print("再等待2s后做第二件事")
	wait(seconds(3))
    print("再等待3s后做第三件事")
	wait(seconds(4))
    print("再等待4s后做第四件事")
end))
-- Beautiful

  是不是换这样写法更符合正常的思维呢,代码维护起来也更容易。之所以Dorothy中能这样写,是因为利用了Lua语言的协程(coroutine)的特性。我们知道多线程是系统在硬件级别控制多段程序并发执行的方式,协程则是多段程序交替地主动转移执行权的并发运行的方式。多线程需要跨越多个程序运行空间(线程),所以存在资源访问的竞争占用关系,线程间切换要付出一定的代价;但是协程是让多段程序在同一个线程中进行先后并发,不存在资源访存和占用的冲突,程序段之间的切换消耗非常低。
  在魔兽争霸3中的脚本系统就充分运用了协程的特性。如果用过魔兽争霸3的地图编辑器的话,一定会对里面的触发器系统印象深刻。一个触发器是用来编写一段游戏规则的脚本程序。来看一段实际的魔兽争霸脚本,摘抄自我以前做的一个魔兽地图:

// Jin死了
function Trig_JinDie_Actions takes nothing returns nothing
	// 等待Jin的死亡动画播放3秒钟
    call TriggerSleepAction( 3.00 )
    // 如果满足Jin无法复活的条件
    if ( Trig_JinDie_Func003C() ) then
    	// 玩家游戏失败,并显示TRIGSTR_3047代表的字符串
        call CustomDefeatBJ( Player(0), "TRIGSTR_3047" )
    else
    	// 设置DaPu的英雄等级降低1级
        call SetHeroLevelBJ( udg_MenT[3], ( GetHeroLevel(udg_MenT[3]) - 1 ), false )
        // 添加一个吸血药水的特效给DaPu模型的origin骨骼点
        call AddSpecialEffectTargetUnitBJ( "origin", udg_MenT[3], "Abilities\\Spells\\Items\\VampiricPotion\\VampPotionCaster.mdl" )
        // 保存DaPu身上的特效
        set udg_SoulLinkEffect[3] = GetLastCreatedEffectBJ()
        // 等待吸血药水的特效播放2秒钟
        call TriggerSleepAction( 2 )
        // 销毁特效
        call DestroyEffectBJ( udg_SoulLinkEffect[3] )
        // 获得Jin的位置
        set udg_MenTStand[2] = GetUnitLoc(udg_MenT[2])
        // 复活Jin
        call ReviveHeroLoc( udg_MenT[2], udg_MenTStand[2], true )
        // 设置Jin的生命为33%
        call SetUnitLifePercentBJ( udg_MenT[2], 33.00 )
        // 删除Jin的位置对象防止内存泄露
        call RemoveLocation( udg_MenTStand[2] )
    endif
endfunction

  写游戏脚本我们本来就应该只要按照自然的逻辑编写的,当然上面代码中的防内存泄露的代码存在稍显不科学。重点就是暴雪的游戏脚本也是这么用自然的逻辑组织的。
  Dorothy的Routine还有一些特殊用法,比如游戏对象的每游戏帧更新的回调可以变成Routine的方式。

local oRoutine = require("oRoutine")
local once = require("once")
local wait = require("wait")
local seconds = require("seconds")

-- 要做的逻辑是每3秒打印node的description重复5次
local node = CCNode()

-- 使用每帧回调实现
local time = 0
local count = 0
node:schedule(function(self, deltaTime)
	time = time + deltaTime
	if time >= 3 then
		if count < 5 then
			print(self.description)
            time = 0
			count = count + 1
		else
			self:unschedule()
		end
	end
)

-- 使用Routine实现
node:schedule(once(function(self)
	for i = 1,5 do
		wait(seconds(3))
		print(self.description)
	end
))

其它oRoutine的用法

-- 在任意代码逻辑可以创建并开启一个Routine
-- 一个Routine就像一个独立的线程不会阻塞主线程
-- main.lua
local CCScene = require("CCScene")
local CCDirector = require("CCDirector")
local CCLayerColor = require("CCLayerColor")
local ccColor4 = require("ccColor4")
local oVec2 = require("oVec2")
local oRoutine = require("oRoutine")
local once = require("once")
local loop = require("loop")
local wait = require("wait")
local cycle = require("cycle")
local seconds = require("seconds")

local winSize = CCDirector.winSize
local layer = CCLayerColor(
				ccColor4(0xff00ffff),
				winSize.width,
                winSize.height)
layer.anchor = oVec2.zero
layer.opacity = 0

oRoutine(loop(function()
	wait(seconds(3))
	print("every 3 second")
end))

local routine = oRoutine(once(function()
	wait(seconds(20))
	print("this routine will be cancelled")
end))

oRoutine(once(function()
	wait(seconds(10))
	oRoutine:remove(routine)
	print("after 10 seconds, cancel routine above")
end))

oRoutine(once(function()
	cycle(2, function(deltaTime)
		local deltaOpacity = deltaTime/2
		layer.opacity = layer.opacity + deltaOpacity
	end)
	layer.opacity = 1
end))

oRoutine(once(function()
	wait(function() return layer.opacity < 1 end)
	print("now layer is fully visible")
end))

local scene = CCScene()
scene:addChild(layer)
CCDirector:run(scene)

  oRoutine的内部实现很简单,看这里

标题目录