Hacker News new | past | comments | ask | show | jobs | submit login
Tetris clone written in Zig running on WebGL and WebAssembly (raulgrell.github.io)
215 points by Hoppetosse on May 6, 2019 | hide | past | favorite | 69 comments



So much cutting edge technology for something that could have been written in BASIC running on a Z80 :)

That being said, Zig is an interesting, well-deigned language. Despite being young, it already has tons of features. And compatibility with the C ABI is a big plus.


...I think it's interesting that WebAssembly moves the "code bloat problem" into focus again, because WebAssembly apps cannot afford to have huge upfront downloads or long installations. They must start in a second or so.

The Tetris example is under 60 kBytes (compressed), and that includes all data as well (there's a sprite/font sheet embedded in the wasm blob). This is in the same general size range as 8-bit BASIC programs.

That means that Zig doesn't need a big runtime to work, similar to C, but unlike C# or Java (or even C++ / Rust, unless you ignore most of the respective standard libraries and restrict yourself to a minimal "embedded-programming" style).

Here's a similar example (shameless plug), a WASM C64 emulator in under 64 KByte download size. This is implemented in C:

https://floooh.github.io/tiny8bit/c64.html

I can (most likely) get to the same result in Zig, but with a much nicer "better C" language (and build system!)

(...of course there's a huge web browser dangling off the WebAssembly blob, but this is mostly runtime components that are not even needed for this type of WASM programs (we really need smaller, more modularized browsers!).


I’m not sure what you mean by your comments around C vs. C++/Rust. All of them have no required runtime. All of them generally have libc supplied by your OS, which is a runtime.

Even in C you must restrict yourself to the runtime your building on, i.e. your target might not have libc. Rust and C++ are the same in the regard that they are capable of doing this.


There was a time when Rust executables came with an embedded jemalloc, but that's been fixed in the meantime (I think). But that's not what I mean in the C++ and Rust case, instead:

Both C++ and Rust make it quite easy to "accidentally" add bloat indirectly through their standard libraries because they encourage high-level abstractions which may "unfold" into a lot of binary code from very little source code.

It can be avoided but requires to give up on many of the high-level language features which Rust and C++ differentiate from (for instance) C.


I can speak better for Rust in this regard, so I won't comment on C++, but I also don't work with #[no_std] often so apologies if anything is inaccurate. jemalloc has been optional in nightly ever since I started using the language in 2015. It switched to the system allocator in the 1.28 release, Aug 2018, it also stabilized the ability to define a custom Global Allocator[1].

> Rust make it quite easy to "accidentally" add bloat indirectly through their standard libraries

This isn't accurate, the #![no_std] feature disables stdlib in your library/binary. It requires you to implement a few things, but it will not compile if you use things in std [2]. You may be interested in reading the embedded book[3] which discusses this area in a lot more detail.

> give up on many of the high-level language features

I also would disagree. You still have a powerful type system, match statements, RAII, hygienic macros, proc_macros, all of the core library[4], optionally have access to the alloc crate[5], cargo, simple unit-test integration, and a growing set of #[no_std] libraries with easy access through crates.io or git dependencies. There's a lot more of course. Point being, there are still a lot of higher-level language features available even in a #[no_std] context, and it's very easy to ensure that your binaries won't compile if this is a requirement and someone tries to pull something in that relies on std.

[1]: https://blog.rust-lang.org/2018/08/02/Rust-1.28.html

[2]: https://doc.rust-lang.org/nomicon/beneath-std.html

[3]: https://rust-embedded.github.io/book/intro/no-std.html

[4]: https://doc.rust-lang.org/core/

[5]: https://doc.rust-lang.org/alloc/


It’s not much of a problem once you have code splitting.

React supports code splitting, which lets you split large amounts JS into separate production JS files; load only what’s necessary at first, and load the rest as-needed: https://reactjs.org/docs/code-splitting.html

With a new programming language (like Zig), I would hope that they eventually try to have code splitting be automatic and transparent.

For JS targets, you create multiple smaller final JS files instead of one giant JS file. For native targets, you could break out extra code into dynamically loaded .so or .dll files. The real challenge is making this whole process as painless and transparent as possible for the developer.


Compiled languages usually have dead-code-elimination (only code that's actually "reachable" is included)... I think the JS world invented the word "tree-shaking" for that.

Not loading everything that's eventually needed at startup can be handled with loading WASM modules on demand (which can be handled the same way as loading DLLs in a native application).

IMHO it's better to fight code bloat at the root, by picking a language without runtime, minimizing dependencies, and picking the right dependencies.


> .I think it's interesting that WebAssembly moves the "code bloat problem" into focus again, because WebAssembly apps cannot afford to have huge upfront downloads or long installations. They must start in a second or so. The Tetris example is under 60 kBytes (compressed), and that includes all data as well (there's a sprite/font sheet embedded in the wasm blob). This is in the same general size range as 8-bit BASIC programs.

Original Tetris was under 32k, and competition implementations since have been under 512 bytes.

60K plus webkit plus OS is plenty of bloat still



> Despite being young, it already has tons of features

Zig is aiming to be lean-and-mean, not feature-rich, but I take your point. I agree it's very impressive how quickly it's come this far.


Didn't know the Zig Programming language before, but its website https://ziglang.org is really an outstanding example how a programming language should be presented: Many examples, comparisons with other language, and even more examples. Code code code.


IIRC this is because it gets posted here every month and there is always someone complaining about those kinds of things and they addressed those points.

It also gets brought up a bit with the Pharo Smalltalk website(lack of code snippets), but Smalltalk is such a radically different language than what we're mostly familiar with (C, Java, Python, JS) that showing a code snippet is mostly pointless. People try to bring this up, but to no avail.

Sometimes the syntax just isn't very important. I think in Zig's case though, that it is fair to ask.


It looks like Zig has some decent documentation (something I treasure), but it looks like file I/O is missing. I'm sure it assumes the user is at least intermediate with C (I can only get by with lots of googling). I'll give Zig a try if someone can put up an example of reading a .CSV file (something I do a lot and wish I had faster tools to do) and if it is less of a hassle than C/C++.

https://ziglang.org/documentation/master/

The install seems to also be a lot more straightforward than many of the other low level languages that I have tried.


They have several examples of reading a file etc, e.g.:

  var file = os.File.openRead(arg) catch |err| {
                  warn("Unable to open file: {}\n", @errorName(err));
                  return err;
              };
              defer file.close();
The rest should be basically string operations...


I'm not sure how I missed this, but thank you for bringing it to my attention. It seems straightforward enough. I should be able to figure out the string operations.


It's also very straightforward to just link to libc and use fopen()/fread()/fwrite()/fclose()/fflush(). That's how I do it with my own (much less mature) language.


Could you be so kind as to point out where you found this example please? I could not find it in the documentation link in parent post.


I/O examples such as that already exist on the Zig webpage, e.g. here:

https://ziglang.org/#A-fresh-take-on-error-handling

But the one I posted is from GitHub, where it has e.g:

https://github.com/ziglang/zig/blob/8139c5a516eaa217ed76acdf...

also:

https://github.com/ziglang/zig/blob/92d9cef07116da8639ac33b7...


Thanks! It would be good to get this as part of the official doc.

I can follow along with almost any language if there is enough examples I can piece together. I didn't think about scrolling through a bunch of GitHub pages. It isn't as convenient, but probably a better example of how to put together a full program as well as style.


Yeah, Zig needs a "Zig by example" (akin to the Go and Rust by example pages).


The web page bobs around when pressing the buttons, when the game area is smaller than the window. Is there any way to make that stop aside from making the area smaller, so that it fits the window?


You could run something like this in the console:

    window.addEventListener("keydown", function(e) {
        // space and arrow keys
        if([32, 37, 38, 39, 40].indexOf(e.keyCode) > -1) {
            e.preventDefault();
        }
    }, false);
That will prevent the scrolling when pressing the arrow keys. It's of course not a perfect solution, and something more robust would be to check if a game is currently running and so on, but it works as a quick hack.


Thanks for the feedback. I've reworked the page so that it's less likely that the game does not fit in a screen without scrolling - not ideal, but it'll help!


What is surprising to me is that adding the shadow of an element (ie. where exactly it will land) made the game more difficult to me because I constantly felt like running out of time.


After playing tons of hours on jstris I can't live without it


Same here. No matter how much I've been trying to convince myself that this is just a tip from the game...


I keep looking at the shadow rather than the block.


Hmm... [Error] TypeError: null is not an object (evaluating 'gl.viewport') (env.js:17) [Error] Unhandled Promise Rejection: TypeError: import env:consoleLog must be an object promiseReactionJob

Looks like not working in Safari 12.1 in macOS High Sierra :-(


The project uses WebGL2, which isn't supported on Safari. I guess it's just an oversight, supporting WebGL1 is probably trivial.


The title should mention this, then. WebGL and WebGL2 aren't the same thing.


...well the difference only matters on Safari, and it is indeed surprising that Apple isn't able to provide a WebGL2 implementation after such a long time of having a (pretty much stalled) WebGL2 WIP version in their browsers.

IMHO it's understandable that the author assumed that WebGL2 is available everywhere.


It matters on more browsers than Safari. Firefox doesn't support WebGL2 on my system for example. I think it's because it uses an older version of Angle.


would you consider implementing a tetromino bag like the one described here[1], I think is common among the most popular tetris implementations, it affects a lot the game strategy, I think would make it more enjoyable.

[1]https://tetris.fandom.com/wiki/Random_Generator


This WASM version is based on my native code implementation which implements something like this.

I believe I independently discovered this concept of a tetromino bag. I've been calling it "Gambler's Accurate Model of Reality" in a homage to Gambler's Fallacy.

https://github.com/raulgrell/tetris/commit/4305cd1ac1bcc9443...

That commit is from Feb 7, 2016, and you can see all the TODO comments for the zig features that didn't exist yet :-)


   Uncaught TypeError: Cannot read property 'viewport' of null
    at env.js:17
(anonymous) @ env.js:17

   Uncaught (in promise) TypeError: WebAssembly.instantiate(): Import #0 module="env" error: module is not an object or function


   Chromium Version 74.0.3729.108 (Official Build) Arch Linux (64-bit)


I think enabling chrome://flags/#enable-webgl-draft-extensions fixed it


Safari doesn't support webgl2 (you can enable it as an experimental feature in the develop menu), so retrieving a webgl2 context returns null.


>Chromium Version 74.0.3729.108 (Official Build) Arch Linux (64-bit)

Safari?


Great work, I wanted to know a way to play with WebAssembly and Zig. This is perfect.

Do you know how easy/difficult would it be to make a desktop version of it ?

One of my long term goal is to make it cross platform.


This is actually a port of the demo made by Andrew Kelley, which was originally written for the desktop using GLFW.

My plan is to integrate my changes into the original project so that it can export to desktop and web.


Wow, that's great!

I'm following you on Github then.


Do random rows get pushed from the bottom on purpose? It looks like a bug.

It's still fun though. :)

Edit: It's definitely on purpose.


I thought it looked like a bug, but it's a feature based on which level you are on and how many lines you've filled(?). Here's the part that decides if it should fill or not: https://github.com/raulgrell/tetris/blob/master/src/main.zig...


I've seen that behaviour in other Tetris games as well. :)


I noticed this too, but it correlates when you go up a "level" - so I assume it's a feature to add random rows each level.


More than one person has mentioned this. Maybe an audio cue will make it more obvious it's on purpose...


Bug report: The game mostly work, but sometimes it builds bogus rows on the bottom. Makes it more difficult to get high points. Debian Linux with Chrome...


I can't tell if you're joking or not, but that's not a bug, that's just what happens in some versions of Tetris.


OK. Not a bug, a feature. :)


55.4kb gziped main.wasm including graphics! Impressive!

Works smoothly.


Is there a LICENSE file or license header somewhere?


I forked a project, didn't even realize it didn't have a license. I'll ask the original creator to decide.


None that I can see.


Nice in Firefox, but my Chrome (Version 73.0.3683.103 (Official Build) (64-bit)) on MacOS does not show the game "frame".


Works on mobile except for the keyboard input :)


How is the compilation to WASM/WebGL realized here? Asking out of curiosity, but don't have a lot of time to research.


As far as I can see:

Compilation to WebAssembly bytecode is done via the new WASM backend in LLVM (the Zig compiler sits on top of LLVM).

The whole interfacing with WebGL and WebAudio is actually quite interesting. It's not done through a runtime provided by the compiler SDK (as it is usually done with emscripten), but instead the Zig-to-JS binding is all done "manually" in the project itself.

There are 2 small "shim files" for WebGL/WebAudio and DOM access written in JS here:

https://github.com/raulgrell/tetris/blob/master/env.js

and here:

https://github.com/raulgrell/tetris/blob/master/dom.js

...and associated Zig interop files here:

https://github.com/raulgrell/tetris/blob/master/src/webgl.zi...

and here:

https://github.com/raulgrell/tetris/blob/master/src/dom.zig

It's really surprisingly little (and clean) code for doing that sort of thing IMHO.


Zig has wasm as a built-in compile target — derived from LLVM 8 so Clang has the same functionality. The link to Dom/OpenGL is still a manual process but there’s some efforts generating the necessary JavaScript helpers.


I noticed a bug where you cannot rotate an item next to a wall if you have not previously rotated it.


Not working on Firefox?


Works for me.


How do I run this?


Nice work.


666


Is that some kind of satanic incantation or just an HTML color code?


Really great.

Do you know of any good resources for 2d game programming in WebAssembly?


If you are not against using an engine, GoDot (open source) is great. It can target web using WebAssembly.


GGEZ looks good. https://ggez.rs/


Maybe in the future. wasm is still not supported target, see https://github.com/ggez/ggez/issues/71




Consider applying for YC's W25 batch! Applications are open till Nov 12.

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

Search: