lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]


On Thu, 4 Aug 2005, Russell Y. Webb wrote:

Yes, I was thinking on use coroutines to do this, but
perhaps there is some way of implement a script that
has sequential tasks, and some taks take a little to
complete.

I can't think of a way to it without coroutines that don't require
rewriting the script code in an unnatural way. Any suggestions?

Coroutines are VERY useful for a lot of different purposes, but they
are not obvious to use. To help the original poster of the thread I put together a quick tutorial on building a scheduler using coroutines. If they (and maybe others?) give me feedback I can clean it up and put it on the Wiki.

Tutorial is attached.

  - Tom


twrensch@sdf.lonestar.org
SDF Public Access UNIX System - http://sdf.lonestar.org
-- A Quick Coroutine/Scheduling "recipe"
--
--     By Tom Wrensch (twrensch@freeshell.org)
--
-- Coroutines are indeed the answer to this particular problem,
-- but using coroutines is non-trivial. This bit of text is
-- intended to both get you up to speed and solve the problem
-- you gave in your E-mail (I hope).

-- I'm going to keep things as dirt simple as possible
-- so no fancy object system and the minimal support needed to
-- both illustrate the technique and solve your problem.

-- In this example we are going to use the idea of Game Time
-- to keep track of when everything happens. We will use a 
-- function waitfor(time) to wait for some number of game
-- clock 'ticks'. We'll start the clock at 1:

GameTime = 1.

-- Next we need to build some actors to play around in the
-- world. Actors in the world will have names, will be able to 
-- walk, sleep, and shoot.

Actor = {}

function Actor:walk(dist)
	print(GameTime, self.name, "has started walking")
	waitfor(dist*1)  -- Assume 1 time unit per distance unit
	print(GameTime, self.name, "has finished walking")
end

function Actor:sleep(time)
	print(GameTime, self.name, "Has gone to sleep")
	waitfor(time)
	print(GameTime, self.name, "has finished sleeping")
end

function Actor:shoot(what)
	print(GameTime, self.name, "has shot "..what.name) 
end

function newActor(name)
	local newobj = {}
	newobj.name = name
	setmetatable(newobj,{__index=Actor})
	return newobj
end

-- The key to making the sleep and walk methods above work is
-- the waitfor(time) function. This function will assume it is
-- running in a coroutine and use yield to stop running and
-- pass necessary information along about when it should be
-- woken up again.

function waitfor(time)
	coroutine.yield(time)
end

-- Now for the hard part. We have to create a central scheduler
-- whose job it is to make sure everyone runs when they should.
-- This is a lot easier than it sounds, but still is not simple.

Scheduler = {}

-- The scheduler will keep a list of coroutines that are waiting
-- for particular clock ticks. We will initialized this list with
-- an init() method and make a method that lets us add new
-- coroutines to the list. We will also add a field to keep track
-- of the largest tick in the ticktable, so we know when to stop.

function Scheduler:init()
	self.ticktable = {}
	self.lasttick = 0
end

function Scheduler:add(fun)
	self:resumeAtTick(coroutine.create(fun),GameTime)
end

-- Now for the guts of the scheduler. We need a method to add a coroutine
-- to the schedule to be executed at a particular tick (in fact we assumed
-- such a method existed in the add(fun) method above).

function Scheduler:resumeAtTick(corout, tick)
	local ticklist = self.ticktable[tick]
	if ticklist == nil then  -- If no list for this tick, add one
		ticklist = {}
		self.ticktable[tick] = ticklist
	end
	table.insert(ticklist, corout)
	self.lasttick = math.max(self.lasttick, tick) -- update largest tick
end

-- And we need a function to actually run through the tick list. I will
-- break this up into two functions. The main one, run(), will pick the
-- next coroutine to run. The second, runOne(corout) will take that
-- coroutine, run it, and handle any returned values from a yield that
--  might cause it to be rescheduled.

function Scheduler:runOne(corout)
	local flag,count = coroutine.resume(corout)
	if flag and count then  -- no errors and wants to restart
		self:resumeAtTick(corout,GameTime+count)
	end
end


function Scheduler:run()
	while GameTime <= self.lasttick do
		local ticklist = self.ticktable[GameTime]
		if ticklist then -- ticklist is not nil
			while table.getn(ticklist) > 0 do
				self:runOne( table.remove(ticklist, 1) )
			end
		end
		GameTime = GameTime + 1
	end
	print("Game Over!")
end

-- Nearly there now. We have to create a test using our new scheduler
-- and game system. The design will require that each actor have their
-- own script.

Mike = newActor("Mike")
Betty = newActor("Betty")

function MikeScript()
	Mike:walk(3)
	Mike:shoot(Betty)
	Mike:sleep(3)
end

function BettyScript()
	Betty:sleep(3)
	Betty:walk(2)
	for i=1,5 do 
		Betty:shoot(Mike)
	end
end


-- Finally we start things up and see what happens.

Scheduler:init()
Scheduler:add(MikeScript)
Scheduler:add(BettyScript)
Scheduler:run()

-- It isn't hard to change things so the yield returns more information
-- that can be used by the scheduler. One trick is to return a function
-- that must evaluate to true to cause the coroutine to be rescheduled.
-- That can easily be used to implement the game time technique shown
-- here, but can also be used to handle more complicated situations
-- (like actors waking up when they are shot).

-- Good luck.