Hacker News new | past | comments | ask | show | jobs | submit login
How I made a 3D game in 2 KB of JavaScript (frankforce.com)
376 points by pjmlp on March 9, 2020 | hide | past | favorite | 78 comments



The first ideas of this 3D road game was created in just 140 characters (or less) and posted here (Dwitter) by him (Frank Force's nickname is "KilledByAPixel"): https://www.dwitter.net/h/road And many other tiny 3D effects and engines in JavaScript were created by Dwitter's users, like these demos: https://www.dwitter.net/h/d3


Hi Rodrigo!

Just wanted to add... Dwitter is awesome for learning stuff like this, since everything is so absurdly small you know the ceiling of time required for understanding a dweet is pretty low - yet at the same time it's surprising how advanced techniques can get in this tiny space.

This makes them so tempting. I sometimes struggle to pick up or return to a technical book when work/life etc gets busy, but when someone posts an interesting new dweet I often can't stop myself from pulling it to pieces.


Seems to be struggling a bit. Alternate link: https://web.archive.org/web/20200309074327/http://frankforce...


here is the codepen link from same article >> https://codepen.io/KilledByAPixel/pen/poJdLwB


FYI, there is no jump button. Also, those are rocks, not ramps. Jumping is not a gameplay mechanic in this game.


Double click to jump.


Thanks. Wish the controls were more obvious. After learning this the game is actually quite playable.


"Resource Limit Is Reached", seems a bit silly for a 2kb game ;)


Should call it "How I host my website for $2 a year."


For $2 a year i think you could manage 2kb ;)


You could get the minified code size lower with a few tricks below. This allows the minifier to stop using the longer method names. It also has the benefit of eliminating an object property lookup which is significantly slower than accessing a variable (under a microscope of course, if called many times). Probably 5-10% savings by doing this for the Math methods.

1. Create variables for any global/builtins (setTimeout/clearTimeout, requestAnimationFrame, etc) at the top of the file.

2. Create variables for global object properties, especially if used multiple times or they have longer names (Object.assign, Object.hasOwnProperty, Math.cos, Math.sin , JSON.stringify).

3. For non globals, within functions, create variables for any object properties or methods that are called multiple times to be worth it; like in formulas where obj.width/height is called 4-5 times, the minifier can't optimize the property name.

4. For Class instance methods that don't allow creating variables for methods (You have to call the method with the class, since they operate on "this" context) but don't have anything unique about "this", you can create a variable once within a function, then just bind itself. If you are using it multiple times to be worth it.

  const classMethod = ClassInstance.classMethod.bind(ClassInstance)
would be minified to

  const Z = X.classMethod.bind(X)
and (in the minified code) used as Z() instead of X.classMethod()

5. In longer functions that use "this.whatever" many times, you can create a variable once for "this" (t or _this etc) and the minifier will be able to eliminate those bytes.


This is very impressive!

Lots of counterintuitive observations when you realize the objective is for compressed 2k size, which means that duplicate code is actually a good thing!

It reminds me of this 3d maze in 1k of javascript from many years ago: https://js1k.com/2010-first/demo/459


Thanks, I also referenced this jk1k racing game in the post...

https://js1k.com/2019-x/demo/4006


I absolutely love projects like this. It's so interesting working within these constraints.

So, js1k is not continuing? On the js1k site I can't see anything stating this. Nothing on the subreddit either. Where is that information coming from?

It's a real shame that this great tradition may not continue, though, perhaps the js2k+ will be the next big thing ;)


Only thing I can find is this tweet from the organizer. https://twitter.com/kuvos/status/1213546333707083778


Love the WASM remark on his tweet.

Think WASM coding like the original Demoscene demos.


Not sure about the 2k+zip, but I do think that there will be a LOT of progress in wasm+canvas for interactive programming... of course this leaves out accessibility, so hopefully it's done with some constraint depending on the space.


I love these projects, but writing statements like "full HD" and "Realistic driving physics and collisions" is a bit too much unless it is tongue-in-cheek :)


HD because many tiny games like this do not stretch to fill the whole window. It requires extra code to do this and have everything scale properly.


I've never heard "HD" be used to mean "full width"


It's "high definition". Since the graphics are all vector graphics, it doesn't seem to be too far of a stretch. All the rendering will be done at the resolution available.


Do you have an HD monitor? If so the game is in HD. Many tiny games and demos like this are rendered to a smaller canvas and will either not stretch to fill your monitor or are rendered at a lower then HD resolution.


When people use that term for games, it usually means the assets have been reworked to look better.

In this case, I guess that would be the procedural generation of the background and trees, for instance, to give it better quality than simple triangles.


No, it means the assets have been reworked to support a higher resolution.


That's what I said.


I haven't either but as someone who plays games occasionally, the definition makes sense to me (lots of games don't scale well). Definitely not something I'd have guessed, though.


Also in the genre of tiny racing games, this 2004 entry from IOCCC comes to mind:

https://www.ioccc.org/2004/vik1.c


Controls:

Mouse = Steer

Click = Brake

Double Click = Jump

https://github.com/KilledByAPixel/HueJumper2k


In the minified code there are still a lot of Math.PI, Math.sin, Math.cos… could this not be optimised further with something like s=Math.sin…?


In the original article, the author claims that lots of repeated strings turn out to be beneficial insofar as they make compression (gzip or whatever) more effective. According to him, it doesn't make sense to manually de-redundantize code when the compressor will achieve the same gain or better.


I was about to say "well, it's probably a lot of work to verify that s isn't already a mangled variable name" but then I realized that doing what you suggested in the pre-minified code should work, yeah.

So not only are you right, it's actually super easy. Just do this at the top level of your code:

    const {cos, sin, PI /* etc */} = Math;


You could do that, but it would not compress as well. For example the word const is not use anywhere in the 2k version, while Math. is used about 40 times so it becomes essentially free when compressed.


> For example the word const is not use anywhere in the 2k version, while Math. is used about 40 times so it becomes essentially free when compressed.

Except that the word const does seem to be used in the uncompressed version, so we don't know what happens during compression in the Closure compiler, no? Or is the 2k version pre-Closure compiler different from the uncompressed version?

Either way the only way to know is to try because asymptotically free is all well and good, but without measurement we don't know the actual behavior in practice and we are just making assumptions about how well or how badly things compress and how the mangling process works out.


Ok, I just tried it. Removing every Math. and adding let{cos,sin,PI,min,max,tan,atan2,abs,ceil}=Math; to the top. It worked better then I expected, though it is still 1 byte larger then without. Could potentially be a win in some situations.


Nice! Thanks for trying!

> it is still 1 byte larger then without

That's actually pretty cool, because together with your earlier remark of ~40x occurrences of Math.<whatever> that means we have a ballpark figure for where the tipping point is!


It gets zipped so the duplication goes away. In fact, repetition is deliberate to get better compression.


And thanks to this article I've learned about the Google Closure compiler: https://developers.google.com/closure/compiler


Is it still relevant and updated nowadays? IIRC that one's been around for at least a decade already.


Absolutely. It took a long time for everyone to catch up on basic features (Closure had modules figured out while the rest of the JS community still had several competing standards), but it’s still tremendously effective.

The only downside is the compiler itself runs on the JVM, and most developers ostensibly prefer a single platform. Given how much other software you need to make a development environment work, this seems silly to me.


A real downside and the reason it never took off, is that you have to annotate all your Javascript code to really leverage the Closure compiler.

Google also doesn't really advertised the use of Closure, you have to go out of your way to find it in their developer website or google it to find it. It's really just an internal tool.

They break backwards compatibility in the Closure Library so much that projects that adopted Closure (like Clojurescript), are stuck with a library from 2017. The project is maintained only to serve Google.


> They break backwards compatibility in the Closure Library so much that projects that adopted Closure (like Clojurescript), are stuck with a library from 2017. The project is maintained only to serve Google.

Is that the case? The current version of ClojureScript depends on org.clojure.google-closure-library 0.0-20191016 – I know it's a fork, but I'm not sure to what extent it is being synced with upstream.



There is a JavaScript port of the Closure compiler, available from npm: https://www.npmjs.com/package/google-closure-compiler


ClojureScript leans heavily on google closure.


yes, but since we are talking about size, a hello world Clojurescript is going to be at least 80KB or greater.


I made a 2D game in 3 KB of JavaScript. Not quite as impressive I suppose.


In the final, minified code there's a lot of calls to Math methods (sin, cos, tan). I wonder if there's potential to shrink this even more with some assignments:

    M=Math,s=M.sin,c=M.cos,p=M.PI,a=M.abs; // etc.


That's what minifiers/uglifiers do? Perhaps OP did not want to use them.


As far as I know minifiers don't do this since it changes semantics. Take this for example:

    // 1
    doSomething();
    console.log(Math.sin(2));

    // 2
    var s = Math.sin;
    doSomething();
    console.log(s(2));

Now, consider this definition for `doSomething()`:

    function doSomething() {
      Math.sin = x => x
    }

Now, the first prints 2 and the second snippet prints 0.9092.

Keeping track of globals to get around this is complicated. Even if you do, you can't always be sure the behaviour of the code isn't changing since `doSomething` could be defined in a module your minifier doesn't know about.


Could alternatively make use of `with`


It looks great, runs smoothly but I'm not having a gameplay experience.


What do you mean by a 'gameplay experience' in this case?


I don't want to hate on the creation at all but its hard not to sound like that in text.

On commodore 64 games tried to have nice graphics, nice music and interesting mechanics. Lots of games failed at 1 of the 3. Sometimes they did really well with the other challenges but with few exceptions it doesn't make up for it.

I think if 2 kb is the challenge the game mechanics are the best hope to make up for the lack of sound and visuals.

Reminds me of burning rubber. A game with truly ugly graphics and terrible music that is quite hilarious to play.

https://www.youtube.com/watch?v=nKSbOyGy_Qo


...which is a shameless clone of Data East’s Bump ‘n’ Jump/Burnin’ Rubber, which had much better art and sound. https://m.youtube.com/watch?v=99gnxjpeL7s


haha, so they kept only the game play. It makes a lot more sense now.


Why are there trees in them middle of the road? ;D


Alien trees...


Nice ! :) Looks like a raycasting engine like this one https://www.playfuljs.com/a-first-person-engine-in-265-lines...


Nope! It is not a raycasting engine at all, but instead built similar to old school racing game tech.


Could you help us understand how it's not ray casting? OP doesn't mean ray tracing by the way. Ray casting is the tech used in Wolf3D for example, or maybe I'm preaching to the choir.


Since the car only goes forward, there's no reason to have any depth or visibility testing, things are just rendered in order (painter's algorithm).


With ray casting engines for each column of pixels you cast a ray and collide it against world geometry to see what it hit and where.

With this engine, and most modern 3d engines you apply a transform to 3d world space points to get a screen space point. You also need to sort objects by distance or use a z buffer.


Makes you wonder if the 70gb installs of modern games are really necessary.

Also, obligatory kkrieger link: https://en.wikipedia.org/wiki/.kkrieger


It's an impressive project, but it shouldn't make you question why games are so large these days.


I think there is a middle ground of many indie games I see on steam that are much larger then they should really be.

As a dev, one problem I saw often is UE4 is massive, well over a hundred meg for just the base engine.


Binary size is a drop in the ocean most of the size of game installs is in assets.


While true, you do have to ask how the hell there could possibly be 100 MB of just code.

Consider Windows calculator. In Windows 10, it consumes 15+ MB of RAM. When desktops have 8+ GB of RAM, 15 MB is essentially nothing. And yet, 15 MB is an astronomical amount of RAM considering the functionality Calculator offers. There's really no reason it should be more than a couple hundred kilobytes.


I didn't have a look at the code but the binaries aren't just code. Apart from things like embedded icons and stuff, there can be lots of engine embedded resources inside;


You can actually look at the UE4 source and see.


There is a reason for that, which is that UE4 has an absolutely massive amount of features out-of-the-box compared to most other engines.

If you were to add all the stuff from the Unity marketplace you need to be equivalent to a base UE4 install, it would probably approach that size as well, presuming all those features were even available.


My non-expert opinion is that the size of most game installs is tied directly to the amount of voiced dialog in the game, and in a similar context, how many languages that voiced dialog has been translated and included in the install.


Makes you wonder if the nearly half gigabyte software kits are really necessary to make a simple website. I am looking at you Angular and React.


The game is pretty fun and playable for its size. Nice work!


damn,

I was reading the blog post and mistakenly refreshed the page. Now, BW is exhausted. I didn't know...


resource limit reached.....


It reminds me my 3D demo in 47 lines of JS (without WebGL) :) https://jsfiddle.net/06L845jx/86/

And 54 lines of JS: https://jsfiddle.net/06L845jx/127/


> It reminds me my 3D demo in 47 lines of JS

How exactly?


That's cool! Reminds me of pavel's 1 line cube tornado.

https://www.dwitter.net/d/14485




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: