let's take a look at the physics of bouncing first.
When a ball bounces off a wall, it reflects off that wall at the same angle it arrived but mirrored around the wall's normal. The ball in the figure is moving at a speed of 50 pixels per second at an angle of 30 degrees from the normal, but who needs trigonometry if you can have vectors? So forget about angles and think vectors. You'll make things much easier if you express this speed and direction as 25 pixels per second along the X-axis and 43.3 pixels per second along the Y-axis. This is how you're moving your ball in code anyway. So we split the speed vector of the ball into two components, one parallel to the wall and one perpendicular to the wall. What actually happens is that the speed component parallel to the wall remains unchanged, while the speed component perpendicular to the wall is inverted.
For horizontal and vertical walls the vector components parallel and perpendicular to the wall are colinear with the X and Y axis. That makes things easier, so we'll look at horizontal and vertical walls first.
Here's the demo. Click on the button below to shoot the ball. The ball may fly through the wall, but that's a problem that will be tackled in the next demos.
This demo is a continuation of the demos of the second article, so I'll only explain the differences.
As a reminder, the speed vector and the walls are represented as line segments in a parametric form. The parameters t and s go from 0 to 1. So when the line segments intersect t and s will have a value between 0 and 1. The t value is interesting because it represents the portion of the speed vector that the ball travelled before hitting the wall. This value is necessary to calculate the exact intersection point.
The checkCollision() function has changed slightly. Instead of returning true or false, it will return the t value. If there is no collision the function will return -1. private function checkCollision(p1:Object, S1:Vector, p2:Object, S2:Vector) { var div = -S2.x * S1.y + S1.x * S2.y; var s = (-S1.y * (p1.x - p2.x) + S1.x * (p1.y - p2.y)) / div; var t = (S2.x * (p1.y - p2.y) - S2.y * (p1.x - p2.x)) / div;
if (t >= 0 && t <= 1 && s >= 0 && s <= 1) { return t; } else { return -1; } }
The step() function is called at onEnterFrame. It loops through the walls, checks if there is an intersection with the ball's speed vector and bounces the ball if there is one.
public function step() { if (this.bPlaying) { Increment the position of the ball with it's speed vector var newX = this.ball_mc._x + this.v.x; var newY = this.ball_mc._y + this.v.y; Loop through the walls array for (var i=0; i<this.walls.length; i++){ See article 2 for more info on how to find the closest point to the wall's edge // find closest point to wall on the ball's edge var vec:Vector = this.walls[i].n.getInverted().cross(this.radius); var pt:Vector = new Vector(this.ball_mc._x, this.ball_mc._y).plus(vec); // check collision of the speed vector starting in the closest // point and the line segment var result = checkCollision({x: pt.x, y: pt.y}, this.v, {x:this.walls[i].x, y:this.walls[i].y}, this.walls[i].v) if ( result != -1 ) { Let 's get exact position of the ball when hitting the wall. The variable result is the portion of the speed vector before the collision. So to get the exact position of the ball at the intersection, we take the current position of the ball and add the portion of speed vector before the collision, which is the speed vector multiplied by the result of the checkCollision() function. var isectPos = new Vector(this.ball_mc._x , this.ball_mc._y).plus(this.v.cross(result));
Draw ghost ball at the exact intersection, let it fade away. var ghostBall = this.debug_mc.attachMovie("ball_mc", "ball_mc", this.debug_mc.getNextHighestDepth(), {_alpha: 60}); ghostBall._x = isectPos.x; ghostBall._y = isectPos.y; ghostBall.onEnterFrame = function() { this._alpha -= 3; if (this._alpha <= 5) { delete this.onEnterFrame; this.removeMovieClip(); } }
To mimic an inelastic (real-world) collision, slow down by 10% after hitting a wall. // slow down by 10% this.v = this.v.cross(0.9);
Now bounce. When bouncing off a vertical wall, the Y component of the speed vector remains the same, while the X component is inverted. For a horizontal wall, the Y component of the speed vector is inverted, while the X component remains the same. We can detect if a wall is horizontal or vertical by examining it's normal. When the x-component of the normal is 0, we have a vertical wall. if (this.walls[i].n.x == 0) { this.v.y *= -1; } else { this.v.x *= -1; }
Now set the ball at the exact intersection and then move it along the new speed vector, but just the portion of the speed vector that hasn't been used yet, which is 1.0 - result. Why is this? Suppose the ball is moving at a speed of 10 pixels per frame. And at the start of the frame in which the intersection will occur it is 6 pixels away from the intersection point. Now you have 4 options: 1. You leave the ball where it is and apply the new speed vector. This will look awkward, because the ball will bounce 6 pixels away from the wall. 2. You move the ball to the exact intersection and leave it there. The new speed vector will be applied in the next frame. This means that the ball will have travelled 6 pixels instead of 10, thus slowing down the ball for 1 frame. But this looks OK, because the ball is drawn at the point where it hits the wall. 3. You move the ball to the exact intersection and apply the new speed vector. This means that the ball will have travelled 15 pixels instead of 10, thus speeding up the ball for 1 frame. Since you never see the ball touching the wall it will look awkward. 4. You move the ball to the exact intersection and apply the new speed vector minus the distance that has been travelled already before the collision. This is the most accurate way to reposition the ball after a collision. But in case of high speed it will look as if the ball never touches the wall. So it's up to you. If you favor looks go for option 2, if you favor accuracy go for option 4. If your right and left brain halfs are equally determined on this one, you can do both: position the ball accurately and draw a ghost ball or some other visual effect at the intersection. newX = isectPos.x + this.v.x * (1.0 - result); newY = isectPos.y + this.v.y * (1.0 - result);
Its a gr8 tutorial for how can we use vectors instead of trigonometry for motions and collisions
Posted by poonam sheth, Whose homepage is http://smspoonam.blogspot.com on Wednesday, 11 October 2006 at 10:17
There's a typo on step 3bis:
...the Y-component of Y...
instead of
...the Y-component of v...
but great work ;).
Posted by Fugus, on Thursday, 19 October 2006 at 12:47
I fixed the typo. Thanx.
Posted by Johan van Mol, on Thursday, 19 October 2006 at 3:06
Thanks for the great set of tutorials, very helpful!
One issue I've been trying to solve is dealing with walls that the ball can hit from either side. The direction of the normal throws things off. What I did was added an else after the 'if (this.walls[i].n.dot(speedVec) < 0) { ' block in move().
Instead of var vec:Vector = this.walls[i].n.getInverted().cross(this.radius); in the else I have it as var vec:Vector = this.walls[i].n.cross(this.radius);
This seems to work, but I'm worried I've recreated the bugs you addressed in demo4, and I'm guessing there might be a better solution out there.
Posted by peter, Whose homepage is http://www.thup.com/ on Thursday, 07 December 2006 at 11:17
do you got a tool to convert a drawed wallshape to this wall array that needed?
Posted by Peter, Whose homepage is http://index.hu on Wednesday, 30 May 2007 at 4:03
First I draw the shape in Illustrator and save the file in Illustrator 8 format. Open the file in a text editor, scroll to the bottom and you'll find something like
(Layer 1) Ln
... more stuff here ...
24 803 m
69 828 l
108 775 l
31 757 l
24 803 l
The numbers in front are the X and Y coordinates. 'm' means moveTo, 'l' means lineTo. I copy and paste these codes, save them as a string in actionscript and parse the string.
Posted by Johan van Mol, on Wednesday, 30 May 2007 at 5:20