/
index.html
299 lines (251 loc) · 10.2 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
<P>Following
posts <a href="http://daniel.ruoso.com/categoria/perl/games-perl-1">1</A>
and <a href="http://daniel.ruoso.com/categoria/perl/games-perl-2">2</A>
on the subject of writing games in Perl, now we are going to add
colision.</P>
<P>The idea is quite simple, we are going to add another square to the
game, and when the ball hits it, it will change direction. Following
the way we were working, I'm going to add another object, called
Wall.</P>
<P>The first thing is modelling our wall, which will be a rectangle,
so it has the following attributes.</P>
<PRE>
package Wall;
use Moose;
use Util;
use SDL::Rect;
# Position - vertical and horizontal
has pos_v => (is => 'rw', isa => 'Num', default => 0);
has pos_h => (is => 'rw', isa => 'Num', default => 0.12);
# Width and height
has width => (is => 'rw', isa => 'Num', default => 0.005);
has height => (is => 'rw', isa => 'Num', default => 0.4);
</PRE>
<P>Unlike the ball, a wall doesn't move, so we don't need a time_lapse
method, but we still have the get_rect and draw methods.</P>
<PRE>
sub get_rect {
my ($self, $height, $width) = @_;
my $inverted_v = ($height - ($self->pos_v + $self->height));
my $x = Util::m2px( $self->pos_h );
my $y = Util::m2px( $inverted_v );
my $h = Util::m2px( $self->height );
my $w = Util::m2px( $self->width );
my $screen_w = Util::m2px( $width );
my $screen_h = Util::m2px( $height );
if ($x < 0) {
$w += $x;
$x = 0;
}
if ($x + $w > $screen_w) {
$w -= ($x + $w) - $screen_w;
}
if ($y < 0) {
$h += $y;
$h = 0;
}
if ($y + $h > $screen_h) {
$h -= ($y + $h) - $screen_h;
}
return SDL::Rect->new( $x, $y, $w, $h );
}
my $color;
sub draw {
my ($self, $surface, $height, $width) = @_;
unless ($color) {
$color = SDL::Video::map_RGB
( $surface->format(),
255, 0, 0 ); # red
}
SDL::Video::fill_rect
( $surface,
$self->get_rect($height, $width),
$color );
}
</PRE>
<P>See
the <a href="http://daniel.ruoso.com/categoria/perl/games-perl-1">first
post</A> for more details on the get_rect and draw codes.</P>
<P>Now we need to add our wall to the game, that will mean a simple
change in our main code, first we need to load the Wall module, then
initialize the Wall just after initializing the ball, and finally
calling the draw method just after calling the same method on
ball.</P>
<PRE>
use Wall;
</PRE>
<PRE>
my $wall = Wall->new;
</PRE>
<PRE>
$wall->draw($app, $height, $width);
</PRE>
<P>If you tried to run the code at this point, you'll notice you won't
see any wall. That happens because the application is only updating
the screen where the ball is passing. The Wall needs to be drawn a
first time, and the screen needs to be updated at that position. This
prevents us from re-updating the wall rect everytime, which is
pointless, since the wall is static - that code goes right before the
main loop.</P>
<PRE>
# let's draw the wall for the first time.
$wall->draw($app, $height, $width);
SDL::Video::update_rects
( $app,
$wall->get_rect($height, $width) );
</PRE>
<P>Now we need to check for a collision. This should happen in the
place of the time_lapse call. Note that while I neglected math in the
movement part, here it's more complicated because I need to react in a
reasonable manner depending on how the collision happened. But as
we're working in Perl and we have CPAN, I can just use Collision::2D
(zpmorgan++ for working on this and pointing me in the correct
direction)</P>
<P>If you don't have the Collision::2D module installed, just call</P>
<PRE>
# cpan Collision::2D
</PRE>
<P>If you're not sure wether you have it or not, just try installing
it anyway, it will suceed if the module is already installed.</P>
<PRE>
use Collision::2D ':all';
sub collide {
my ($ball, $wall, $time) = @_;
my $rect = hash2rect({ x => $wall->pos_h, y => $wall->pos_v,
h => $wall->height, w => $wall->width });
my $circ = hash2circle({ x => $ball->cen_h, y => $ball->cen_v,
radius => $ball->radius,
xv => $ball->vel_h,
yv => $ball->vel_v });
return dynamic_collision($circ, $rect, interval => $time);
}
</PRE>
<P>I assumed an API that wasn't currently implemented in our Ball
object, so I changed the ball so that pos_v, pos_h, width and height
return the bounding dimensions for the ball I won't put the code in
the post, but you can check at
the <a href="http://github.com/ruoso/games-perl/">github repo</a>.</P>
<P>Okay, now it's time to check for collisions and act
accordingly. Again, we'll assume an 100% efficient collision, so the
code looks like:</P>
<PRE>
my $frame_elapsed_time = ($now - $oldtime)/1000;
if (my $coll = Util::collide($ball, $wall, $frame_elapsed_time)) {
# need to place the ball in the result after the bounce given
# the time elapsed after the collision.
my $collision_remaining_time = $frame_elapsed_time - $coll->time;
my $movement_before_collision_h = $ball->vel_h * $coll->time;
my $movement_before_collision_v = $ball->vel_v * $coll->time;
my $movement_after_collision_h = $ball->vel_h * $collision_remaining_time;
my $movement_after_collision_v = $ball->vel_v * $collision_remaining_time;
if ($coll->axis eq 'x') {
$ball->cen_h(($ball->cen_h + $movement_before_collision_h) +
($movement_after_collision_h * -1));
$ball->cen_v($ball->cen_v +
$movement_before_collision_v +
$movement_after_collision_v);
$ball->vel_h($ball->vel_h * -1);
} elsif ($coll->axis eq 'y') {
$ball->cen_v(($ball->cen_v + $movement_before_collision_v) +
($movement_after_collision_v * -1));
$ball->cen_h($ball->cen_h +
$movement_before_collision_h +
$movement_after_collision_h);
$ball->vel_v($ball->vel_v * -1);
} elsif (ref $coll->axis eq 'ARRAY') {
my ($xv, $yv) = @{$coll->bounce_vector};
$ball->cen_h(($ball->cen_h + $movement_before_collision_h) +
($xv * $collision_remaining_time));
$ball->vel_h($xv);
$ball->cen_v(($ball->cen_v + $movement_before_collision_v) +
($yv * $collision_remaining_time));
$ball->vel_v($yv);
} else {
warn 'BAD BALL!';
$ball->cen_h(($ball->cen_h + $movement_before_collision_h) +
($movement_after_collision_h * -1));
$ball->cen_v(($ball->cen_v + $movement_before_collision_v) +
($movement_after_collision_v * -1));
$ball->vel_h($ball->vel_h * -1);
$ball->vel_v($ball->vel_v * -1);
}
} else {
$ball->time_lapse($oldtime, $now, $height, $width);
}
</PRE>
<P>Okay, the above code was a bit complicated, let's brake it down...
<PRE>
my $frame_elapsed_time = ($now - $oldtime)/1000;
</PRE>
<P>Collision::2D works with time in seconds, it calculates if the two
objects would have collided during the duration of this frame.</P>
<PRE>
if (my $coll = Util::collide($ball, $wall, $frame_elapsed_time)) {
...
} else {
$ball->time_lapse($oldtime, $now, $height, $width);
}
</PRE>
<P>Now we check if there was a collision. If not, we just proceed to
the regular code that calculates the new position for the ball.</P>
<PRE>
my $collision_remaining_time = $frame_elapsed_time - $coll->time;
my $movement_before_collision_h = $ball->vel_h * $coll->time;
my $movement_before_collision_v = $ball->vel_v * $coll->time;
my $movement_after_collision_h = $ball->vel_h * $collision_remaining_time;
my $movement_after_collision_v = $ball->vel_v * $collision_remaining_time;
</PRE>
<P>In the case we have a collision, Collision::2D tells us when and
how it happened. In order to implement the bouncing, I also calculate
how far they would have been proceeded before and after the collision.</P>
<PRE>
if ($coll->axis eq 'x') {
...
} elsif ($coll->axis eq 'y') {
...
} elsif (ref $coll->axis eq 'ARRAY') {
...
} else {
...
}
</PRE>
<P>The method that describes how the collision happened is "axis". If
it was a purely horizontal colision, it will return 'x', if it was
purely vertical, it will return 'y', if it was mixed, it will return a
vector that describes it. In the case of a bug, it will return undef.</P>
<PRE>
$ball->cen_h(($ball->cen_h + $movement_before_collision_h) +
($movement_after_collision_h * -1));
$ball->cen_v($ball->cen_v +
$movement_before_collision_v +
$movement_after_collision_v);
$ball->vel_h($ball->vel_h * -1);
</PRE>
<P>In the case of perfect horizontal or vertical collision (or bug),
we reposition the ball by first calculating where it would be at the
time of the collision and then bounce it away - depending on how the
collision occurred.</P>
<PRE>
my ($xv, $yv) = @{$coll->bounce_vector};
$ball->cen_h(($ball->cen_h + $movement_before_collision_h) +
($xv * $collision_remaining_time));
$ball->vel_h($xv);
$ball->cen_v(($ball->cen_v + $movement_before_collision_v) +
($yv * $collision_remaining_time));
$ball->vel_v($yv);
</PRE>
<P>This last part of the code uses a cool feature for Collision::2D,
which returns a bounce information for that collision, which we then
use to figure out the correct position after the bounce.
<P>And now we can run our code. I have made some other changes not
explained here, because they are just settings that control the
behavior. Remember to access
the <a href="http://github.com/ruoso/games-perl/">github repo</a> for
more details.</P>
<P>Now a small video of the game running.</P>
<object width="480" height="385"><param name="movie"
value="http://www.youtube.com/v/PcDe0dKOflA&hl=pt_BR&fs=1&"></param><param name="allowFullScreen"
value="true"></param><param name="allowscriptaccess"
value="always"></param><embed src="http://www.youtube.com/v/PcDe0dKOflA&hl=pt_BR&fs=1&"
type="application/x-shockwave-flash" allowscriptaccess="always"
allowfullscreen="true" width="480" height="385"></embed></object>