Skip to content

Instantly share code, notes, and snippets.

@zenmumbler
Created July 26, 2020 13:00
Show Gist options
  • Save zenmumbler/e11d81d1c35901aae33ced33900dd6f8 to your computer and use it in GitHub Desktop.
Save zenmumbler/e11d81d1c35901aae33ced33900dd6f8 to your computer and use it in GitHub Desktop.
Basic Unity-like Coroutines in ES6
<!DOCTYPE html>
<html>
<head>
<title>Basic Unity-like Coroutines</title>
<script>
/*
This example illustrates a simple coroutine controller to act similarly to how Unity's
system works. The payload here is not very exciting but focus on the fact that the actual effort
in a normal usage scenario would just be writing the more() function and starting it somewhere.
This example will run happily in Safari 10+, Edge 13+, Firefox and Chrome.
Unity Coroutine Docs:
https://docs.unity3d.com/Manual/Coroutines.html
JS Docs on Generators and Iterators:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators
CC0, by @zenmumbler
https://zenmumbler.net/examples/coroutine.html
*/
function msg(m) {
const e = document.createElement("p");
e.textContent = m;
document.querySelector("#msgs").appendChild(e);
}
// Example of a kind of discriminated union entry result code to control coroutine behaviour
// Equivalent to Unity's WaitForSeconds(s).
function delaySeconds(s) {
return { c: "delay", s };
}
// The Coroutine controller, this could be embedded in a larger run-loop mechanism.
const Coroutine = {
_tasks: [],
_animRef: 0,
start: function(g) {
this._tasks.push(g);
if (! this._animRef) {
this._animRef = requestAnimationFrame(this.frame.bind(this));
}
},
frame: function() {
if (this._tasks.length === 0) {
this._animRef = 0;
msg("Done");
return;
}
const now = Date.now();
// use filter as loop mechanism to remove finished tasks immediately
this._tasks = this._tasks.filter(c => {
// quick n dirty way to handle timed delays
if ((typeof c.waitUntil === "number") && c.waitUntil > now) {
return true;
}
// run the task until it `yield`s or exits
const res = c.next();
if (res.done) {
return false; // delete this task
}
else {
// res.value is what was `yield`ed from the task
if (res.value && res.value.c === "delay") {
c.waitUntil = now + (res.value.s * 1000);
}
else {
c.waitUntil = undefined;
}
}
return true;
});
// like Unity, synchronize the Coroutine tasks to the update loop
this._animRef = requestAnimationFrame(this.frame.bind(this));
}
};
// ----
function* fade(index) {
for (let f = 1; f >= 0; f -= 0.1) {
// log a message, this is the core of this coroutine
msg("Val(" + index + ") = " + f.toFixed(1));
// this task has a 50/50 chance of either waiting 1 second or running next frame
yield (Math.random() > 0.5) ? delaySeconds(1) : undefined;
}
}
let fix = 0;
function more() {
// pass the index to the task
Coroutine.start(fade(++fix));
}
</script>
<style>
header { position: fixed; box-sizing: border-box; left: 0; top: 0; right: 0; height: 40px; padding: 10px; border-bottom: 1px solid #ccc; background-color: white; }
#msgs { padding-top: 40px; }
#msgs p { margin: 0; font: 12pt monospace; }
</style>
</head>
<body>
<header>
<button onclick="more()">Start Coroutine</button>
</header>
<div id="msgs"></div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment