circuitpython_class_diagram.png
Public Domain by wikipedia user Aaron Rotenberg.

One of the goals of this project is to take advantage of the memory and performance of the SAMD51 on the NeoTrellis M4 Express to use some more advanced (some might say proper) object-oriented techniques.

Composition

We started by breaking down the game into it's parts: a bird flying to avoid running into obstacles (which we called posts), and a game entity that managed the bird and posts and oversaw the rules and mechanics of play.  The game maintains an instance of the Bird class and a list of Post instances.

Encapsulation

This is one of the most important, and sadly often ignored, principles of object-oriented design. Put simply, nothing outside of an object has any business knowing anything about the object other than the public API is exposes. The espousal of encapsulation can be seen in the generous use of private instance variables and methods (those that begin with an underscore).

Python's property mechanism is perfect for enforcing encapsulation. It's not always bad to expose internal details, but the object should generally be in control of it. As the system gets more complex it becomes more important to maintain control over who gets access to what so as to avoid unexpected consequences.

Double Dispatch

In this code, the game has a bird and several posts. It wants to know if the bird collided with a post.

The brute force approach is to get the coordinates from the bird and from each post and do the math to figure out if there is a collision. However, doing so throws out much of the advantage of using an object-oriented system.

The object-oriented way is to put the computation where the data is. In this case the data is in two places: the bird knows where it is, and each post knows where it is. Look again at the Game code for detecting a collision.

Download: file
    def _check_for_collision(self):
        """Return whether this bird has collided with a post."""
        collided = False
        for post in self._posts:
            collided |= self._bird.is_colliding_with(post)
        return collided

For each post, the bird is asked if it is colliding with that post. Next let's look at the is_colliding_with method in Bird.

Download: file
    def is_colliding_with(self, post):
        """Check for a collision.
        post -- the Post instance to check for a collicion with
        """
        return post.is_collision_at(3, self._y_position())

The bird knows where it is, and simply asks the post if it  is occupying that location. The post knows where it is and has been given a location to check against. Now all the information needed is at hand and the computation can be done by the is_collision_at method.

Download: file
    def _on_post(self, x, y):
        """Determine whether the supplied coordinate is occupied by part of this post.
        x -- the horizontal pixel coordinate
        y -- the vertical pixel coordinate
        """
        return x == self._x and (y < self._top or y > (3 - self._bottom))

    def is_collision_at(self, x, y):
        """Determine whether something at the supplied coordinate is colliding with this post.
        x -- the horizontal pixel coordinate
        y -- the vertical pixel coordinate
        """
        return self._on_post(x, y)

Note that the actual comparison is in the _on_post method, as this functionality is used in multiple places in the Post class (here and draw_on). Another fundamental best practice is avoiding the duplication of functionality. 

As indicated by the title of this section, this technique is known as Double Dispatch. A Post is passed to a method in the Bird, which then calls a method in that Post with information that only the bird knows (the bird's location). This way each object is in control of where its information goes.

This guide was first published on Dec 05, 2018. It was last updated on Dec 05, 2018.

This page (Design Discussion) was last updated on Dec 05, 2018.