Flash Animation Maths Graphics

For a Flash game, I needed to be able to simulate a ball bouncing around a landscape with arbitrary shapes. It's not convenient or performant to do accurate physics in Flash, and it would also mean that we would need additional data describing the shapes for the purpose of the physics hit testing.

Instead, we can approximate it using the existing shapes, with hit testing. In the direction of travel, hit test a number of points across 180 degrees of the ball's surface, and use those to work out when it needs to bounce.

Setup

The movie is 550 by 400 pixels and runs at 30 frames a second, all in a single frame. It has three parts:

  • The level is a movie clip with the landscape drawn into it as ordinary Flash shapes, here a few red blocks and curves. Nothing about the shapes is stored anywhere else, so they can be drawn however you like.
  • The arena holds the level, as level, and does all the hit testing. It lives at _root.game.arena.
  • The ball is a movie clip with a script of its own, and moves itself each frame.

The arena has one function the ball needs, workOutBallHit(x, y, dx, dy, r). It's given the ball's position, its velocity in pixels per frame and its radius, and it returns an object with where the ball should go next, new_x and new_y, and its velocity from then on, new_dx and new_dy. It doesn't move anything itself, so the ball stays in charge of its own movement, and the arena only answers questions about the level.

The ball finds the arena when it starts, works out its radius from its size, and sets off at about 4.2 pixels a frame, down and to the right. Then each frame it asks the arena what happens next, and moves:

arena = _root.game.arena;
x = _x;
y = _y;
dx = 2.73113;
dy = 3.2121;
r = (_width + _height) * 0.25;
onEnterFrame = function() {
  if (!arena) {
    return;
  }
  hit = arena.workOutBallHit(x, y, dx, dy, r);
  x = hit.new_x;
  y = hit.new_y;
  dx = hit.new_dx;
  dy = hit.new_dy;
  _x = x;
  _y = y;
  if (hit.hitObject) {
    arena.hitBrick(hit.hitObject);
  }
};
The ball's script: start moving, then ask the arena every frame

The radius is a quarter of the width plus the height, which is half the average of the two, so a ball that isn't quite round still gets a sensible one. The ball keeps its position in x and y rather than reading _x and _y back each frame, because Flash stores those in twentieths of a pixel, and the small steps would be rounded away bit by bit.

The result also has a hitObject, for a level where some of the shapes are bricks that the ball knocks out. The ball hands it to the arena's hitBrick to deal with. In this movie, workOutBallHit always sets it to null and hitBrick is empty, so nothing gets knocked out.

The hit tests take points on the Stage, and the ball's position is used for them directly. That works because the game, the arena and the level all sit at the origin, where their coordinates and the Stage's are the same. The arena also makes an empty movie clip, debug, at depth 2000, above everything else, and draws the probes into it each frame, which is what you can see on the ball in the movie.

Probes

A probe is a single point on the edge of the ball, tested against the level with hitTest. With its third argument, the shape flag, set to true, hitTest checks the point against the filled shapes themselves rather than the box round them, so a point in the gap inside a curve doesn't count as a hit.

The probes have to face the way the ball is going, so they're worked out fresh each frame. The direction of travel is a = Math.atan2(dy, dx), and the probes go round the ball from 90 degrees one side of it to 90 degrees the other, 0.1 radians apart, which is 32 of them across the half of the ball that's moving into things. A probe at angle a1 from the direction of travel is at:

x + r * Math.cos(a + a1), y + r * Math.sin(a + a1)
Where a probe is, at an angle a1 from the direction of travel

The back half of the ball is never tested, and that matters. After a bounce, the ball is moving away from the surface, so the middle of the contact is now behind it, where nothing is tested, and the ball doesn't bounce back into a surface it's still touching.

What the bounce needs to know is which part of the ball is touching the surface, and that's found with two sweeps. The first starts at one side and works round towards the other until a probe hits, and that angle is a1. If nothing hits, there's no bounce this frame and it's done. If something does, the second sweep starts at the other side and works back towards a1 until a probe hits, and that's a2. The two are the ends of the part of the ball that's touching:

var a = Math.atan2(dy, dx);
var a1;
var a2;
var h;
h = false;
for (a1 = -1.5707963; a1 <= 1.5707963; a1 += 0.1) {
  h = level.hitTest(x + r * Math.cos(a + a1), y + r * Math.sin(a + a1), true);
  if (h) {
    break;
  }
}
if (h) {
  h = false;
  for (a2 = 1.5707963; a2 >= a1; a2 -= 0.1) {
    h = level.hitTest(x + r * Math.cos(a + a2), y + r * Math.sin(a + a2), true);
    if (h) {
      break;
    }
  }
}
Sweeping in from each side to find the two ends of the contact, with the debug drawing taken out

Each sweep stops at its first hit, so the probes in the middle of the contact are never tested. On a frame where the ball is in open space all 32 probes are tested, and when it's touching something, fewer.

The probes round the front of a ball touching a surface, the two ends of the contact, and the normal between them
The probes on a ball moving into a surface, and the two ends of the contact

In the movie, the first sweep is drawn in green and the second in red, each up to the probe where it stopped. While the ball is in open space, you see a green fan across its front half, and when it touches something, the green stops at one end of the contact and the red comes round from the other side as far as the other end.

Bouncing

The ball always moves on by its current velocity, so new_x is x + dx and new_y is y + dy. If neither sweep hit anything, the velocity stays the same too, and the ball carries on in a straight line.

If the probes found a contact, the velocity is bounced. The surface faces straight out of the middle of the part of the ball that's touching it, so the normal, the direction square to the surface, is halfway between the two ends: a + (a1 + a2) / 2. It points from the centre of the ball into the surface.

Then the velocity d is reflected in the normal n, which is d − 2n(n·d). The dot product n·d is how fast the ball is going into the surface. Taking that off once would leave the ball sliding along the surface, and taking it off twice sends it back out again as fast as it went in. The part of the velocity along the surface doesn't change:

o.new_x = x + dx;
o.new_y = y + dy;
if (h) {
  var na = a + (a1 + a2) * 0.5;
  var nx = Math.cos(na);
  var ny = Math.sin(na);
  var nd = nx * dx + ny * dy;
  o.new_dx = dx - 2 * nx * nd;
  o.new_dy = dy - 2 * ny * nd;
} else {
  o.new_dx = dx;
  o.new_dy = dy;
}
Moving the ball, and reflecting its velocity in the normal when it has hit something

Reflecting a velocity doesn't change its length, and there's no gravity or friction, so the ball keeps the same speed forever and never stops bouncing. In the movie, the direction of travel is a short black line from the centre of the ball, the ends of the contact are two more, and the normal is in blue.

Because only the part of the surface touching the ball is used, the normal follows whatever shape is there. On a curve, it's square to the curve where the ball touches it. On the point of a corner, the contact is either side of the point, so the normal points from the centre of the ball towards it, which is how a real ball bounces off a corner. If the ball runs into an inside corner and touches both sides at once, the first sweep finds one side and the second finds the other, so the normal points into the corner and the ball comes back out of it.

The probes only find a surface once the edge of the ball is inside it, and the ball moves on by its old velocity on the frame it bounces, so it can sink up to two frames' movement into a surface, about 8 pixels here, before it comes back out.

The two sweeps don't land on quite the same angles, because 180 degrees isn't a whole number of steps of 0.1 radians, so the second sweep's probes are about 0.04 radians off the first's. If a contact is narrower than that, the second sweep can miss it, and the ball bounces on the next frame, when it's a little further in and the contact is wider.

Conclusion

This worked well. It's very performant, and accurate enough for many purposes, with the huge advantage that the shapes the ball bounces off can be completely dynamic. Anything drawn into the level is bounced off, without any extra data, and the shapes can be moved, scaled or animated while the ball is bouncing round them.

Further improvements could be made if the ball needs to interact with moving parts. With more probes along each direction, at different distances in from the edge of the ball, it can see how far into the ball an object has reached. If an intersection is further into the ball than it would be if the object wasn't moving, then the object has pushed into the ball, and the ball can be given extra momentum to match.

Source code

The ActionScript was recovered from the movie with the SWF disassembler.

Scripts

  • ball.as ActionScript 1 · 90 lines · the ball, and the arena's hit testing

Download