Created
July 26, 2020 13:00
-
-
Save zenmumbler/e11d81d1c35901aae33ced33900dd6f8 to your computer and use it in GitHub Desktop.
Basic Unity-like Coroutines in ES6
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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