Skip to main content

Exercise 8: Accurate collisions

Please complete Exercise 7 first, since the steps below build upon your work from that project.

In Exercise 7, we noticed that the red ball moves partway into the wall before bouncing. This slow motion video shows the problem:

Understanding the problem

The ball moves into the wall because our CHECK_COLLISION() function only tests a single point at the center of the ball:

FUNC CHECK_COLLISION(): BOOL
VAR BOUNCE: BOOL
FALSE -> BOUNCE

# (FIND THE CENTER BY ADDING 8, SINCE OUR SPRITE IS 16 X 16)
VAR CHECK_X, CHECK_Y
.X + 8 -> CHECK_X
.Y + 8 -> CHECK_Y

VAR TILE_LAYER_B: TILE_LAYER
ENGINE::TILE_LAYER_B -> TILE_LAYER_B

# READ THE TILE NUMBER AT THE BALL'S CENTER
VAR TILE
TILE_LAYER_B.GET_TILE_AT_XY(CHECK_X, CHECK_Y) -> TILE

IF TILE = 2 THEN # DIAMOND
# ERASE THE DIAMOND WITH USING TILE #0
TILE_LAYER_B.SET_TILE_AT_XY(CHECK_X, CHECK_Y, 0)
SOUND::PLAY_TRACK(ID_SOUND::SOUND_0, 0)
MAIN::DIAMOND_COUNT + 1 -> MAIN::DIAMOND_COUNT
ELSIF TILE <> 0 THEN
# THE BALL HIT A WALL OF SOME KIND
TRUE -> BOUNCE
END IF

RETURN BOUNCE
END FUNC

The diagram below shows where .X + 8 and .Y + 8 is located in our sprite image, at the moment when CHECK_COLLISION() decides to bounce:

Coordinates of ball&#39;s center

For perfect accuracy, we would need to test every single point around the outer edge of the ball. That's a bit expensive, however. Performing too many checks might slow down our program. If we assume that the walls are relatively thick (not just isolated pixels), then we can get reasonably good accuracy by checking only a handful of points around the edge of the ball. The more points we check, the more accurate the bouncing will be.

Below is another diagram showing two points we could test, one at the top center of the ball (.X + 8 and .Y), and one at the bottom center (.X + 8 and .Y + 15). Keep in mind that the sprite dimensions are 16 × 16.

Coordinates ball&#39;s top and bottom

Testing these two points will make accurate bouncing for vertical collisions, but the above picture shows that the ball could still move horizontally into the wall. Let's start by writing code to test just these two points. (We'll improve it with more points later in step 32.)

Writing the code

  1. Open your project called "BOUNCE BRIX" that you saved from Exercise 7. On the CODE screen, click the "NEW FILE" button to create a new file.

  2. Click the pencil icon to rename the file from FILE_0 to EXPERIMENT:

    Rename the new file

  3. Add this code into the file:

    The "EXPERIMENT" file:

    CLASS POINT
    VAR X, Y
    END CLASS

    MODULE EXPERIMENT
    VAR POINTS: POINT[]
    END MODULE

    DATA EXPERIMENT::POINTS
    [
    { X: 8, Y: 0 },
    { X: 8, Y: 15 }
    ]
    END DATA

    The EXPERIMENT file

    What does it do?

    The code above declares a class called POINT which we can use to represent our (X,Y) points that we will check for collisions.

    Then it makes an array called EXPERIMENT::POINTS that will hold a list of points. The object { X: 8, Y: 0 } will handle the test for .X + 8 and .Y. The object { X: 8, Y: 15 } will handle the test for .X + 8 and .Y + 15. Representing them as an array will make it easier for us to experiment with other locations later.

  4. Now we need to update the "PLAYER" file with a LOOP that will test each point from EXPERIMENT::POINTS. The main idea is to replace this:

    .X + 8 -> CHECK_X
    .Y + 8 -> CHECK_Y

    ...with this:

    .X + EXPERIMENT::POINTS[I].X -> CHECK_X
    .Y + EXPERIMENT::POINTS[I].Y -> CHECK_Y

    You don't need to write everything by hand, though. You could just copy and paste from the box below into your "PLAYER" file.

    (Click to see the new code for the "PLAYER" file)

    The "PLAYER" file:

      CLASS PLAYER EXTENDS ACTOR
    # REMEMBER THE BALL'S VELOCITY
    VAR DX
    VAR DY

    HOOK THINK()
    IF GAMEPAD::LEFT AND .DX >= -2 THEN
    .DX - 1 -> .DX
    END IF
    IF GAMEPAD::RIGHT AND .DX <= 2 THEN
    .DX + 1 -> .DX
    END IF
    IF GAMEPAD::UP AND .DY >= -2 THEN
    .DY - 1 -> .DY
    END IF
    IF GAMEPAD::DOWN AND .DY <= 2 THEN
    .DY + 1 -> .DY
    END IF

    # ADD THE BALL'S VELOCITY, MOVING THE BALL BY ONE STEP
    .X + .DX -> .X
    IF .CHECK_COLLISION() THEN
    .X - .DX -> .X # UNDO OUR MOVE
    -.DX -> .DX # BOUNCE
    END IF

    .Y + .DY -> .Y
    IF .CHECK_COLLISION() THEN
    .Y - .DY -> .Y # UNDO OUR MOVE
    -.DY -> .DY # BOUNCE
    END IF

    # MAKE THE CAMERA FOLLOW THE BALL
    ENGINE::SET_CAMERA_CX(.X)
    ENGINE::SET_CAMERA_CY(.Y)
    END HOOK

    FUNC CHECK_COLLISION(): BOOL
    VAR BOUNCE: BOOL
    FALSE -> BOUNCE

    VAR I
    0 -> I
    LOOP
    IF I >= EXPERIMENT::POINTS.SIZE
    DO DROP

    # (FIND THE CENTER BY ADDING 8, SINCE OUR SPRITE IS 16 X 16)
    VAR CHECK_X, CHECK_Y
    .X + EXPERIMENT::POINTS[I].X -> CHECK_X
    .Y + EXPERIMENT::POINTS[I].Y -> CHECK_Y

    VAR TILE_LAYER_B: TILE_LAYER
    ENGINE::TILE_LAYER_B -> TILE_LAYER_B


    # READ THE TILE NUMBER AT THE BALL'S CENTER
    VAR TILE
    TILE_LAYER_B.GET_TILE_AT_XY(CHECK_X, CHECK_Y) -> TILE

    IF TILE = 2 THEN # DIAMOND
    # ERASE THE DIAMOND WITH USING TILE #0
    TILE_LAYER_B.SET_TILE_AT_XY(CHECK_X, CHECK_Y, 0)
    SOUND::PLAY_TRACK(ID_SOUND::SOUND_0, 0)
    MAIN::DIAMOND_COUNT + 1 -> MAIN::DIAMOND_COUNT
    ELSIF TILE = 3 THEN # BRITTLE TILE
    TILE_LAYER_B.SET_TILE_AT_XY(CHECK_X, CHECK_Y, 0)
    TRUE -> BOUNCE
    ELSIF TILE = 4 THEN # EXIT TILE
    TRUE -> MAIN::REACHED_EXIT
    ELSIF TILE <> 0 THEN
    # THE BALL HIT A WALL OF SOME KIND
    TRUE -> BOUNCE
    END IF

    I + 1 -> I
    END LOOP

    RETURN BOUNCE
    END FUNC
    END CLASS
  5. Click the "RUN" button to play your game. The red ball should now bounce accurately against its top and bottom, but horizontal bouncing will still go into the wall.

  6. Try changing the array in your "EXAMPLE" file to experiment with other points. Here's some ideas:

    Our original approach, testing just the ball's center:

    DATA EXPERIMENT::POINTS
    [
    { X: 8, Y: 8 }
    ]
    END DATA

    Testing the upper left corner:

    DATA EXPERIMENT::POINTS
    [
    { X: 0, Y: 0 }
    ]
    END DATA

    Testing the lower right corner:

    DATA EXPERIMENT::POINTS
    [
    { X: 15, Y: 15 }
    ]
    END DATA

    Testing the left and right edges:

    DATA EXPERIMENT::POINTS
    [
    { X: 0, Y: 8 },
    { X: 15, Y: 8 }
    ]
    END DATA

    Back to the drawing board

    It's not easy to visualize { X: 15, Y: 8 } as the right edge of our ball. Wouldn't it be nicer to specify the geometry graphically, rather than typing in numbers by hand? Hybrix's boards system can do this! We just need to teach it how, by defining a new symbol type.

S_POINT

  1. Go to the BOARDS screen, and click the "SYMBOL TYPES…" button. Then click the "NEW TYPE…" button. Click the pencil icon to rename the type from SYMBOL_TYPE_0 to S_POINT:

    Creating a new symbol type

    (We'll use this symbol type S_POINT to represent the individual (X, Y) locations.)

  2. Make these changes:

    • Change the "FIGURE" type from "BOX" to "POINT"
    • Change the "INITIAL COLOR" to white (#7)
    • Mark the checkbox for "CHILD ONLY".

    The result should look like this:

    Configuring the S_POINT type

  3. Click the "NEW FIELD…" button and choose the "METRIC" field type. Then click the "CREATE" button:

    Creating a new &quot;METRIC&quot; field

  4. Click the pencil icon to rename the field from FIELD_0 to X:

    Renaming the field

  5. Also mark the checkbox for "EMIT". Change the property to "LEFT X". The result should look like this:

    The completed &quot;X&quot; field

  6. Now do the same operations to create a metric field named Y, whose metric property is "TOP Y". The result should look like this:

    The completed &quot;Y&quot; field

S_SPRITE_GEOMETRY

  1. Then click the "NEW TYPE…" button to create a second symbol type. Click the pencil icon to rename the type from SYMBOL_TYPE_0 to S_SPRITE_GEOMETRY.

    Make these changes:

    • Change the "FIGURE" type from "BOX" to "CLIP"
    • Change the "ALLOWED CHILDREN" box to be S_POINT.

    The result should look like this:

    Configuring the S_SPRITE_GEOMETRY type

  2. Click the "NEW FIELD…" button and choose the "SYMBOL CHILDREN" field type. You will need to scroll down to find it.

    Creating a new &quot;SYMBOL CHILDREN&quot; field

    Click the "CREATE" button.

    (We'll use this S_SPRITE_GEOMETRY symbol type to collect the array of S_POINT objects.)

  3. Click the pencil icon to rename the field from FIELD_0 to POINTS. Also mark the checkbox for "EMIT". Change the "INCLUDE CHILDREN" box to be S_POINT.

    The result should look like this:

    The completed &quot;POINTS&quot; field

Creating the symbols

  1. Click the "DONE" button to return to the BOARDS screen. Click the "NEW BOARD" button to create a new board. Click the pencil icon to rename this board from BOARD_2 to SPRITE_GEOMETRY:

    Creating the &quot;SPRITE_GEOMETRY&quot; board

  2. Click the "CREATE" button. Choose S_SPRITE_GEOMETRY symbol type. Then click in the middle of the board to create a new symbol:

    Creating the S_SPRITE_GEOMETRY symbol

  3. Click the "CHANGE…" button under "FIGURE CLIP", and choose CLIP_0. The "?" box should change to show the red ball sprite as shown below:

    Choosing CLIP_0

  4. Next, click on the "SYMBOLS" tab to view the tree of symbols. Zoom in to 800% by using the zoom slider. To avoid the ball disappearing from view, make sure it is selected before zooming. (If your mouse has a scroll wheel, you can also zoom using the wheel while pressing the CTRL key on a PC, or while pressing the ALT key on a Mac.)

    Zooming to 800%

  5. Then click the "CREATE" button and this time choose S_POINT. (Don't confuse it with DECO_POINT!)

    Place the new point at the top of the ball. Then create a second point at the bottom of the ball. The result should look like this:

    Drawing the points

    This is good start, but notice that the (X, Y) coordinates are too big. We expected them to be { X: 8, Y: 0 } and { X: 8, Y: 15 }, but in the screenshot below they are { X: 162, Y: 137 }. That's way outside the sprite's 16 × 16 grid! (Your numbers may be different—it depends on where your sprite is positioned on the board.)

    Incorrect (X,Y) values

    The problem is that these coordinates measure from (0, 0) at the center of board, which is generally NOT the upper left corner of the sprite for S_SPRITE_GEOMETRY. How to fix that?

  6. Click the "SYMBOL TYPES…" button again and select S_POINT. Click the "NEW FIELD…" button and choose the "SYMBOL PARENT" field type. Then click the "CREATE" button:

    Creating a SYMBOL PARENT field

  7. Rename the new field from FIELD_0 to PARENT_GEOMETRY. Change its "PARENT TYPE" box to be S_SPRITE_GEOMETRY.

    Configuring the GEOMETRY parent field

    👉 Do not mark the "EMIT" checkbox. Our code won't need the PARENT_GEOMETRY field; it's just a helper for calculating the metric fields.

  8. Find the panel for the X field of S_POINT. Mark the checkbox for "RELATIVE TO", and specify "LEFT X". For the "OF SYMBOL FROM" box, specify the PARENT_GEOMETRY field.

    The result should look like this:

    The X field

  9. Find the panel for the Y field of S_POINT. Mark the checkbox for "RELATIVE TO", and specify "TOP Y". For the "OF SYMBOL FROM" box, specify the PARENT_GEOMETRY field.

    The result should look like this:

    The Y field

    What does "relative to" mean?

    In the symbol tree, the S_POINT symbols are children under the S_SPRITE_GEOMETRY symbol:

    The symbol tree

    For each point, the S_POINT symbol's PARENT_GEOMETRY field looks upwards in the tree to find the S_SPRITE_GEOMETRY symbol. Then our X and Y fields will be measured relative to that symbol, rather than relative to the board. "Relative to" means (0,0) is now the upper-left corner of the sprite, rather than the center of the board.

  10. Click the "DONE" button to return to the board. Now the point's coordinates show { X: 0, Y: 15 } for X and Y, the correct values!

    The corrected X and Y values

    The X and Y field names appear in yellow. Below that, another panel shows greenish-blue labels "X, Y"; these are figure properties measured using board coordinates. The panel also shows the figure's width and height properties, labeled "W, H". The colors help us avoid confusing field names (defined by you in your symbol type) versus figure properties (whose names are just informational labels).

  11. To see how the "relative to" math works, hover your mouse cursor over the yellow "X" field name. A tooltip appears, showing that the relative value 8 was calculated by subtracting 162 (the point's X in board coordinates) minus 154 (the sprite's X in board coordinates).

    The tooltip

Putting it all together

Now that we can visually specify our points, we just need to get them into our code.

  1. Select the red ball and type in a name BALL_GEOMETRY. Then mark the checkbox for "EMIT A PICKS VAR".

    Creating a picks var

  2. Go to the DATA screen, then click on the "PICKS" tab. Now we can see that Step 27 automatically created a symbol pick called SYMBOLS::BALL_GEOMETRY that contains our points:

    The &quot;DATA&quot; screen

    Crow Dan is unhappy because the S_SPRITE_GEOMETRY class doesn't exist yet. We'll fix that in the next step.

    MODULE SYMBOLS
    VAR BALL_GEOMETRY: S_SPRITE_GEOMETRY[]
    END MODULE

    DATA SYMBOLS::BALL_GEOMETRY
    {
    POINTS: [
    S_POINT {
    X: 8,
    Y: 15
    }, S_POINT {
    X: 8,
    Y: 0
    }
    ]
    }
    END DATA
  3. Go to the CODE screen, and open the "SYMBOL_TYPES" file. Add these lines to the top:

    CLASS S_SPRITE_GEOMETRY
    VAR POINTS: S_POINT[]
    END CLASS

    CLASS S_POINT
    VAR X, Y
    END CLASS

    This should eliminate the compiler error.

    (Click to see the full code for the "SYMBOL_TYPES" file)

    The "SYMBOL_TYPES" file:

    CLASS S_SPRITE_GEOMETRY
    VAR POINTS: S_POINT[]
    END CLASS

    CLASS S_POINT
    VAR X, Y
    END CLASS

    # -----------------------------------------------------------------------------
    # THIS BOARD SYMBOL PLACES AN ACTOR AT A GIVEN SPOT WHEN THE SCENE IS LOADED.
    CLASS S_ACTOR_SPOT
    VAR X: INT
    VAR Y: INT
    VAR ACTOR_FACTORY: ACTOR_FACTORY
    VAR STARTING_CLIP: CLIP
    VAR THEME: BYTE
    VAR FLIP_X: BOOL, FLIP_Y: BOOL, ROTATE: BOOL
    END CLASS

    # -----------------------------------------------------------------------------
    # THE "S_SCENE" BOARD SYMBOL SPECIFIES A TILEMAP AND A COLLECTION OF ACTORS.
    CLASS S_SCENE
    VAR TITLE: STRING
    VAR TILEMAP: TILEMAP
    VAR TILESET: TILESET
    VAR ACTOR_SPOTS: S_ACTOR_SPOT[]
    VAR BACKGROUND_COLOR: BYTE

    # ---------------------------------------------------------------------------
    FUNC LOAD()
    VAR TILESET: TILESET
    VAR TILEMAP: TILEMAP

    .TILEMAP -> TILEMAP
    .TILESET -> TILESET

    # FREE THE MEMORY FROM THE OLD TILEMAP COPY
    ENGINE::CLEAR_ACTORS()
    ENGINE::TILE_LAYER_B.LOAD_TILEMAP(NULL)

    IF TILEMAP <> NULL AND TILESET <> NULL THEN
    VAR TILEMAP_COPY: TILEMAP
    NEW TILEMAP() -> TILEMAP_COPY
    TILEMAP_COPY.COPY_FROM(TILEMAP)
    ENGINE::TILE_LAYER_B.LOAD_TILEMAP(TILEMAP_COPY)
    ENGINE::TILE_LAYER_B.LOAD_TILESET(TILESET)
    END IF

    ENGINE::LOAD_ACTOR_SPOTS(.ACTOR_SPOTS)

    IF .BACKGROUND_COLOR > 0 THEN
    .BACKGROUND_COLOR -> IO::BACKGROUND_COLOR
    END IF
    END FUNC
    END CLASS
  4. Open the "PLAYER" file. The last fix is to replace EXPERIMENT::POINTS with SYMBOLS::BALL_GEOMETRY.POINTS. To make the code more efficient, we can put it into a local variable called POINTS:

    VAR POINTS: S_POINT[]
    SYMBOLS::BALL_GEOMETRY.POINTS -> POINTS
    (Click to see the finished PLAYER file)

    The "PLAYER" file:

    CLASS PLAYER EXTENDS ACTOR
    # REMEMBER THE BALL'S VELOCITY
    VAR DX
    VAR DY

    HOOK THINK()
    IF GAMEPAD::LEFT AND .DX >= -2 THEN
    .DX - 1 -> .DX
    END IF
    IF GAMEPAD::RIGHT AND .DX <= 2 THEN
    .DX + 1 -> .DX
    END IF
    IF GAMEPAD::UP AND .DY >= -2 THEN
    .DY - 1 -> .DY
    END IF
    IF GAMEPAD::DOWN AND .DY <= 2 THEN
    .DY + 1 -> .DY
    END IF

    # ADD THE BALL'S VELOCITY, MOVING THE BALL BY ONE STEP
    .X + .DX -> .X
    IF .CHECK_COLLISION() THEN
    .X - .DX -> .X # UNDO OUR MOVE
    -.DX -> .DX # BOUNCE
    END IF

    .Y + .DY -> .Y
    IF .CHECK_COLLISION() THEN
    .Y - .DY -> .Y # UNDO OUR MOVE
    -.DY -> .DY # BOUNCE
    END IF

    # MAKE THE CAMERA FOLLOW THE BALL
    ENGINE::SET_CAMERA_CX(.X)
    ENGINE::SET_CAMERA_CY(.Y)
    END HOOK

    FUNC CHECK_COLLISION(): BOOL
    VAR BOUNCE: BOOL
    FALSE -> BOUNCE

    VAR POINTS: S_POINT[]
    SYMBOLS::BALL_GEOMETRY.POINTS -> POINTS

    VAR I
    0 -> I
    LOOP
    IF I >= POINTS.SIZE
    DO DROP

    # (FIND THE CENTER BY ADDING 8, SINCE OUR SPRITE IS 16 X 16)
    VAR CHECK_X, CHECK_Y
    .X + POINTS[I].X -> CHECK_X
    .Y + POINTS[I].Y -> CHECK_Y

    VAR TILE_LAYER_B: TILE_LAYER
    ENGINE::TILE_LAYER_B -> TILE_LAYER_B

    # READ THE TILE NUMBER AT THE BALL'S CENTER
    VAR TILE
    TILE_LAYER_B.GET_TILE_AT_XY(CHECK_X, CHECK_Y) -> TILE

    IF TILE = 2 THEN # DIAMOND
    # ERASE THE DIAMOND WITH USING TILE #0
    TILE_LAYER_B.SET_TILE_AT_XY(CHECK_X, CHECK_Y, 0)
    SOUND::PLAY_TRACK(ID_SOUND::SOUND_0, 0)
    MAIN::DIAMOND_COUNT + 1 -> MAIN::DIAMOND_COUNT
    ELSIF TILE = 3 THEN # BRITTLE TILE
    TILE_LAYER_B.SET_TILE_AT_XY(CHECK_X, CHECK_Y, 0)
    TRUE -> BOUNCE
    ELSIF TILE = 4 THEN # EXIT TILE
    TRUE -> MAIN::REACHED_EXIT
    ELSIF TILE <> 0 THEN
    # THE BALL HIT A WALL OF SOME KIND
    TRUE -> BOUNCE
    END IF

    I + 1 -> I
    END LOOP

    RETURN BOUNCE
    END FUNC
    END CLASS
  5. With these changes, we're no longer using the code from the EXPERIMENTS source file. You can delete that file:

    Deleting the &quot;EXPERIMENTS&quot; file

    Then click the "RUN" button to confirm that your program still works.

  6. Now we can finally improve our bouncing to be more accurate! Go back to the BOARDS tab, and add more points around the edge of the ball:

    Adding points around the ball&#39;s edge

    It should now bounce correctly without going into the wall. You can now experiment with other placements of points to see how it affects the bouncing.

Great job! You've come a long way!

That's all for Exercise 8. In the next exercise, we'll add some other actors to make the maze more fun.