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