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

1 Comment »

  1. Great chunk of tutorials, I’d like to see some more if you had the time to put them together.

    The one question I had: between tutorials 2 and 3 you stopped using a “main” class for your application. Was there a good reason for this change, or was it just personal preference in organization methods?

    Comment by Zander — 2008.05.17 @ 20:28


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: