Down the Ruby Rabbit Hole
Written on Nov. 12, 2012, 8:51 a.m.
A somewhat casual gamer, I've always been intrigued by what makes a good game tick. Strong characters and fluid gameplay are key, but what makes these things work the way they do? I'd known for a long time that video games are essentially massive blocks of code, but how exactly does each part contribute to the whole? Seeking answers to these questions, I started searching for a simple game development library that I could use to learn what goes on behind the scenes.
I stumbled on Gosu the other day whilst tinkering around with bits of code and trying to figure out what I wanted to learn next. Gosu is a library designed specifically for 2D game development (think scrolling platformers), available for Ruby and C++. It seemed like it could be an interesting next step into my exploration of coding and how it works, so I downloaded the Ruby gem and got to work finding sprites and backgrounds for my "game".
Getting It Together
I've always loved anything and everything Alice in Wonderland, so it only made sense that my first game would be based there. It's important to pick something that you won't get tired of after a short while, since you'll probably find yourself opening and reopening your work quite often, annoying soundtrack included. The sky's the limit here.
Lastly, this tutorial requires a basic understanding of the ins and outs of Ruby — Why's Poignant Guide to Ruby is great for this. If you're planning to play along, you'll want to make sure you've got Ruby, and then install Gosu (gem install gosu).
Setting the Stage
What is a Library?
Code libraries are packages of pre-written code that you can access and use in your own projects. A typical library might include classes and functions with pre-defined characteristics.
Before we can even think about "drawing" our objects, we need to create a window for our game to load in. We do this by creating an instance of Gosu::Window, labeling it as GameWindow and defining its size and title.
This will create a 416x369 window where you can place objects. I chose these dimensions because they match the size of my background, but feel free to make it as big (or as small!) as you desire. GameWindow.new.show just creates a new window and shows it:
Next, we'll need to draw objects we want to appear in our game — I used the following things, but you can theme your game however suits your fancy:
- A background (.png)
- A soundtrack (.ogg)
- The Cheshire Cat
The background and soundtrack are properties of the game, so we'll want to load those first. This can be done by calling Gosu's built-in Image and Song functions and using them to create new instances, which we store as @background_image and @music, respectively. The self attribute indicates that these items should load in our GameWindow, and we've set our soundtrack to loop for maximum Wonderland goodness. We also need to tell the game engine to draw our background image, so we introduce a "draw" method as well.
For now, we've only got a background and a bit of music — boring, but we'll add more later.
Now that we've got our background all smoothed out, we can move on to more fun things, like creating our characters (Alice and the Cheshire Cat), specifying their actions and having them perform them. This is a three-part process: We first have to create a class for each character, define the methods (actions) and properties of a given character, and include them in our game's run loop. Don't worry if this sounds confusing now — it'll be easier to understand when you've got some characters and code in front of you to mess around with.
First up is Alice, who'll be controlled by our user's arrow keys. We begin by defining her properties — which image(s) will represent her and where she will be placed when the game starts:
I'm a bit nit-picky, so I wanted to be able to tell at a glance which way Alice is facing — @left_image and @right_image accomplish this through calling the same Image function that we use to load our background. When the game initializes, Alice starts out facing left (@is_facing_left = true) and stays that way until she is moved right (when @is_facing_left = false).
@x, @y and @z indicate where an instance of Alice will be placed when it is created — these are coordinates, so setting @x and @y to 0,0 will put her in the top left corner of the screen. Change these values to something that fits your needs, but remember that @z will always be equal to 1, since we're working in a 2D plane.
If you make any of these values larger than the size of your window, Alice will still load but you won't be able to see her.
The (window) indicates that Alice should be loaded into your GameWindow instance. This is a small but important detail — leave it out, and your game won't work.
If you try saving this then running your game (ruby game.rb), nothing happens. This is because even though we've specified how Alice should be loaded, we haven't actually written any code indicating she should be drawn (created) in our GameWindow. We solve this by adding the following method to our Alice class:
This snippet essentially indicates that if @is_facing_left is set to true, the image of Alice facing left should be drawn — if @is_facing_left is set to false/is not true (she's facing right), then the image of Alice facing right should be drawn at the appropriate coordinates.
With the above methods, we're able to draw Alice (call an instance of the Alice class) — but she is stuck in one place. We remedy this by defining methods that specify how far and in which direction she will move when a given arrow key is pressed (detecting which key is pressed is something we'll do shortly). It's worth noting that we still won't be able to move her after we do this, but this step is vital in enabling our player to control her with arrow keys. Here's a method that specifies what happens when Alice is moved left:
Alice will move 4.5 pixels left each time the function is called (by pressing the left arrow key) until she reaches the left edge of the screen (when @x = 10). Since she's moving to the left, @is_facing_left remains true.
The function for her to move to the right, up and down are all fairly similar:
Although Alice will always move 4.5 pixels in her chosen direction, the wording of each direction method will shift depending on which way she is going:
- When Alice moves to the right, @is_facing_left becomes false, which triggers the drawing of @right_image instead of the default @left_image.
- When Alice moves up or down, her @y coordinate changes but @x remains the same.
- The @y's value can only fall between 205 and 276 — the top and bottom coordinates of the grassy area where it makes sense for Alice to be — we don't want her airborne.
That's it for Alice! We've defined where she appears, where she can move to and how she looks. Here's the finished code for her class:
As you can see above, we've also added x and y attr_accessor properties to the beginning of the Alice class. This allows the game window to handle detecting how close Alice is to an edge by accessing the x and y properties of an instance of Alice.
Alice and Game Loops
What is a Game Loop?
Game screens are constantly changing, so things need to be constantly re-rendered. Game Loops enable this to happen — they run 60 times per second and assess what is currently on the screen, redrawing or removing anything that shouldn't be there. Any drawing to the screen and most characters' actions are contained within the game loop.
We have Alice, but we need to get her wired up to the game loop so that she works in tandem with everything. The game loop is always running in the background (and thus exists already) — even if you can't see it, the screen is being re-drawn and updated 60 times per second. That said, it doesn't know what to do with your objects — we still need to define an update method that is run on each iteration of the Game Loop. This newly created method specifies which keys will make Alice do what: Since we've already defined how Alice will move, this is fairly straightforward...
If the left arrow key is pressed, we call our @alice_move_left() function, which indicates that Alice should move to the left until she is 10 pixels away from the left edge of the screen. The rest of the direction methods follow the same format — GameWindow currently looks like this — notice that we've created our instance of Alice in our initialize method:
The Cheshire Cat
Having an Alice sprite that we can move around the screen every which way is all well and good, albeit somewhat boring. I figured I'd spice things up a bit by adding the elusive Cheshire Cat to the equation — in my game, he wakes up when Alice approaches him and falls asleep when she leaves. Simple, but kinda fun to play around with.
We begin by creating a CheshireCat class in which to define our initialize and draw methods. initialize specifies which images will represent each of the Cheshire Cat's states of awakeness and where he should be positioned. It's similar to the Alice's initialize, but with a few more properties:
There's a simple if-then statement that checks to see whether @should_be_awake is true — if it is, @awake_image and @laugh_image are drawn, the Cheshire Cat's eyes open and a speech bubble (cat_laugh.png) appears at the top of the screen. In order to allow the Game Loop to change the Cheshire Cat's state of awakeness, we added attr_accessor to the appropriate properties.
I've already decided that I want the Cheshire Cat to wake up when Alice approaches him, so we need to create a way to detect an oncoming collision. This can be done by comparing Alice's position to that of the Cheshire Cat. This is done inside the GameWindow's update.
With all these numbers, this might be a bit confusing, so let's break it down into something a bit more digestible:
- @alice.x and @alice.y refer to @alice's x and y coordinates, which are controlled by the user
- @cat.x refers to @cat's x and y coordinates, which have been set to 350 and 224 by initialize method in CheshireCat
- 25 and 15 are the number of pixels away from the Cheshire Cat that Alice has to be before @should_be_awake will change to true and he'll wake up.
If Alice is 25 pixels to the left or right of the cat's position, and is within 15 pixels of the cat's feet, he'll wake up and @should_be_awake will become true, triggering @laugh_image and @awake_image to be drawn instead of @sleep_image during the next Game Loop iteration. The "and" in this statement ensures that Alice is actually close to the Cheshire Cat when she wakes him up — without it, she could be at the bottom of the screen, 100 pixels under the cat and he'd still wake up.
Putting on the Show
That's pretty much all you need for your game to run — here's a quick peek at what your code should look like thus far, if you've used the same images, class names and functions as me:
Note: The last four lines enable your player to quit the game by pressing their "esc" key, which can be useful if they're playing fullscreen or just want to make things easier.
So that's all there is to it! You've just created a functional game in Ruby, complete with characters that interact with each other. Feel free to play around with your code a bit, adjust edge detection tolerances and watch the Cheshire Cat wake up from all the way across the screen — or add a new character, like the White Rabbit. Perhaps even package the game to show it off. That wasn't so hard, was it?