Keeping it Small and Simple

2008.02.26

Rubygame tutorial #4: Like wow, a re-usable pixel

Filed under: Rubygame Tutorial — Tags: , , , — Lorenzo E. Danielsson @ 19:05

Welcome to another Rubygame tutorial. This time we will create a more generic moving pixel so that we can re-use it. We will also allow the pixel to make some noise if it crashes against a border.

I am going to go through different parts of the program. At the end I will put it all together.

Initialization

First off all, since we will be using sound in this application, we need to initialize the sound sub-system. To do that we can do as follows:


1 Rubygame.init
2 Mixer::open_audio
3
4 screen = Screen.new [500, 500]
5 events = EventQueue.new
6 clock = Clock.new

Most of this should be familiar by now. What is new is the call to Rubygame::Mixer::open_audio. This is required if we want to be able to play samples.

We are also going to create two Pixel instances. This is how we prepare these two.


1 p1 = Pixel.new "Pixel the First", screen
2 p1.color = [255, 240, 0]
3 p1.keys = [K_UP, K_DOWN, K_LEFT, K_RIGHT]
4 p1.crash_sound = Mixer::Sample.load_audio "pop.wav"
5
6 p2 = Pixel.new "Pixel the Second", screen
7 p2.color = [0, 255, 255]
8 p2.keys = [K_W, K_X, K_A, K_D]
9 p2.crash_sound = Mixer::Sample.load_audio "error.wav"

Our new pixel class (which we shall get to shortly, is a bit expanded compared to the one in the previous tutorial). Our pixel now has a name. We give it a name by passing the name in as the first argument to the Pixel constructor. Then we pass in the surface that the pixel will draw itself onto.

Our new Pixel has a color attribute. We can create pixels in different colors, which we do here. The Pixel also takes an array of keys used for moving it. The order of the keys here is significant. You must pass them in the order: up, down, left and right. Finally, the Pixel takes a sound to be played if the pixel crashes against a border. I copied two sounds from /usr/share/sounds/ but you can choose whatever you like. You can even record your own. Just try to keep them relatively short. You wouldn’t want a recording of one of Edgar Allan Poe’s stories to be played every time a pixel hits a borders.

Keep the samples in the same directory as the ruby code, or pass the full path to the sample. The samples should be in WAV format.

The new Pixel class

Now let us look at the new version of Pixel. We have a little bit more information here than in the previous version. First, here is the structure of the class and the constructor.


 1 class Pixel
 2   attr_accessor :color, :keys, :name
 3   attr_reader :hit
 4   attr_writer :crash_sound
 5
 6   def initialize(name, surface)
 7     @name = name
 8     @surface = surface
 9     @x = @surface.w / 2
10     @y = @surface.h / 2
11     @vx, @vy = 0, 0
12     @color = [255, 255, 255]
13     @hit = false
14     @keys = []
15     @crash_sound = nil
16   end
17 end

We have a few attributes that we want to make accessible:

  • color: the color of the pixel
  • keys: the keys used to move the pixel (up, down, left and right)
  • name: the name of the pixel
  • hit: a flag that is set if we just crashed into a border (read-only)
  • crash_sound: the sound to play when we hit a border (write-only)

Don’t worry too much about the fact that hit is read-only and crash-sound is write only. It was a spur of the moment thing. Use the rules that make sense to you, and your needs.

In the initialize method we set up a few other, internal use variables, such as vx and vy which are used for horizontal and vertical velocity. Again, never mind the fact the in previous versions I called these horizontal and vertical direction (dx and dy). That was also a spur of the moment thing. Feel free to do the right thing. Also feel free to define what the right thing is. I felt free to somewhere along the line redefine what the right thing is.

The variables vx and can both have one of three values:

  • 0 means no movement (zero velocity)
  • -1 means upward motion for vy or leftward for vx
  • 1 means downward motion for vy or rightward for vy

If you are not sure of why this is so, think about it for a while. Take into consideration how screen coordinates work and it will be obvious.

Next, we need a move method.


 1 def move
 2   @hit = false
 3   return if @vx.zero? && @vy.zero?
 4   
 5   unless (0..@surface.w-1).include? @x + @vx
 6     @vx = 0
 7     @hit = true
 8     Mixer::play(@crash_sound, 0, 0) unless @crash_sound.nil?
 9     return
10   end
11
12   unless (0..@surface.h-1).include? @y + @vy
13     @vy = 0
14     @hit = true
15     Mixer::play(@crash_sound, 0, 0) unless @crash_sound.nil?
16     return
17   end
18
19   @x += @vx
20   @y += @vy
21 end
22

Every time we are going to move a pixel we need to check if it just hit a border. However, we are victims of a particular idea of justice that says that innocent until proven guilty so we start by setting hit to false. At the same time, if there is no horizontal and no vertical velocity, then there really isn’t much point in the rest of the method.

Next we have two block that check if the next position (horizontally and vertically) is within the screen or if we hit a border. (Note that x+vx will be the position that we are about to move to. I am sure that you can think of 1001 other ways of implementing this, many of which are more efficient. Guess what? That’s an exercise: to re-write the two tests that start with unless statements.

If we have hit a border, we stop the pixel, set the hit flag, play a crash sound and exit the method. If we didn’t exit the method we would get to the end where the pixel is actually moved, which wouldn’t be a good idea. If you are sure why, walk straight until you hit the wall. When you do still walk forward.

So, how about the event method? Remember that the @keys array holds the keys that are used to control this Pixel. So the new method looks like this.


1 def event(key)
2   case key
3   when @keys[0]: @vx, @vy = 0, –1
4   when @keys[1]: @vx, @vy = 0, 1
5   when @keys[2]: @vx, @vy = –1, 0
6   when @keys[3]: @vx, @vy = 1, 0
7   end
8 end
9

Hopefully that isn’t difficult to understand. Last we have the draw method. But that hasn’t changed enough to warrant us looking at it in any detail. So let’s instead look at the whole program.

Putting it all together

Forcing all the little bits and pieces into a single program we get this:


  1 #! /usr/bin/ruby
  2
  3 # Move a pixel around the screen.
  4
  5 require rubygame
  6 include Rubygame
  7
  8 class Pixel
  9   attr_accessor :color, :keys, :name
 10   attr_reader :hit
 11   attr_writer :crash_sound
 12
 13   def initialize(name, surface)
 14     @name = name
 15     @surface = surface
 16     @x = @surface.w / 2
 17     @y = @surface.h / 2
 18     @vx, @vy = 0, 0
 19     @color = [255, 255, 255]
 20     @hit = false
 21     @keys = []
 22     @crash_sound = nil
 23   end
 24   
 25   def move
 26     @hit = false
 27     return if @vx.zero? && @vy.zero?
 28     
 29     unless (0..@surface.w-1).include? @x + @vx
 30       @vx = 0
 31       @hit = true
 32       Mixer::play(@crash_sound, 0, 0) unless @crash_sound.nil?
 33       return
 34     end
 35
 36     unless (0..@surface.h-1).include? @y + @vy
 37       @vy = 0
 38       @hit = true
 39       Mixer::play(@crash_sound, 0, 0) unless @crash_sound.nil?
 40       return
 41     end
 42
 43     @x += @vx
 44     @y += @vy
 45   end
 46
 47   def draw
 48     @surface.set_at [@x, @y], @color
 49   end
 50
 51   def event(key)
 52     case key
 53     when @keys[0]: @vx, @vy = 0, –1
 54     when @keys[1]: @vx, @vy = 0, 1
 55     when @keys[2]: @vx, @vy = –1, 0
 56     when @keys[3]: @vx, @vy = 1, 0
 57     end
 58   end
 59 end
 60
 61 Rubygame.init
 62 Mixer::open_audio
 63
 64 screen = Screen.new [500, 500]
 65 events = EventQueue.new
 66 clock = Clock.new
 67
 68 p1 = Pixel.new "Pixel the First", screen
 69 p1.color = [255, 240, 0]
 70 p1.keys = [K_UP, K_DOWN, K_LEFT, K_RIGHT]
 71 p1.crash_sound = Mixer::Sample.load_audio "pop.wav"
 72
 73 p2 = Pixel.new "Pixel the Second", screen
 74 p2.color = [0, 255, 255]
 75 p2.keys = [K_W, K_X, K_A, K_D]
 76 p2.crash_sound = Mixer::Sample.load_audio "error.wav"
 77
 78 running = true
 79
 80 while running
 81   events.each do |event|
 82     case event
 83     when KeyDownEvent
 84       p1.event(event.key) if p1.keys.include? event.key
 85       p2.event(event.key) if p2.keys.include? event.key
 86       running = false if event.key == K_Q
 87     when QuitEvent
 88       running = false
 89     end
 90   end
 91
 92   screen.fill [0, 0, 0]
 93   [p1, p2].each { |pixel|
 94     pixel.move
 95     pixel.draw
 96     puts "#{pixel.name} crashed!!!!" if pixel.hit
 97   }
 98
 99   clock.tick
100   screen.update
101 end
102
103 Mixer::close_audio
104 Rubygame.quit
105
106

Notice what happens on a KeyDown event. A specific Pixels’s event method is called if that Pixel has registered that it handles that key press. I’ve also made ‘q’ quit the program. I only did that to be cool.

When it comes to the move/draw cylce of the pixels, remember that you have more than one pixel to take into consideration. I only added the console message to find some use for the name and hit attributes. It at least shows that the external world can know whether or not a Pixel hit a border.

Finally, notice that I close the audio sub-system when the application quits. That is a good idea (as in it really, really is a good idea).

Exercises

1. It kind of/really (depending on your perspective) sucks that both the pixels start at the same position on the screen. Add the ability to specify a starting position for a pixel. (Hint: if you start adding attributes or lots and lots of code then you probably have a hormonal imbalance)

2. Add another two pixels to the program. Make sure that each pixels has its own set of keys to control it.

3. Make the program display four pixels as in the previous exercise, but have them all be controlled by the same set of keys. Make sure the pixels all have different starting positions.

4. Re-write the program in such a way that each Pixel gets a random starting location.

5. See if you can get the program to detect when two Pixel objects collide.

6. Add a counter that counts how many times a pixel has crashed. At the end of the program dumps some stats about number of crashes to the terminal.

7. Allow a pixel to move not only horizontally and vertically but diagonally as well. There are two ways to do this, and you should try both. One is that a change in vx should not reset the value of vy and vice versa. The other is that you register more keys (up-left, up-right, down-left and down-right) for controlling the pixel.

8. Instead of using an array to hold the motion keys, use a hash with UP, DOWN, LEFT and RIGHT as hash keys (also the others if you want to add diagonal motion).

9. Make the program well-behaved, as in don’t crash if a sound sample isn’t found. Add some error-handling.

10. Change the draw method to plot four pixels. @x and @y should be a reference point. The first pixel should be drawn 5 pixels above @y and 5 pixels to the left of @x. The second pixel should be 5 pixels above @y and 5 pixels to the right of @x. The third pixel should be 5 pixels below @y and 5 pixels to the left of @x. Finally, the last pixel should be 5 pixels below @y and 5 pixels to the right of @x. The four pixels should now make up the corners of a square. Update the collision detection in the move method to check if any of the points has hit a border. Don’t write more checks than is necessary. Calling the class ‘Pixel’ may not be appropriate anymore.

11. Modify the program in exercise 10 to instead of drawing four corners of a square, the four pixels should make up the four corners of a diamond shape.

12. Write a program similar to mine, but that plays a sample any time the Pixel’s x and y coordinates are both divisible by 5. This sound should be different from the crash sound.

13. Speaking of crash sound, is it really a good idea with different crash sounds for each Pixel? If not, don’t offer a writable attribute to set the crash sound. Hard-code it into the class once and for all.

14. Is it a bit weird to have 3 possible velocities. When was the last time you saw that in the real world. See if you could get the pixel to accelerate up to its final speed instead of going from 0 to 1 in an instant.

15. Expanding further on the idea of acceleration, what if the pixel should accelerate as long as a key is held down (the longer you hold left pressed down the faster it moves in that direction)? Or each time the down button is pressed, it should increase it’s downward velocity?

16. See if you can convince our Pixel to join Pixels without Borders. Any time it gets to the top of the screen it should appear at the very bottom. If it dissapears to the right it should reappear from the left. You know the idea. Make it happen.

18. My program is intentionally raw. Think of ways to improve or worsen it (your choice). Once you’ve understood how everything works together you should be able to re-write the whole program in different ways. You could for instance try to write a version that doesn’t use classes at all. With more practice you become older. Er.. maybe you learn a thing or two as well.

Conclusion

Are you as sick and tired of plotting individual pixels as I am? Good. Next time we will look at other drawing methods. Eventually we will get to more interesting things like being able to

Advertisements

2008.02.22

Rubygame tutorial #3: In control

Filed under: Ruby Programming, Rubygame Tutorial — Tags: , , , — Lorenzo E. Danielsson @ 07:37

Welcome back. Due to (somewhat) popular demand, I’m going to continue the rubygame tutorials. In this tutorial we will look at a keyboard event: KeyDownEvent.

Looking back for a second or two

Let us remind ourselves of the steps we need to take to create a very simple Rubygame program:

  1. Create a screen
  2. Create a event queue
  3. Create a clock
  4. Enter the main loop. The main loop does drawing and contains an event loop.
  5. The event loop should have some exit condition

That is really it. Armed with only this knowledge and a few drawing methods we can do a lot already.

A single pixel

Let’s start off with a program that plots a single pixel. The following program will just plot a single pixel at the center of the screen.


 1 #! /usr/bin/ruby
 2
 3 # A pixel.
 4
 5 require rubygame
 6 include Rubygame
 7
 8 screen = Screen.new([640, 400])
 9 events = EventQueue.new
10 clock = Clock.new
11 clock.target_framerate = 120
12 running = true
13
14 while running
15   events.each do |event|
16     case event
17     when QuitEvent
18       running = false
19     end
20   end
21
22   screen.fill([0, 0, 0])
23   screen.set_at([screen.w / 2, screen.h / 2], [255, 255, 255])
24   screen.update
25   clock.tick
26 end
27

Not too interesting I guess. I single pixel and it doesn’t even move! Let’s change that.

Exercises

1. Modify the program to display a blue pixel.

2. Modify the progam to plot the pixel at the position 100, 100.

3. Change the screen size to 500, 500.

4. Write a program that takes the pixel coordinates as command-line arguments. Provide the center as a default position if no arguments are passed. Give an error if the coordinates are beyond the size of the screen.

Preparing for motion

Let’s give our pixel a life of its own. We will create a pixel class. Here is a new version of the program.


 1 #! /usr/bin/ruby
 2
 3 # Another pixel.
 4
 5 require rubygame
 6 include Rubygame
 7
 8 class Pixel
 9   attr_reader 😡, :y
10
11   def initialize(x, y, surface)
12     @x, @y = x, y
13     @surface = surface
14   end
15
16   def plot
17     @surface.set_at [@x, @y], [255, 255, 255]
18   end
19 end
20
21 screen = Screen.new [640, 400]
22 events = EventQueue.new
23 clock = Clock.new
24 clock.target_framerate = 120
25 pix = Pixel.new(screen.w/2, screen.h/2, screen)
26 running = true
27
28 while running
29   events.each { |event|
30     case event
31     when QuitEvent
32       running = false
33     end
34   }
35
36   pix.plot
37   screen.update
38   clock.tick
39 end

We still only see a single pixel at the middle of the screen. Still no fun! 😦

On the other hand, we now have a pixel that knows its own position and is able to “draw itself” onto its target surface.

Exercises

1. Add a color attribute to the Pixel class. Set the color attribute to red before plotting the pixel.

2. Create five Pixel instances, each with different coordinates and plot each of them.

Now, move it!

So far we have used QuitEvent to determine if the user has closed the Rubygame screen. Now let’s look at another event: KeyDownEvent. This event is triggered any time a key is pressed down. We can find out which key via KeyDownEvent#key. Let’s use this to add the ability to move the pixel around.

We give our Pixel class some new methods: up, down, left, right and stop. Internally, it will also have two new fields: one to determine the horizontal direction and one to determine the vertical direction.


 1 #! /usr/bin/ruby
 2
 3 # Moving pixel.
 4
 5 require rubygame
 6 include Rubygame
 7
 8 class Pixel
 9   attr_reader 😡, :y
10
11   def initialize(x, y, surface)
12     @x, @y = x, y
13     @dx, @dy = 0, 0
14     @surface = surface
15   end
16
17   def up
18     @dx, @dy = 0, –1
19   end
20
21   def down 
22     @dx, @dy = 0, 1
23   end
24
25   def left
26     @dx, @dy = –1, 0
27   end
28
29   def right
30     @dx, @dy = 1, 0
31   end
32
33   def stop
34     @dx = @dy = 0
35   end
36
37   def move
38     tx, ty = @x + @dx, @y + @dy
39     return unless (0..@surface.w-1).include? tx
40     return unless (0..@surface.h-1).include? ty
41
42     @x, @y = tx, ty
43   end
44
45   def plot
46     @surface.set_at [@x, @y], [255, 255, 255]
47   end
48 end
49
50 screen = Screen.new [640, 400]
51 events = EventQueue.new
52 clock = Clock.new
53 clock.target_framerate = 120
54 pix = Pixel.new(screen.w/2, screen.h/2, screen)
55 running = true
56
57 while running
58   events.each { |event|
59     case event
60     when QuitEvent
61       running = false
62     when KeyDownEvent
63       case event.key
64       when K_UP: pix.up
65       when K_DOWN: pix.down
66       when K_LEFT: pix.left
67       when K_RIGHT: pix.right
68       when K_SPACE: pix.stop
69       end
70     end
71   }
72
73   screen.fill [0, 0, 0]
74   pix.move
75   pix.plot
76   screen.update
77   clock.tick
78 end

Exercises

1. Try to comment out the screen.fill line inside the drawing loop. What happens?

2. Currently, the pixel can move in four directions: up, down, left and right. How could you modify the program to allow the pixel to move diagonally. (There are several ways, experiment as much as you can.)

3. Modify the program to create three Pixels. Give each a different starting position. When one of the motion keys is pressed, all three should move.

4. What if you wanted to create three Pixels that each used a different set of keys to control its motion, how would you go about that? Would you make any changes to the Pixel class itself?

5. Rewrite the program to check the KeyUpEvent instead of KeyDown. How does this affect the program?

Conclusion

In this tutorial you have learned how to use the KeyDown event. We will continue with our moving pixel in the next tutorial (hey, I told you I was going to go real slow).

In the meantime, experiment with what you have learned so far. There are loads of things you can do to get extra practice. For instance you could modify the program so that the pixel only moves while a key is pressed down. Once the key is released it should stop (use both KeyDownEvent and KeyUpEvent). The more you practice the better you will become.

2007.12.20

Rubygame tutorial #2: pixels

Filed under: Rubygame, Rubygame Tutorial — Tags: , , , , — Lorenzo E. Danielsson @ 13:10

Welcome to my second rubygame tutorial. This time we will look at pixels, which are the smallest of the building blocks available to you. We will also have a short introduction to timing. At the end of this tutorial you will have a framework for most of the applications that we will be writing over the next few tutorials.

Pixels

A pixel is the smallest possible unit that can be displayed on the screen. There are three attributes of a pixel that you always need to keep in mind: its x-coordinate, its y-coordinate and its color.

In contrast to pygame and just about every single game development library on the planet, Rubygame does not have a method to plot individual pixels. It used to but it was removed. Its not really a problem, because we can easily create our own, but it is a little annoying that something that you need so frequently is not there. Oh, well, let us not worry about such details now.

I normally learn best by looking at examples, so here we go. The following program plots random pixels to the screen.


 1 #! /usr/bin/ruby
 2
 3 # Plot random pixels.
 4
 5 require rubygame
 6
 7 Width = 640
 8 Height = 400
 9
10 module Rubygame
11   class Surface
12     def put_pixel(point, color)
13       self.draw_box point, point, color
14     end
15   end
16 end
17
18 class RandomPixels
19   include Rubygame
20
21   def initialize
22     @screen = Screen.new [Width, Height]
23     @events = EventQueue.new
24
25     @screen.fill [0, 0, 0]
26     @screen.update
27   end
28
29   def event_loop
30     loop do
31       @events.each { |event|
32         case event
33         when QuitEvent
34           return
35         end
36       }
37
38       draw
39       @screen.update
40     end
41   end
42
43   def draw
44     point = [rand(Width), rand(Height)]
45     color = [rand(255), rand(255), rand(255)]
46     @screen.put_pixel point, color
47   end
48 end
49
50 Rubygame.init
51 RandomPixels.new.event_loop
52 Rubygame.quit

The first thing we do is inject a little extra functionality into Rubygame’s Surface class. I chose to call it put_pixel (reminds me of Turbo Pascal’s graph library), but feel free to call it anything you want. The put_pixel method simple draws a box which is a single pixel high and a single pixel wide. The result is that I’ve plotted a single pixel.

Most of the rest of the program should be familiar to you, at least if you have already read tutorial #1. What is new is that there is a call to the draw method inside the event loop. The draw method creates a random point (x, y) and a random color (red, green, blue). Then it calls our Surface#putpixel method, passing in the point and color.

Notice that since the draw method is called inside the event loop, this program will continuously plot randomly colored pixels and random coordinates. There is also a call to Surface#update in the event loop. More about what that does below.

Exercises

  1. Modify the above program to only plot pixels that are random shades of blue.
  2. Modify the above program to only plot pixels of a single color, such as orange.
  3. Modify the above program to keep count of how many pixels have been plotted.
  4. Modify the program to only plot pixels in the top half of the screen.

Timing

Try running the above program. While it is running, open up a terminal and start ‘top’. You can also use any other utility that lets you see your system’s resource usage (such as Gnome system monitor). You may notice that CPU usage is quite high. What is happening?

Our program is actually very busy all the time, it is plotting points and checking if any event have occured, and its doing this constantly. Because of that, the program signals the kernel that “Hey, I’ve got a heavy workload over here. Better help me out!”

The fact is that the program isn’t really that busy. There is time for it to just relax a bit once in a while. If we can let our program take a break, it means there is more time available for the kernel to help out other tasks that are running on your system, and the whole system ends up being happier.

That is obvioulsy grossly over-simplified, but i think you get the point. We need to let our program “go to sleep” every so often. This can be achieved with the help of a clock. The following program looks like the previous one, but we have added a clock to it.


 1 #! /usr/bin/ruby
 2
 3 # Plot random pixels.
 4
 5 require rubygame
 6
 7 Width = 640
 8 Height = 400
 9
10 module Rubygame
11   class Surface
12     def put_pixel(point, color)
13       self.draw_box point, point, color
14     end
15   end
16 end
17
18 class RandomPixels
19   include Rubygame
20
21   def initialize
22     @screen = Screen.new [Width, Height]
23     @events = EventQueue.new
24     @clock = Clock.new
25     @clock.target_framerate = 120
26
27     @screen.fill [0, 0, 0]
28     @screen.update
29   end
30
31   def event_loop
32     loop do
33       @events.each { |event|
34         case event
35         when QuitEvent
36           return
37         end
38       }
39
40       draw
41       @clock.tick
42       @screen.update
43     end
44   end
45
46   def draw
47     point = [rand(Width), rand(Height)]
48     color = [rand(255), rand(255), rand(255)]
49     @screen.put_pixel point, color
50   end
51 end
52
53 Rubygame.init
54 RandomPixels.new.event_loop
55 Rubygame.quit

There are two things that are important here. The first happens on lines 24 and 25, where we initialize a clock and set a desired framerate for it. I’m going to let you experiment a little with the target_framerate. Set it to different values, high as well as low ones. Each time you do, run the program and see how “fast” the program itself seems to run. Also check the CPU usage. What do you notice?

The second thing that you should notice is that in the event loop, we added an extra line. On line 41 we call Clock#tick. This is the line that puts our program to rest for a short while. This call is inside the event loop so it gets called frequently.

Timing the program has other uses than just to lower its CPU usage, but we will look at that more in a later tutorial.

Exercises

  1. Set Clock#target_framerate to 10. What happens to your program? What happens to CPU usage while the program is running?
  2. Set Clock#target_framerate to 1000. What happens to your program? What happens to CPU usage while the program is running?

Doing more than one thing at once

Our program plots a single pixel every iteration of the event loop. Plotting just a single pixel is an expensive operation since @clock.tick and @screen.update take significant time (relatively speaking). In order to speed things up, it is better that we perform more than a single drawing operation at a time. In the following program we plot one hundred pixels each cycle of the event loop.


 1 #! /usr/bin/ruby
 2
 3 # Plot random pixels.
 4
 5 require rubygame
 6
 7 Width = 640
 8 Height = 400
 9
10 module Rubygame
11   class Surface
12     def put_pixel(point, color)
13       self.draw_box point, point, color
14     end
15   end
16 end
17
18 class RandomPixels2
19   include Rubygame
20
21   def initialize
22     @screen = Screen.new [Width, Height]
23     @events = EventQueue.new
24     @clock = Clock.new
25     @clock.target_framerate = 120
26
27     @screen.fill [0, 0, 0]
28     @screen.update
29   end
30
31   def event_loop
32     loop do
33       @events.each { |event|
34         case event
35         when QuitEvent
36           return
37         end
38       }
39
40       draw
41       @clock.tick
42       @screen.update
43     end
44   end
45
46   def draw
47     100.times do
48       point = [rand(Width), rand(Height)]
49       color = [rand(255), rand(255), rand(255)]
50       @screen.put_pixel point, color
51     end
52   end
53 end
54
55 Rubygame.init
56 RandomPixels2.new.event_loop
57 Rubygame.quit

Normally we want to assemble our whole frame before displaying it. If we were to draw directly to the screen, we would get a lot of flicker. So instead we perform all our drawing on a hidden buffer. When we have drawn the entire frame, we call Surface#update to make the new frame visible.

This happens at a very high speed, so our eyes don’t see that what we are doing is switching buffers all the time. But updating the screen is relatively time-consuming. At a later stage we will see how we can update only those parts of our surface that have changed since the previous frame.

Since we only call Surface#update once our whole frame is assembled, it also means we have a limited time to perform all our drawing. If it takes a minute to draw a frame then our framerate will be 1 frame per minute. Any gamer who plays your game for more than a minute deserves a reward, for patience or stupidity, whichever comes first.

Exercises

  1. In the last program, experiement with the number of pixels to plot in the draw method. Try a relatively low number such as 10. Also try higher number such as 1000 or 10000. What do you think would happen to the program if you choose a very large number?
  2. In the Surface#set_pixel method we are calling Surface#draw_box with two points that are the same. Modify the method so that the second point is 5×5 points away from the first. What happens to the program.

Conclusion

Now you are able to create a Rubygame screen and plot pixels onto it. There is actually quite a bit we can do armed with only that knowledge. We will continue plotting pixels for a little longer, while we look at some interesting events that we can let our program respond to.

2007.12.09

Rubygame tutorial #1: getting started

Filed under: Rubygame, Rubygame Tutorial — Lorenzo E. Danielsson @ 21:58

I while ago I posted on Ruby/SDL, which allows you to program SDL applications in Ruby. It works, but I think many Ruby programmers would like to have a higher-level API, something similar to pygame.

Enter rubygame. The first rubygame site states that:

Rubygame is a cross-platform multimedia library for ruby, the most beautiful programming language in the world. It embraces the ruby spirit to provide developers with a library that is clean and easy to use so you can get things done painlessly, and yet powerful and flexible so you can get them done right

I have written a few pygame tutorials to date, but since I happen to also love Ruby I thought that I would try to put together some similar tutorials for Ruby programmers as well. So, let’s get started.

Getting Rubygame

Before you can start developing using rubygame, you obviously need to get it installed. It doesn’t exist in the Debian repositories (at least not yet), but I downloaded it by hand, and it wasn’t difficult at all to install. Just make sure you have all the dependencies installed. Then just follow the instructions in the README.

What you need to know already

I assume that you have some familiarity with Ruby, but you don’t have to be an expert at all (it would be difficult for me to demand that out of you since I am by no means one myself!).

Displaying a window

Let’s get started with a simple program that displays a small window. Type this in as you see it and save it as something like window.rb. You should have some familiarity with how to use a computer and how to type commands in a terminal window.


1 #! /usr/bin/ruby -w
2
3 require rubygame
4
5 Rubygame.init
6 screen = Rubygame::Screen.new [640, 400]
7 loop {}
8

You can run it directly by typing ruby window.rb (assuming that’s what you called it) or you can chmod +x window.rb in which case you can run it with ./window.rb.

You should see an empty, black window. The window is 640 pixels wide and 400 pixels high. You will also notice that clicking on the window’s close button doesn’t do a thing. In order to close the window you will have to activate your terminal window and press CTRL+C.

In order to use rubygame you first have to require it. Then you should call Rubygame.init to allow rubygame to initialize itself. Create a Rubygame::Screen, passing in the window’s width and height as an Array. The next line just jumps the program into an indefite loop, which is why we have to press CTRL+C to stop the program.

Exercises

1. Modify the program to use other values than 640 and 400 for the width and the height of the window. Run the program to confirm that the window size changes. For instance, create a window that is 320×200.

2. Modify the program to accept the width and the height of the window as command-line arguments.

3. Create a program that upon start up prompts the user for the width and the height and then creates a window of those dimensions.

4. Create two instances of Rubygame::Screen. You can call them screen1 and screen2 if you wish. What do you think should happen? Run the program. What actually happens? Can you explain why?

Adding an event loop

It would probably be a good idea to create a program that behaves properly, at the very least when the user tries to close the window. The following does just that:


 1 #! /usr/bin/ruby -w
 2
 3 require rubygame
 4
 5 Rubygame.init
 6 screen = Rubygame::Screen.new [640, 400]
 7 events = Rubygame::EventQueue.new
 8
 9 loop do
10   events.each { |event|
11      if event.class == Rubygame::QuitEvent
12        Rubygame.quit
13        exit
14      end
15   }
16 end

The first new thing is that we create and EventQueue. Events are things that happen to the window, like the mouse moving over it, a key on the keyboard being pressed while the window is active and so on. Another event is the Quit event, which is triggered when the close window button is clicked.

In order to be able to respond to events that happen during the life time of the application, we need an event loop. This is basically just a loop that checks if there are any new events in the event queue. If there are we can deal with them. In this case all we do is check if the event is the QuitEvent, and if so, close down Rubygame and quit the application.

Exercises

1. Rewrite all the exercises you wrote in the previous section to include an event loop.

2. See if you can re-write the application to break out of the outer loop when the application receives the Quit event. There are several ways to do this.

Adding some color

So far so good. But let’s change the background color. Try this:


 1 #! /usr/bin/ruby -w
 2
 3 require rubygame
 4
 5 Rubygame.init
 6 screen = Rubygame::Screen.new [640, 400]
 7 events = Rubygame::EventQueue.new
 8 screen.fill [255, 255, 0]
 9 screen.update
10
11 loop do
12   events.each { |event|
13      if event.class == Rubygame::QuitEvent
14        Rubygame.quit
15        exit
16      end
17   }
18 end

What is new is that we fill the screen with a color. The color comes in a triplet consisting of red, green and blue (RGB). Each can be a value between 0 and 255.

Exercises

1. Create a window with a white background color.

2. Create a window with a red background color.

3. Experiment with setting different background colors. If you are not familiar with RGB values then spend a little extra time to figure out how to get colors like yellow, brown, cyan etc.

4. Create a program that asks the user to specify the values for red, green and blue. Check that the values are in the valid range (0-255) and then use these for the background color.

5. Create a program that upon start-up checks the time of the day and sets the brightness of the background color accordingly. Use a blue scale. If it is midnight, the screen should be black. If it is midday, the screen should be bright blue. Any other time the background should be something in between corresponding to the brightness outside.

But wait

I was having so much fun that I almost forgot: Ruby is an object-oriented programming language. Below is a modified version of the previous program that sticks everything into a class and adds a few LOC at the same time. 😉


 1 #! /usr/bin/ruby -w
 2
 3 require rubygame
 4
 5 class Simple
 6   include Rubygame
 7
 8   def initialize
 9     @screen = Screen.new [640, 400]
10     @events = EventQueue.new
11     @screen.fill [255, 240, 0]
12     @screen.update
13   end
14
15   def event_loop
16     loop do
17       @events.each { |event|
18         case event
19         when QuitEvent
20           return
21         end
22       }
23     end
24   end
25 end
26
27 Rubygame.init
28 Simple.new.event_loop
29 Rubygame.quit
30

This example can be seen as a form of template for the next few programs that we will be writing.

Exercises

1. Modify the last version of the program so that it has the attributes width and height. The constructor should take a width and height as well. If you like, you can let the constructor have default values for these two.

Conclusion

Well, with that, the first Rubygame tutorial is at an end. In the next tutorial we will start drawing some things on the screen and looking at a few more events that our program can deal with. Until then, practice, practice, practice. And, remember to always have fun when you program in Ruby.

Blog at WordPress.com.