Sunday, May 28, 2017

Making a (really simple) game to Learn rust.

So there was a a post on hacker news On making a game in Rust.    In the comments I mentioned it would be nice to have a simple game example to enable people to learn Rust by extending the example.

Rather than sit and hope that such an example turns up I thought I should have a go myself.   The problem with that is, of course, that I don't have a very good grasp on Rust myself.   I wanted the example to give myself a base to learn from.   Nevertheless I'm diving in and we'll see how it ends up.
Bored already? Just download the example and dive in. http://fingswotidun.com/blag/tinydraw_rust/tinydraw_rust.tar.gz

In the responses to my comment someone pointed me to  rust_minifb
which does look like a good place to start.

I got my ChromeBook with Crouton and ran rustup to get me up to date and made a tiny program using rust_minfb.   It didn't work.  A somewhat inauspicious start. 

My code, as little as there was of it, appeared to be fine.   The error was in building rust_minifb.  

"can't find crate x11_dl"  cargo wailed.

But wait! Here it is https://crates.io/crates/x11-dl

The detail in which the devil was concealing itself was the altitude of a single character.

rust_minifb wanted x11_dl
cargo had  x11-dl (note the dash instead of underscore

Had I found a bug in rust_minifb?  I asked on #rust-beginners.   A helpful resident tried


git clone https://github.com/emoon/rust_minifb
cd rust_minifb
cargo run --example noise

and reported that the noise example worked fine for them. 

I tried the same procedure on my ChromeBook and was rewarded with the familiar "can't find crate x11_dl"

At this stage I did what any reasonable person in this position would do.  I moved to my desktop machine.  cargo run --example noise worked, as did my tiny test program.  I could put pixels onscreen. Hooray
rust_minifb also gives us some input. The Window struct provides methods such as get_mouse_pos and is_key_down. We can read input and we can put something on screen, that's two of the main requirements for a game. We still need a sense of time. Turns out Rust makes this bit quite easy.

    let (ticker_tx,ticker_rx) = channel();

    thread::spawn(move|| {
        loop {
            thread::sleep(Duration::from_millis(16));
            ticker_tx.send("tick").unwrap();
        }
    });
That sets up a thread that that repeatedly sleeps for a bit and sends a tick. There are more accurate ways to do this if we want to be in sync with the video frame. but for our purposes, this is good enough. It stops things from running too fast and using all of the CPU. Most importantly, it's nice and simple.
So now we have most of the operating system necessities done. We can get our bunch of u32RGB pixels onscreen, We only need to make those pixels contain what we want to see. That's something we can do purely from within Rust without having to figure out how to talk to the operating system or other APIs. Hardware accelerated rendering would need that, but we're going to just use good-old-fashioned CPU power. The idea is to learn Rust first. Once you have that under your belt you can go and fight in the API wars safe in the knowledge that your Rust is perfect and any further problems surely reside in the OpenGL driver.

What we need are some drawing commands. This is something Rusts traits are good for. We can have a trait that contains drawing functions and then use them on anything that implements the trait. For starters lets have


trait PixelCanvas {
  fn clear(&mut self );  
  fn plot_pixel(&mut self, x:i32, y:i32, color:u32);
  fn draw_line(&mut self, x1:i32, y1:i32, x2: i32, y2:i32, color:u32);
  fn draw_horizontal_line(&mut self, x1:i32, x2:i32, y:i32,color: u32);
  fn draw_rectangle(&mut self, left:i32, top:i32, width:i32, height:i32, color: u32);
  fn fill_rectangle(&mut self, left:i32, top:i32, width:i32, height:i32 ,color: u32);
  fn draw_circle(&mut self, x:i32, y:i32, radius:u32,color: u32);
  fn fill_circle(&mut self, x:i32, y:i32, radius:u32,color: u32);
}
That lets us clear the screen and put a few things on it. These are very much the sort of thing that you'd see in any programming language. Some languages have the self parameter implied. The only distinctly Rust-ish thing about the trait is the &mut self. &mut self means "self refers to a thing that this function might change". That's rather understandable. my_picture.draw_line(10,10,20,20,mycolor) should be expected to change my_picture, that's the entire point of a line drawing function.

Traits can also have default implementations. If you include a function body in the trait definition, it will use that if the trait implementer does not provide an implementation of its own. For the PixelCanvas trait I wrote the most simple naive implementations of drawing functions that use features of the trait itself. So draw_line splits the recursively splits the line in 2 until it is left with a pixel length line then uses plot_pixel, fill_rectangle and fill_circle draw themselves using horizontal_line which in turn uses plot_pixel. Ultimately that means any implementer wanting to use the drawing functions need only implement plot_pixel.

That may not sound like an efficient solution, and it really isn't, but It will work. If you drew a 1000x1000 filled rectangle it would generate a thousand draw_horizontal_line calls which would each in turn call a thousand plot_pixel calls, This would result in a million pixels begin individually clipped and drawn. It's really easy to improve this situation. Any PixelCanvas implementer that provides it's own implementation of draw_horizontal_line would be able to eliminate most of the inefficiency. If you need it to run really fast you can implement every function in the trait with versions targeted at your specific needs. The beauty of this approach is it supplies functionality first while permitting performance later.

As an aside, You may think of pixels as a bunch of little squares. You may vociferously disagree with this notion. Here's a PDF written by someone who doesn't like little squares very much. Really, it all depends on what you are doing.

Pixels can be thought of as either an approximation of an ideal image or it can be the image itself. If you want pixel art, bitmapped fonts, lines that are only one pixel thick and you want to individually address pixels. Then it makes sense to think of the pixels as individually numbered little squares. pixel(0,0) is in the top left corner. For low resolution displays, every pixel is important, you want to be able to directly control them. If you want to turn individual pixels on and off, this is the mode for you.

The other way to look at it is pixels as point samples of a continuous image. This is the world of paths, strokes, fills and filters. The position of the top left pixel becomes a matter of (sometimes fierce) debate (0,0)? (0.5,0.5)? (-0.5,-0.5)?!?. The PDF linked above gives you a run down of the options. In this point of view a line is a mathematical line segment, without a specified width it would be invisible. Giving it a width makes it a rectangle (or perhaps something more exotic if you are using fancy line caps) The pixels themselves get set to an approximation of the shape. Anti-aliasing is usual here, but nice crisp single pixel lines are not, Accuracy in position takes priority so when a line falls between pixels, those pixels share some of the load each and you get a little two pixel smudge. If your pixels are small enough, and you are doing the right blending, and your display is calibrated correctly you might not even notice the difference.

The tiny set of drawing function I provide here are using the little squares approach, for the simple reason that it is much easier to write.

For our first interactive graphical program, We're going to make a program that I always start with when I teach kids programming. The JavaScript version that I use is;

print("Draw with the arrow keys");

var cx=320;
var cy=240;

function move() {
  // the arrow keys have key codes 37,38,39 and 40
  
  if (keyIsDown(38)) {    cy-=1;  }
  
  if (keyIsDown(40)) {    cy+=1;  }
  
  if (keyIsDown(37)) {    cx-=1;  }
  
  if (keyIsDown(39)) {    cx+=1;  }
  
  fillCircle(cx,cy,6);
}

run(move);
I provide a few global functions in the environment that provide the I/O and timing. We have build up a Rust program that provides a similar level of a base, so we should be able to replicate this program. So starting with the rust_minifb example on github. We make a few changes. The TinyDraw project have be downloaded here. Or just view the source files
main.rs
drawing.rs
Here's what the guts of the program looks like.

    let (ticker_tx,ticker_rx) = channel();

    thread::spawn(move|| {
        loop {
            thread::sleep(Duration::from_millis(16));
            ticker_tx.send("tick").unwrap();
        }
    });


    while window.is_open() && !window.is_key_down(Key::Escape) {
        ticker_rx.recv().unwrap();  

        if window.is_key_down(Key::Down) { cy+=1; }
        if window.is_key_down(Key::Up) { cy-=1; }
        if window.is_key_down(Key::Left) { cx-=1; }
        if window.is_key_down(Key::Right) { cx+=1; }

        if window.is_key_down(Key::Space) { frame.clear(); }


        frame.fill_circle(cx,cy,5,0xff80ff40);

        frame.render_to_window(&mut window);
    }
The Loop waits on the timer channel, checks the keys, draws a circle, then puts it on-screen. The frame object is a simple wrapper to the buffer from the rust_minifb example. frame is a FrameBuffer which knows how to put itself onto a window. More importantly, in drawing.rs we implement the PixelCanvas trait for FrameBuffer.

When I use this program as a teaching example. The first step I take after the kids have stopped drawing pictures (and the inevitable WASD conversion that kids these days apparently absolutely require) is to add a clear screen function. That is included in the program above at no extra charge. Clear screen actually has an ulterior motive. Any kid that is playing with drawing where they can clear the screen by pressing space will soon figure out that they can hold space down while drawing. Their eyes light up immediately, a circle is moving around the screen controlled by their key-presses. The drawing program has suddenly metamorphosed into the seed of a game.

And that's where I should probably stop for now. We clearly have not made a game. but there is enough there to make something that could legitimately be called a game. While the drawing functions are not spectacular, they give you a starting point. People wanting to learn Rust from a game perspective can use the seed as a starting point with all of the growth metaphors that may apply to such an endeavour.

Of course I'm going to keep working on this. My next stage is managing a collection of game entities. I don't know how to do that yet, So I'll have to learn a bit more Rust and get back to you later.

No comments:

Post a Comment