I made something kinda interesting this week in JavaScript. It probably qualifies as “over-engineering”, but it was for my use, to meet my needs, and I had making it, and I’m having fun using it (so there! :-p).
It’s an implementation of measurable units of various types that I tend to use in my recent JavaScript-games-making excursions: such as seconds, milliseconds, pixels, radians, and frames. It lets you express that quantities are in rad/s, f/s, px/s^2 (for acceleration values), etc, and convert between them via multiplication and division (px/s divided by a time value results in px/s^2 (pixels-per-second-per-second), or if multiplied produces a value in number-of-pixels). It throws an error if you mix up which units you meant to use, which helps enforce the sanity of the arithmetic you’re performing.
Usage looks something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
var U = MajicUnits; // Spelled funny to match my initials: MaJiC // How fast the ship spins when you press the "right" arrow key: // half a revolution per second. rotationSpeed = U.radians( Math.PI ).per.second; currentRotation = U.radians( Math.PI/4 ); // eighth-turn // Acceleration when the user presses the "up" arrow key: thrustAmount = U.pixels( 6 ).per.second.per.second; currentVelocity = U.pixels( 0 ).per.second; var nowTime = U.now(); // Equivalent to nowTime = U.seconds( (new Date).valueOf() ); var delta = nowTime.sub(lastTime); // How much time elapsed since previous frame delta.as( U.millisecond ); // -> e.g. 40 delta.as( U.second ); // -> e.g. 0.04 var frameRate = U.frames( 50 ).per.second; // 50 f/s var secsPerFrame = frameRate.inverse; // 0.02 s/f (20 milliseconds-per-frame) currentVelocity = currentVelocity.add( thrustAmount.mul( delta ) // convert px/s^2 to px/s, scaling for one frame ); currentRotation = currentRotation.add( rotationSpeed.mul( delta ) // convert rads/s to rads, scaling for one frame ); rotationSpeed.as( U.radian.per.second ); // returns the number value rotationSpeed.as( U.radian ); // throws an exception because you're wrong about the unit type rotationSpeed.valueOf(); // Error. Don't try using as a plain number. rotationSpeed.toString(); // -> "3.141592653589793 rad/s" rotationSpeed.relax(); rotationSpeed.valueOf(); // Okay. .relax() enables straight use as a number. var r = rotationSpeed.mul( U.seconds(1) ); // X rad/s * Y s = XY rad r = r.div( U.radians( Math.PI / 2 ) ); // XY rad / Z rad = XY/Z units r.valueOf(); // Okay! Value is in base "unit", so can be treated as a normal number (without .relax()). U.addUnitType("action", "actions", "act"); // Add a new unit type, unassociated with others U.addUnitType("minute", "minutes", U.seconds(60)); // Add a new unit type, exactly equivalent to 60 seconds var bar = U.actions( 120 ).per.minute; bar.toString(); // "2 act/s" |
The reason for this little library is that I’ve frequently run into problems thinking in the wrong units in my games. One variable might hold a length of time in seconds, another in milliseconds (from new Date()), and I’ll try to use them together, forgetting to convert between them. In MajicUnits, they’re all represented in seconds under the hood, and I can specify what unit I want when pulling them back out.
Or I often forget to scale a velocity or acceleration by the inter-frame time before adding to current state, resulting in a full second’s acceleration taking place in a single frame. Using MajicUnits, I’ll see an exception thrown if I forget to multiply by the frame delta.
It’s the sort of stupid error that led to NASA losing its $125 mil Mars orbiter because one group coded for metric units while another used imperial. If you have a language to express (and enforce!) unit types being used, you’ll catch the problem. Better, of course, if you can do it at compile-time, without requiring the offending code to run before you can catch it, but this is JavaScript, and writing my own to-JavaScript compiler that adds units of measure as strictly-enforced types at the language level would be really drastically over-engineering. 🙂
The library’s pretty crude at the moment: I’ve coded exactly as much as I need for the moment. There are places you could feed it bad input and break its assumptions. It automatically creates plural versions of your added units, adding ‘es’ if it ends in ‘s’, otherwise adding ‘s’. That obviously wouldn’t work with ‘activity’ (-> ‘activitys’), and can’t handle exceptions like ‘forum’ -> ‘fora’ or something. Rather than have an automatic pluralizer, I should be having the user explicitly specify when they register the unit… (All of the above is no longer accurate.)
A bigger problem is that when an exception is thrown I can’t currently just find what line of code went wrong. I throw an Error object (which includes file and line number info), but it points at the place inside the library that threw the exception, rather than at the failing invocation that caused the exception. Most implementations of Error provide some sort of stacktrace, so perhaps I can generate one and then turn it into a string that includes that stacktrace info… because when it comes out on my Firefox console I don’t get the stacktrace currently. Anyway.
You can see the current implementation (as of this writing) here (UPDATED), if you’re curious and/or masochistic (it does some magic with JS prototypes to do its job, and lacks any useful documentation/comments, though I plan to add some).
It’s written as one part of this game Gate Arena that I’ve been working on lately, but am rewriting to emphasize a more declarative style of expressing objects and logic and their relationships, to increase code reuse and the ease of adding new enemy types, etc. I plan to reuse the resulting engine for further games, increasing my flexibility for participation in game jams and whatnot.