The value of coroutines?

Hello All,

I’m trying to figure out the value in Coroutines? I’ve got a multiplayer game (Colyseus) and I’m currently getting the reconnecting functionality working on reload, and I’m considering if having a heartbeat functionality to reconnect without reloading is worthwhile. I thought this might be something that a Coroutine would handle, but I’m less sure now. Any thoughts are appreciated.

Coroutines provide you with a means of running Lua code over a long period of time outside of the normal flow of execution. You provide a function to be run in the coroutine and you can from within the coroutine pause execution and from the outside resumes it again.

You can use it to synchronise a sequence of asynchronous function calls. Or to run an entire game loop. Or pathfinding over multiple frames. Or the AI logic of individual agents.

4 Likes

I am using coroutines as part of an event system. For example, when the player interacts with an NPC, I want to play a cutscene, show some dialog, disable player input, move characters around, etc.

All of this happens over many frames. I want some of these actions to happen at the same time (scrolling dialog and character movement) and others to happen alone. How do I keep track of everything and coordinate it all, then give input back to the player at the end? By using coroutines, I can pause and resume execution throughout the event!

For example: scrolling dialog box. I want to push one new character to the box every frame. Each cycle of the coroutine pushes one character of a string, then pauses until the next frame’s update().

7 Likes

We use coroutines for scripting in game dialog one line at a time. Doing it this way enables us to use Lua directly between lines to do things like get user input / change variables / do anything we want all in the Lua script without needing to code custom functions for everything. It is powerful and flexible.

4 Likes

Why not just have a separate script file, that would behave basically like a coroutine wouldn’t it?

True, but coroutines are much more low-cost than script files. And they don’t have to be associated with game objects (although you do need at least one script file to start the coroutine).

And I love the way in which you can make long running and complex tasks look synchronous. With coroutines you can write code that looks like this:

moveTo(position, 200) -- move to position, 200 pixels per second
wait(2) -- wait 2 seconds
fireRocket(target) -- fire rocket at target
wait(1) -- wait 1 second
selfDestruct() -- boom!

A sequence of events that happen over a longer period of time, written one instruction after the other, with no notion of callbacks, delays or anything like that.

The moveTo() and wait() functions looks like this:

local function moveTo(target, speed)
	local co = coroutine.running()
	local pos = go.get_position()
	local distance = vmath.length(target-pos)
	local duration = distance / speed
	go.animate(".", "position", go.PLAYBACK_ONCE_FORWARD, target, go.EASING_LINEAR, duration, 0, function()
		coroutine.resume(co)
	end)
	coroutine.yield()
end

local function wait(delay)
	local co = coroutine.running()
	timer.delay(delay, false, function()
		coroutine.resume(co)
	end)
	coroutine.yield()
end
6 Likes

I see, so potentially the heartbeat with the server might be a good option for a coroutine?

Additionally, data gathering for a UI, think FTL’s ship data could be a good option?

Thank you for the help.

3 Likes

I am not myself familiarized with the ins and outs of implementing coroutines.
The “wait(seconds)” seems a bit less useful to me than a wait for the move to to actually complete?

@britzl, how would you recommend that be implemented?

E.g.

moveTo(player,  target, speed)
waitForMoveTo()

For me a coroutine is usually an alternative to a mess of nested timers or go.animates.

No no, the moveTo() function will not return until the move is finished. Then it will wait another 2 seconds at that location, then fire a rocket, wait another second and finally self destruct.

The two wait() were not put there to wait for the previous commands to complete. Sorry if it looked that way!

1 Like

Thanks! I realize now that the moveTo() already has the behavior built into it.

1 Like

@britzl I’ve been playing quite a bit wih coroutines lately and I got stuck with a scenario.

How can I yield the result until a message is received from another GO?

Something like this should work:

function init(self)
	self.co = coroutine.create(function()
		print("in coroutine")
		print("waiting for message")
		local message_id = coroutine.yield()
		print("received message", message_id)
	end)
	coroutine.resume(self.co)
end

function on_message(self, message_id, message, sender)
	if self.co and coroutine.status(self.co) == "suspended" then
		coroutine.resume(self.co, message_id)
	end
end
5 Likes