Context:
I’m developing a scripting API for my game engine, such that scripts can be “suspended” across multiple game frames for an intuitive flow, thus avoiding implementation of numerous tedious state machines, “command list” implementations, etc. For example:
suspend fun ScriptContext.npcBehavior() {
while (true) {
npc.walkTo(100, 0, 0)
npc.textBlurb("Hello!")
npc.walkTo(0, 0, 0)
npc.textBlurb("World!")
}
}
walkTo
is implemented something like:
suspend fun Npc.walkTo(x: Float, y: Float, z: Float) {
suspendCancellableCoroutine { cont ->
walkingSystem.beginWalking(this, x, y, z, cont)
}
}
Then later, we resume like:
class WalkingSystem {
fun perFrameUpdate() {
for (agent in agentList) {
agent.pos += ...
if (distance(agent.pos, agent.destination) < threshold) {
agent.cont.resume(Unit)
}
}
}
}
Of course we’d have to remove the agent from the list first, avoid concurrent modification due to resuming continuations, etc. But you get the idea.
If I use Dispatchers.Unconfined
and either CoroutineStart.DEFAULT
or UNDISPATCHED
, it seems to work as expected, remaining on the main thread so long as I do everything on the main thread (which is mandatory for OpenGL, unfortunately. Shared contexts and context swapping are not an option.)
My Questions:
EDIT: I just stumbled upon Dispatchers.Main
and the fact you can implement your own, and conceptually it sounds close to what I need, while potentially permitting the entire coroutine library to be used “safely,” provided you’re okay spinning up a few extra threads. (Sandboxing this for non-codies would be ideal though.) However, I can’t find a resource on implementing CoroutineDispatcher
for myself, so I might be in for a deep dive here if the benefits are really there…
Any resources on implementing my own CoroutineDispatcher
, or even better, a Main
one? Could I just inherit from some existing dispatcher, override dispatch
and check if the threads match, then do what Unconfined
does if on main?
If implementing Main
would be infinitely better than using Unconfined
, then ignore the remaining questions. Otherwise…
Unconfined Questions
-
Under my current approach with
Unconfined
, what conventions must scripts follow to stay on the main thread? So far, all I’m aware of is disallowing calls towithContext
anddelay
– are there any other key methods? -
Is there a better way of disallowing methods then redeclaring them on a dispatch receiver with
@Deprecated
? Perhaps I could wrapkotlin.coroutines
behind an extremely restricted subset of commands, though allowing direct use ofChannels
,select
, and probably more would be nice. -
Is there a better configuration for this than what I described? I like
Unconfined
because it immediately executes the script in place, guaranteeing the calling or resuming thread is the executing one. It also means I have control over all threads in my engine, without deferring to anExecutor
at the top level - the API is essentially self-contained. Probably the biggest caveat is the “experimental” tag onUnconfined
, and fears of violating the “main thread” rule without realizing. -
Could I actually use
withContext
somehow, ensuring that execution returns to the calling thread after resuming and leaving scope? This would allow easy integration of wrapped “blocking” calls, for instance. I just can’t support any arbitrary thread running any script after resuming! (Which appears to be the case withUnconfined
, if I’ve understood correctly.) EDIT: I’m not sure if this is true any more. -
Any major problems I’m missing with this? Any big “gotchas” from calling
resume
outside any coroutine related scope?
Thanks for your time.