Asteroids (Gameplay)

:: racket, games in racket

… having spent most of the first blog post on writing a small game engine to be able to handle the Asteroids game, in this post we’ll look at how to implement the actors to actually make the game playable.

This is the second part of a two-part series on implementing an Asteroids game, and, if you haven’t already, you might want to read the first part, since it covers the basics of how the game is built, and things in this blog post will not make much sense without it. Also, if you are in a hurry, you can find the full implementation in this GitHub gist

In the first blog post we already implemented the asteroids and have them interact with each other and we’ll continue by looking at another important actor in the game: the space ship.

The Space Ship

Just as we did with asteroids, the model of the space ship is drawn on paper and is made to fit inside a unit circle (a circle of radius 1). The space ship will have two parts: an outline of the ship itself and a small triangle at one end, representing the ship’s thrust — this will be shown when the ship is under acceleration:

Here are the definitions for the space ship models. Since the space ship will be of constant size, the scale can also be defined as a global variable:

1
2
3
4
5
6
7
8
9
(define space-ship-points
  '((5/5 0/5) (-4/5 -3/5) (-3/5 -1/5) (-3/5 1/5) (-4/5 3/5) (5/5 0/5)))

(define space-ship-path (points->dc-path space-ship-points))

(define space-ship-thrust-path
  (points->dc-path '((-3/5 -1/5) (-4/5 0/5) (-3/5 1/5) (-3/5 -1/5))))

(define space-ship-scale 30)

Just as with the asteroids, we can create the space ship with a physics body and have update/delta-time and paint methods, but unlike asteroids, the ship will start with no initial velocity or acceleration, that is, it will not move:

 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
(define space-ship%
  (class actor%
    (init-field [position (v2 (/ canvas-width 2) (/ canvas-height 2))])
    (super-new)

    (define the-body
      (body
       position
       vzero                ; no initial velocity
       vzero                ; no initial acceleration
       space-ship-scale
       0                    ; orientation
       0                    ; no angular-velocity
       0.98))               ; velocity damping

    (define/override (update/delta-time dt)
      (set! the-body (update-body the-body dt)))

    (define pen (make-scaled-pen "forestgreen" 5 space-ship-scale))
    (define thrust-pen (make-scaled-pen "dark orange" 3 space-ship-scale))

    (define/override (paint _canvas dc)
      (draw-model dc space-ship-path pen the-body)
      ;; If the ship is accelerating or has an angular velocity, also draw the
      ;; thrust outline.
      (when (or (not (zero? (body-angular-velocity the-body)))
                (> (vlength (body-acceleration the-body)) 0))
        (draw-model dc space-ship-thrust-path thrust-pen the-body)))
    ))

To make the ship move, we will need to handle user input and update the ship’s acceleration and angular velocity. This is done by implementing the keyboard-event method of the actor<%> interface, and there are two key things about the implementation:

  • the keyboard events modify the acceleration and angular velocity of the physics body representing the ship, they don’t move the ship itself, which is done by the update-body function of the physics engine which will calculate a new position based on the speed and velocity of the body.
  • the function monitors key presses and key releases separately, and, when a key is released, it sets the acceleration or angular velocity to zero. The ship will continue to maintain its speed and direction and will slow down because the velocity damping value is lower than 1. All this is automatically handled by the update-body function of the physics engine.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
(define space-ship%
  (class actor%
    ;; Rest of the space-ship% class is unchanged

    (define/override (keyboard-event event)
      (case (send event get-key-code)
        ((left)
         (set! the-body (struct-copy body the-body [angular-velocity -0.005])))
        ((right)
         (set! the-body (struct-copy body the-body [angular-velocity 0.005])))
        ((up)
         (define a (vscale (vrotate vright (body-orientation the-body)) 0.001))
         (set! the-body (struct-copy body the-body [acceleration a])))

        ((release)
         (define code (send event get-key-release-code))
         (when (member code '(left right))
           (set! the-body (struct-copy body the-body [angular-velocity 0])))
         (when (member code '(up))
           (set! the-body (struct-copy body the-body [acceleration vzero]))))))

    ))

We can use the space ship object as it is now, however, unless the user is very skilled, the ship can easily fly off the screen so it is no longer visible. To keep the ship inside the window (that is, the playing field), we can add collisions with the walls, same as we did to keep the asteroids inside the playing field in the previous blog post. Since the ship also has a physics body, we can reuse the body-wall-collision? function to check for the actual collision and tell the space ship that it bumped into a wall:

1
2
3
4
5
(define (handle-space-ship-wall-collision s w)
  (when (body-wall-collision? (send s get-body) (send w get-normal) (send w get-distance))
    (send s bumped-into-wall (send w get-normal))))

(add-collision-handler space-ship% wall% handle-space-ship-wall-collision)

The space-ship% class itself will need to be extended with a get-body and bumped-into-wall methods, which are pretty simple: we handle bumping into the wall using the same bounce-body function from the physics engine, as we did for the asteroids:

1
2
3
4
5
6
7
8
(define space-ship%
  (class actor%
    ;; Rest of the space-ship% class is unchanged

    (define/public (get-body) the-body)

    (define/public (bumped-into-wall normal)
      (set! the-body (bounce-body the-body normal)))))

To try out the new code, we can create a simple scene with just the walls and the space ship and see how it moves around:

1
2
3
4
5
6
(add-actor (new wall% [normal vright] [distance 0]))
(add-actor (new wall% [normal vleft] [distance canvas-width]))
(add-actor (new wall% [normal vdown] [distance 0]))
(add-actor (new wall% [normal vup] [distance canvas-height]))
(add-actor (new space-ship%))
(run-game-loop)

Colliding with Asteroids

If we add asteroids to the scene, the ship will fly through them, since there is no collision handler between the ship and asteroids. We can add a collision handler just as we did for the asteroid-asteroid collision, by simply using bodies-collide? function to determine if the body of the ship collides with the body of the asteroid. However, in case of the space ship, this test is too strict since, unlike the asteroids, the ship does not resemble a circle. To make the collision test more sensitive, once we have determined that the bodies collide, we can check if any of the points representing the ship is actually inside the asteroid. This way, we have a fast, but coarse collision test first, and if that one succeeds we have a somewhat slower but more precise test. When a collision happens, the space ship actor is simply removed from the scene:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(define (handle-space-ship-asteroid-collision s a)
  (define collision?
    (and (bodies-collide? (send s get-body) (send a get-body))
         (let* ([a-body (send a get-body)]
                [s-body (send s get-body)]
                [s-center (body-position s-body)])
           (for/or ([point (in-list space-ship-points)])
             (match-define (list x y) point)
             (body-point-collision? a-body (vplus (v2 x y) s-center))))))
  (when collision?
    (remove-actor s)))

(add-collision-handler space-ship% asteroid% handle-space-ship-asteroid-collision)

And here is the body-point-collision? function, which checks is a single point is inside the radius of a body:

1
2
3
(define (body-point-collision? b p)
  (define centre-to-point-distance (vlength (vminus p (body-position b))))
  (< centre-to-point-distance (body-radius b)))

We can add back our sample asteroids into the scene to test if the new code is working, but, if you try to run the program so far, you’ll notice that there is no good visual indication of the collision, and while the program behaves correctly and the ship disappears, it is not very intuitive for the user:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
(add-actor (new wall% [normal vright] [distance 0]))
(add-actor (new wall% [normal vleft] [distance canvas-width]))
(add-actor (new wall% [normal vdown] [distance 0]))
(add-actor (new wall% [normal vup] [distance canvas-height]))

(add-actor (new asteroid%
                [initial-position (v2 150 100)]
                [initial-direction (random-direction)]
                [initial-speed 0.05]))
(add-actor (new asteroid%
                [initial-position (v2 550 200)]
                [initial-direction (random-direction)]
                [initial-speed 0.05]))
(add-actor (new asteroid%
                [initial-position (v2 550 500)]
                [initial-direction (random-direction)]
                [initial-speed 0.05]))
(add-actor (new space-ship%))
(run-game-loop)

Explosions

To improve the visual look of the game, we can create an explosion% actor — this will be a purely visual actor, which will not interact with any other actors and will display a set of expanding bubbles that all “emerge” from a specified position. The actor has the familiar update/delta-time and paint methods: bubbles are updated according to their position, direction and speed and drawn on the screen as circles.

Since it does not interact with anything else, the explosion% actor will remain in the scene forever. To limit this, we can define a life-time for this actor: number of milliseconds the explosion lasts and once this life time expires, the actor removes itself from the scene.

 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
(struct bubble (position direction size speed) #:transparent)

(define explosion%
  (class actor%
    (init-field position               ; position where the explosion happened
                [bubble-count 50]      ; number of bubbles to draw
                ;; life time of the explosion, the object is removed after
                ;; this amount of time
                [life-time 2000])
    (super-new)

    (define bubbles
      (for/list ([n (in-range bubble-count)])
        (bubble position
                (random-direction)
                (random 5 30)
                (random-around 0.3 #:nudge 0.5))))

    (define/override (update/delta-time dt)
      (set! life-time (- life-time dt))
      (if (< life-time 0)
          (remove-actor this)           ; we're done, remove ourselves from the scene
          (set! bubbles
                (for/list ([b (in-list bubbles)])
                  (match-define (bubble position direction size speed) b)
                  (bubble
                   (vplus position (vscale direction (* speed dt)))
                   direction            ; does not change
                   (* size 1.02)        ; bubble size grows with time
                   speed)))))           ; speed does not change

    (define pen (send the-pen-list find-or-create-pen "darkslategray" 2 'solid))

    (define/override (paint _canvas dc)
      (define old-pen (send dc get-pen))
      (send dc set-pen pen)
      (for ([b (in-list bubbles)])
        (match-define (bubble p _d s _e) b)
        (send dc draw-ellipse (- (v2-x p) (/ s 2)) (- (v2-y p) (/ s 2)) s s))
      (send dc set-pen old-pen))))

Finally, we can update the handle-space-ship-asteroid-collision function to create an explosion at the position where the space ship used to be:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(define (handle-space-ship-asteroid-collision s a)
  (define collision?
    (and (bodies-collide? (send s get-body) (send a get-body))
         (let* ([a-body (send a get-body)]
                [s-body (send s get-body)]
                [s-center (body-position s-body)])
           (for/or ([point (in-list space-ship-points)])
             (match-define (list x y) point)
             (body-point-collision? a-body (vplus (v2 x y) s-center))))))
  (when collision?
    ;; Add the explosion
    (add-actor (new explosion% [position (body-position (send s get-body))]))
    (remove-actor s)))

And here is the end result:

Shooting Missiles

Since the asteroids can destroy the space ship when they collide with it, it is only fair to allow the ship to destroy asteroids. The way that works in the game is that the ship can shoot missiles, which, when they hit asteroids, break them up into smaller ones and very small asteroids are completely destroyed.

Missiles will be actors which are created by the space-ship% when the user presses the Space key, but otherwise will have a a lifetime of their own, being actors themselves. They also have a physics body, so we can make use of the physics engine for moving them. Here is the missile% actor:

 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
(define missile%
  (class actor%
    (init-field position                ; position where the missile starts
                direction               ; direction in which it is moving
                [life-time 5000])
    (super-new)

    (define length 30)                  ; the length of the missile

    (define the-body
      (body
       (vplus position (vscale direction length))
       (vscale direction 0.3)           ; direction and speed
       vzero                            ; no acceleration
       length                           ; radius
       0                                ; no orientation
       0                                ; no angular velocity
       1))                              ; no velocity damping

    (define/public (get-tip-position) (body-position the-body))

    (define/override (update/delta-time dt)
      (set! life-time (- life-time dt))
      (when (< life-time 0)
        (remove-actor this))
      (set! the-body (update-body the-body dt)))

    ;; Pen used to draw the laser shot
    (define pen (send the-pen-list find-or-create-pen "corflowerblue" 2 'solid))

    (define/override (paint _canvas dc)
      (define old-pen (send dc get-pen))
      (send dc set-pen pen)
      (define tip (get-tip-position))
      (define tail (vplus (get-tip-position)
                          (vscale (vnorm (body-velocity the-body))
                                  (* -1 (body-radius the-body)))))
      (send dc draw-line (v2-x tip) (v2-y tip) (v2-x tail) (v2-y tail))
      (send dc set-pen old-pen))

    ))

Missiles are created by the space ship when the user presses and holds the Space key and, just like with the moving keys we’ll keep track of when the user presses and releases the key, shooting missiles repeteadly while the Space key is held down:

  • the new shooting? member of the space-ship% class is a flag indicating that the space ship is shooting. It is set and cleared in response to keyboard events in keyboard-event
  • the new repeat-shoot-time holds the time remaining until the next missile launch and it is decremented in update/delta-time — when this timer reaches 0 and shooting? is #t a new missile object is created.
  • the update/delta-time method is updated to create missile% actors if needed
  • the keyboard-event method is updated to look for Space key presses and releases.
 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
(define space-ship-shoot-interval 250)

(define space-ship%
  (class actor%
    ;; Rest of the space-ship% class is unchanged

    (define shooting? #f)    ;; When #t, the ship is shooting missiles
    (define repeat-shoot-time 0)

    (define/override (update/delta-time dt)
      (set! the-body (update-body the-body dt))
      ;; When the ship is shooting, check the repeat-shoot-time and create
      ;; missile% objects
      (when shooting?
        (set! repeat-shoot-time (- repeat-shoot-time dt))
        (when (<= repeat-shoot-time 0)
          (set! repeat-shoot-time (+ repeat-shoot-time space-ship-shoot-interval))
          (define position (body-position the-body))
          (define direction (vrotate vright (body-orientation the-body)))
          (define scale (body-radius the-body))
          (add-actor (new missile%
                          [position (vplus position (vscale direction scale))]
                          [direction direction])))))

    (define/override (keyboard-event event)
      (case (send event get-key-code)
        ((left)
         (set! the-body (struct-copy body the-body [angular-velocity -0.005])))
        ((right)
         (set! the-body (struct-copy body the-body [angular-velocity 0.005])))
        ((up)
         (define a (vscale (vrotate vright (body-orientation the-body)) 0.001))
         (set! the-body (struct-copy body the-body [acceleration a])))

        ((#\space)                     ;; Start shooting missiles
         (set! shooting? #t))

        ((release)
         (define code (send event get-key-release-code))
         (when (member code '(left right))
           (set! the-body (struct-copy body the-body [angular-velocity 0])))
         (when (member code '(up))
           (set! the-body (struct-copy body the-body [acceleration vzero])))

         (when (member code '(#\space)) ;; Stop Shooting Missiles
           (set! shooting? #f)
           (set! repeat-shoot-time 0)))))

    ))

A missile will collide with asteroids, so we need to add a collision handler for this case. In case of the missile, we check if the tip of the missile is inside the asteroid using the body-point-collision? defined previously. When an asteroid is hit by a missile, we remove it from the scene, along with the missile and replace them with an explosion%. Also, if the asteroid was big enough, we add three smaller asteroids to replace it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
(define (handle-asteroid-missile-collision a l)
  (define a-body (send a get-body))

  (when (body-point-collision? a-body (send l get-tip-position))
    (remove-actor a)                   ; This asteroid is no more
    (remove-actor l)                   ; ... and neither is the missile
    (add-actor (new explosion% [position (body-position a-body)])) ; add an explosion

    ;; Add some smaller asteroids to the scene
    (define size (body-radius a-body))
    (when (> size 25)
      (define new-size (* size 0.60))
      (define position (body-position a-body))
      (define direction (vnorm (body-velocity a-body)))
      (define offset (vscale direction size))
      (for ([rotation (list 0 (/ (* 2 pi) 3) (- (/ (* 2 pi) 3)))])
        (define new-position (vplus position (vrotate offset rotation)))
        (add-actor (new asteroid%
                        [size new-size]
                        [initial-position new-position]))))))

(add-collision-handler asteroid% missile% handle-asteroid-missile-collision)

Putting the initial asteroids back into the scene, we now have a functional Asteroids game:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
(add-actor (new wall% [normal vright] [distance 0]))
(add-actor (new wall% [normal vleft] [distance canvas-width]))
(add-actor (new wall% [normal vdown] [distance 0]))
(add-actor (new wall% [normal vup] [distance canvas-height]))

(add-actor (new asteroid%
                [initial-position (v2 150 100)]
                [initial-direction (random-direction)]
                [initial-speed 0.05]))
(add-actor (new asteroid%
                [initial-position (v2 550 200)]
                [initial-direction (random-direction)]
                [initial-speed 0.05]))
(add-actor (new asteroid%
                [initial-position (v2 550 500)]
                [initial-direction (random-direction)]
                [initial-speed 0.05]))
(add-actor (new space-ship%))
(run-game-loop)

Sourcing Asteroids

So far, in our scene we have always added three asteroids which started at predefined positions. This makes the game predictable and, as the space ship shoots the asteroids, eventually there will be no more asteroids in the scene. We need a way to create new asteroids as existing ones are destroyed and we can do this by having an actor which monitors the amount of asteroids in the scene and creates new ones when this amount runs low.

To determine the total amount of asteroids in the scene, we can iterate over all the asteroids and calculate the sum of their areas (this is more predictable than simply summing their radius):

1
2
3
4
(define (total-asteroid-area)
  (for/sum ([actor (in-list the-scene)] #:when (is-a? actor asteroid%))
    (define radius (body-radius (send actor get-body)))
    (* pi radius radius)))

The asteroid-spawner% actor will use its update/delta-time method to check if the total asteroid area is less than a specified threshold, min-total-area, and create a new asteroid by calling spawn-asteroid. Calculating the total asteroid area is an expensive operation and this value does not change that often (certainly not 60 times each second). To reduce the computing resources, the asteroid-spawner% only does its checks at predefined intervals (once a second by default), and it keeps track of this using a counter, remaining-time which is decremented each time update/delta-time is called.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
(define asteroid-spawner%
  (class actor%
    (init-field
     [spawn-rate 1000]
     [min-total-area 70000])
    (super-new)

    (define remaining-time 0) ; time until next check

    (define/override (update/delta-time dt)
      (set! remaining-time (- remaining-time dt))
      (when (< remaining-time 0)
        (define total-area (total-asteroid-area))
        (when (< total-area min-total-area)
          (spawn-asteroid))
        (set! remaining-time spawn-rate)))
    ))

Finally, the spawn-asteroid function is responsible for adding a new asteroid to the scene. To make it appear that asteroids come from “outside”, it selects a random position on the screen and calculates a position outside the screen such that the asteroid moves towards the target:

1
2
3
4
5
6
7
8
(define (spawn-asteroid)
  (define target (v2 (random canvas-width) (random canvas-height)))
  (define direction (random-direction))
  (define position
    (vplus target (vscale (vnegate direction) (max canvas-width canvas-height))))
  (add-actor (new asteroid%
                  [initial-position position]
                  [initial-direction direction])))

We can now replace the three initial asteroids in our scene with a single asteroid-spawner% actor: when it initially runs, it will notice that the scene is empty and start spawning asteroids and it will keep spawning new ones as old ones are destroyed.

Keeping Spares

Many games have a concept of “lives”: then the space ship is destroyed the user gets a second chance (and a third one too). This concept can be implemented using yet another actor: the spares% actor checks periodically if a space ship is still present in the scene and creates a new space ship or changes the game outcome to 'game-over if the user ran out of spare ships.

The spares% actor also displays the number of total and remaining spare ships in the top right corner of the game window (and uses bodies and models to do the drawing):

 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
(define spares%
  (class actor%
    (init-field [initial 5])
    (super-new)

    (define remaining initial) ; Spare space ships still remaining
    (define check-interval 3000)
    (define cooldown 0) ; Remaining time until next check

    (define/override (update/delta-time dt)
      (set! cooldown (- cooldown dt))
      (when (< cooldown 0)
        (set! cooldown (+ cooldown check-interval))
        (define ship                    ; try to find the ship in the scene
          (for/first ([actor (in-list the-scene)]
                      #:when (is-a? actor space-ship%))
            actor))
        (unless ship                    ; there is no ship!
          (if (> remaining 0)
              (add-actor (new space-ship%))
              (set! game-outcome 'game-over))
          (set! remaining (sub1 remaining)))))

    (define scale (* space-ship-scale 0.75)) ; draw smaller ships for the spares

    (define bodies ; physics bodies used for spare space ships, used so we can use `draw-model`
      (for/list ([index (in-range initial 0 -1)])
        (define x (- canvas-width (* 2 index scale)))
        (define y scale)
        (body (v2 x y) vzero vzero scale 0 0 0)))

    (define remaining-pen (make-scaled-pen "teal" 5 scale))
    (define used-pen (make-scaled-pen "dark gray" 5 scale))

    (define/override (paint canvas dc)
      (for ([body (in-list bodies)]
            [index (in-naturals)])
        (define pen (if (>= index remaining) used-pen remaining-pen))
        (draw-model dc space-ship-path pen body)))
    ))

The spares% actor will add new space ships in the middle of the screen, but during a game, there might be asteroids at that location, causing an instant collision, which is unfair to the player. We could find an empty spot on the screen and place the ship there, but a simpler approach is to make the ship indestructible for a short period of time after it is created — this will allow the user to move to a safe position before this indestructible period runs out.

This “cooldown” mechanism is implemented in two steps: first the space-ship% object maintains a “cooldown” counter which is initialized to an arbitrary value (5 seconds in the game) and decremented in the update/delta-time method of the ship class. The space ship - asteroid collision handler, handle-space-ship-asteroid-collision, checks this cooldown value and only runs if the cooldown is zero, that is, it only checks for collisions after the cooldown period has passed. In addition to this, to give the user a visual cue that the ship is invincible, a circle is drawn around the ship in the space-ship%’s paint method. To keep the blog post shorter, the implementation will not be shown here, but you can check out the GitHub gist for the full game.

Counting the Score

Every game needs to keep score, how else would players be able to compare their skill level? In the asteroids game, the simple way to keep score is to award points for each asteroid that is destroyed, with more points awarded for destroying smaller ones, since they are harder to hit. We’ll keep the score in the global game-score variable and update it in the handle-asteroid-missile-collision function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(define game-score 0)

(define (handle-asteroid-missile-collision a l)
  (define a-body (send a get-body))

  (when (body-point-collision? a-body (send l get-tip-position))
    ;; Rest of the `handle-asteroid-missile-collision` body is unchanged

    ;; Update the game score -- user gets more points for hitting a smaller
    ;; asteroid.
    (set! game-score (+ game-score (+ 100 (* 1000 (max 0 (- 1 (/ size 100)))))))))

To display the score, we can use another actor, whose paint function will draw the score in the top left corner of the screen. To make it more interesting, the displayed score will lag behind the actual game score and will be updated in the update/delta-time method — this will produce the pleasing “counting up” effect for the number shown on the screen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
(define game-score%
  (class actor%
    (init)
    (super-new)

    (define displayed 0)                ; the game score we actually display.

    (define/override (update/delta-time _dt)
      (when (< displayed game-score)
        (define difference (- game-score displayed))
        (set! displayed (exact-truncate (+ displayed (* 0.10 difference))))))

    (define text-font (send the-font-list find-or-create-font 24 'default 'normal))

    (define/override (paint canvas dc)
      (define label (~a displayed #:width 7 #:left-pad-string "0" #:align 'right))
      (define old-font (send dc get-font))
      (send dc set-font text-font)
      (send dc draw-text label 5 5)
      (send dc set-font old-font))
    ))

Everything Together

With all the actors set up, we can create the scene adding initial actors and run the game (you can find the fill implementation in this GitHub gist:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(add-actor (new space-ship%))           ; our space ship
(add-actor (new spares% [initial 3]))   ; add some spare ships
(add-actor (new game-score%))           ; show the game score
(add-actor (new asteroid-spawner%))     ; someone has to produce the asteroids

;; These are the walls making up the scene, everything bounces inside these
;; walls.
(add-actor (new wall% [normal vright] [distance 0]))
(add-actor (new wall% [normal vleft] [distance canvas-width]))
(add-actor (new wall% [normal vdown] [distance 0]))
(add-actor (new wall% [normal vup] [distance canvas-height]))

(run-game-loop)

And here is me playing the game:



Final Thoughts

This has been a long blog post, but the actual implementation is still relatively small and self contained: both the game engine and the game itself are implemented in about 1000 lines of Racket code. I found the “Actor” concept an interesting one and it can be used in creative ways to implement an entire game. Apart from obvious actors such as asteroids and space ships, the game score and spare lives are also implemented as actors. Here is the full list once more:

  • asteroid% — this is the most recognizable actor, it can draw asteroids and use the physics engine to move them around and collide with other things
  • ship% — this is another obvious actor, representing the space ship. In addition to the functionality of the asteroid for drawing and moving, it can also respond to user input to change its acceleration or to create missile% actors.
  • missile% — representing missiles as actors means that we can let the game engine manage multiple missiles in the scene and we don’t have to worry about doing that ourselves.
  • wall% — to keep asteroids and space ship inside a playing field, we used a wall actor whose sole purpose is to act as a collision target for asteroids and ship.
  • asteroid-spawner% — a simple actor which monitors the scene and introduces new asteroids if the supply is low
  • explosion% — an actor to display the animation for an “explosion” — yes, bubbles are not very realistic, I know.
  • game-score% — an actor that displays the game score — since the score is displayed using an animation which makes the counter roll up, it is simpler to just make an actor that uses the update/delta-time and paint to do this.
  • spares% — an actor which monitors the scene and add another space ship if the current one is destroyed, this is used to implement the concept of “lives” in the game and allow the user a second chance when their ship is destroyed.