Our fun with delegation, plus an early call to breakfast meant that yesterday I didn’t get to collisions. Let’s work on that today. [Ron makes a rookie mistake.]

We have a ship and asteroids, which is enough to let us work on collisions. They can collide, and when they do, the ship is destroyed and the asteroid either splits or is destroyed. There’s more than enough there to work on for a single morning, I suspect.

Given yesterday’s experience with a Mover to handle motion, I think we’ll probably move toward a delegate to handle collisions as well. We certainly know that there are common elements to all collisions, such as determining that you’re close enough to be colliding. Once the collision is detected, I imagine that we’ll send a message to each collider, informing them that they’re colliding, and probably what they’re colliding with. We’ll see how it goes.

I can foresee one bit of “trouble” with doing collisions so soon: things will probably change when we add in another kind of collider. So be it. As Hill[hill] so eloquently puts it, we are in the business of changing code.

Let’s drive this with tests. As a starting theory, let’s imagine that we’ll treat collisions among separate types separately, as we’ve done in at least one prior version. We currently have the ship separate from the asteroids, so let’s assume that we’ll keep that structure. Another question is this:

Suppose we have the ship and an asteroid in hand. Do we

  1. Send both of them to a collision handler?
  2. Ask one of them to collide with the other and deal with both?
  3. Ask ship to collide with asteroid and asteroid to collide with ship?

We might like to ask whether they’re in range only once, a the message to each would tell them that they were actually colliding. Let’s sketch what the top-level code might look like:

# ship vs asteroids
for asteroid in asteroids:
	ship.collideWithAsteroid(asteroid)

Looking at it this way, it just makes sense to tell the ship that an asteroid is involved. Otherwise we’d be throwing information away.

OK. Despite my belief that we will probably do this with delegation, let’s assume for now that it’s up to the ship. If it wants to delegate, we’ll deal with that later.

So our first tests will be about ship and asteroids.

class TestCollisions:
    def test_far_away(self):
        ship = Ship(Vector2(0, 0))
        asteroid = Asteroid(2, Vector2(100,100))
        ship.collideWithAsteroid(asteroid)
        assert ship.active

I’m making a new assumption here, namely that the ship has an active member, and that when it collides with an asteroid, that member will go False. In this case, no collision, so it should remain True.

The test is quietly failing, since ship doesn’t understand the message nor does it have the active member. PyCharm offers:

    def collideWithAsteroid(self, asteroid):
        pass

And then this:

class Ship:
    def __init__(self, position):
        self.active = True
        self.mover = Mover(position, Vector2(0,0))
        self.angle = 0
        self.acceleration = u.SHIP_ACCELERATION
        self.accelerating = False
        ship_scale = 4
        ship_size = Vector2(14, 8)*ship_scale
        self.ship_surface, self.ship_accelerating_surface = SurfaceMaker.ship_surfaces(ship_size)

PyCharm offered None. I typed True. Only took me two tries. Test should be passing now. It is. Let’s do a harder one. What should our kill distances be?

The ship is (14x8)*4, or 56x32, and big asteroids are 128. So my starting distance for the first test is just barely in range at 141.41. I’ll move it to 200,200. We’ll suppose that we can ask everyone for their kill_distance. The asteroid’s will be its size, and let’s try 45 for the ship, as a compromise between 56 and 32. I add those as members. Now to fix up the first test and add the second.

class TestCollisions:
    def test_far_away(self):
        ship = Ship(Vector2(0, 0))
        asteroid = Asteroid(2, Vector2(200,200))
        ship.collideWithAsteroid(asteroid)
        assert ship.active

    def test_close_enough(self):
        ship = Ship(Vector2(0, 0))
        asteroid = Asteroid(2, Vector2(100, 100))
        ship.collideWithAsteroid(asteroid)
        assert not ship.active

The second test is failing. Enhance the collideWithAsteroid:

    def collideWithAsteroid(self, asteroid):
        dist = self.mover.position.distance_to(asteroid.mover.position)
        if dist < self.kill_distance + asteroid.kill_distance:
            self.active = False

The tests are passing. I don’t like anything about this code, other than the fact that it passes the tests. What don’t I like?

  1. I’m violating the Law of Demeter by ripping the position out of the delegates, and even on the one I own it’s kind of ugly.
  2. I’m not destroying the asteroid
  3. I’m not expressing the idea of destroying the ship, I’m just setting the flag.

There’s probably more.

Let’s try to be a bit more expressive here.

    def collideWithAsteroid(self, asteroid):
        if asteroid.withinRange(self.mover.position, self.kill_distance):
            self.active = False

This demands that asteroid answer withinRange:

    def withinRange(self, point, distance):
        dist = point.distance_to(self.mover.position)
        return dist < self.kill_distance + distance

This passes the tests.

Wait. Are my kill distances too large? Shouldn’t they be kill_radius? Let’s rename and then rethink the values.

    def __init__(self, size=2, position=None):
        asteroid_sizes = [32, 64, 128]
        try:
            asteroid_size = asteroid_sizes[size]
        except IndexError:
            asteroid_size = asteroid_sizes[2]
        self.kill_radius = asteroid_size/2
        self.offset = Vector2(asteroid_size/2, asteroid_size/2)
        mover_position = position if position else Vector2(0, random.randrange(0, u.SCREEN_SIZE))
        angle_of_travel = random.randint(0, 360)
        mover_velocity = velocity = u.ASTEROID_SPEED.rotate(angle_of_travel)
        self.mover = Mover(mover_position, mover_velocity)
        self.surface = SurfaceMaker.asteroid_surface(asteroid_size)

I think I can see some improvements to that code.

    def __init__(self, size=2, position=None):
        asteroid_radii = [16, 32, 64]
        try:
            asteroid_radius = asteroid_radii[size]
        except IndexError:
            asteroid_radius = asteroid_radii[2]
        self.kill_radius = asteroid_radius
        self.offset = Vector2(asteroid_radius, asteroid_radius)
        mover_position = position if position else Vector2(0, random.randrange(0, u.SCREEN_SIZE))
        angle_of_travel = random.randint(0, 360)
        mover_velocity = velocity = u.ASTEROID_SPEED.rotate(angle_of_travel)
        self.mover = Mover(mover_position, mover_velocity)
        self.surface = SurfaceMaker.asteroid_surface(asteroid_radius*2)

Let’s do one more thing:

class Asteroid:
    def __init__(self, size=2, position=None):
        asteroid_radii = [16, 32, 64]
        try:
            self.kill_radius = asteroid_radii[size]
        except IndexError:
            self.kill_radius = asteroid_radii[2]
        self.offset = Vector2(self.kill_radius, self.kill_radius)
        mover_position = position if position else Vector2(0, random.randrange(0, u.SCREEN_SIZE))
        angle_of_travel = random.randint(0, 360)
        mover_velocity = velocity = u.ASTEROID_SPEED.rotate(angle_of_travel)
        self.mover = Mover(mover_position, mover_velocity)
        self.surface = SurfaceMaker.asteroid_surface(self.kill_radius*2)

I’m not making the temp asteroid_radius, just putting the value directly into kill_radius.

And now, in the interest of peace in the galaxy, let’s decide that throughout, we’ll use the word radius, instead of the k-word.

class Asteroid:
    def __init__(self, size=2, position=None):
        asteroid_radii = [16, 32, 64]
        try:
            self.radius = asteroid_radii[size]
        except IndexError:
            self.radius = asteroid_radii[2]
        self.offset = Vector2(self.radius, self.radius)
        mover_position = position if position else Vector2(0, random.randrange(0, u.SCREEN_SIZE))
        angle_of_travel = random.randint(0, 360)
        mover_velocity = velocity = u.ASTEROID_SPEED.rotate(angle_of_travel)
        self.mover = Mover(mover_position, mover_velocity)
        self.surface = SurfaceMaker.asteroid_surface(self.radius * 2)

I also modify this to refer to other_radius:

    def withinRange(self, point, other_radius):
        dist = point.distance_to(self.mover.position)
        return dist < self.radius + other_radius

Let’s fix up ship similarly.

    def __init__(self, position):
        self.active = True
        self.radius = 25
        self.mover = Mover(position, Vector2(0,0))
        self.angle = 0
        self.acceleration = u.SHIP_ACCELERATION
        self.accelerating = False
        ship_scale = 4
        ship_size = Vector2(14, 8)*ship_scale
        self.ship_surface, self.ship_accelerating_surface = SurfaceMaker.ship_surfaces(ship_size)

    def collideWithAsteroid(self, asteroid):
        if asteroid.withinRange(self.mover.position, self.radius):
            self.active = False

Tests are passing. I take that as a good sign.

Now what about the action on collision? What do we really want to happen? Well, the ship should explode, not just wind up inactive. And the asteroid should split. Now if I were not a Detroit school tester, I’d want to check to see that we call suitable methods on both objects. But right now I don’t know what those methods should even do.

I want to see this in action, not that I don’t think it works, but I just want to think about it.

Let’s add collision checking to the main loop, and let’s only draw the ship if it is active.

def check_collisions():
    if ship.active:
        for asteroid in asteroids:
            ship.collideWithAsteroid(asteroid)


while running:
    # poll for events
    # pygame.QUIT event means the user clicked X to close your window
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    screen.fill("midnightblue")

    # pygame.draw.circle(screen,"red",(u.SCREEN_SIZE/2, u.SCREEN_SIZE/2), 3)
    if ship.active: ship.draw(screen)
    for asteroid in asteroids:
        asteroid.draw(screen)

    keys = pygame.key.get_pressed()
    if keys[pygame.K_f]:
        ship.turn_left(dt)
    if keys[pygame.K_d]:
        ship.turn_right(dt)
    if keys[pygame.K_j]:
        ship.power_on(dt)
    else:
        ship.power_off()
    ship.mover.move(dt)
    for asteroid in asteroids:
        asteroid.mover.move(dt)
    check_collisions()

    # flip() the display to put your work on screen
    pygame.display.flip()

    # limits FPS to 60
    # dt is delta time in seconds since last frame, used for framerate-
    # independent physics.
    dt = clock.tick(60) / 1000

This could be said to work:

ship disappears on collision

The collision seems to come a bit early, though it was inevitable. We could trim a little bit from the radii if we’re feeling kind. But it’s basically what we asked for.

Let’s commit: Initial collision between asteroid and ship. And reflect:

Reflection

We have a moderately reasonable collision scheme. We send collideWtihAsteroid to ship, which seems like a reasonable thing to say. The ship sends a general inRange query to the asteroid, and we can imagine that we’ll make this the standard question to ask.

Upon a positive answer, we just deactivate the ship. We should at least call a method. And we don’t do anything to the asteroid at all. We’d like to have it split or if it is small, remove itself. The thing is, we haven’t begun to address how this might be done.

In other versions, we’ve had the objects add themselves to transactions or other such things. The issue with that is that we have to create and use the transactions, or we have to give the objects access to their own collections.

A common approach to this in other simple implementations of Asteroids or similar games is to leave that action up to the main game loop. It might determine whether the objects are colliding and then decide what to do about it.

That kind of approach tends to turn the objects into unintelligent chunks of data, but the code, while procedural, tends to be pretty simple.

Consider what needs to happen to an asteroid in our current situation, with our four big asteroids colliding with the ship. The asteroid comes into range of the ship, and it needs to split into two asteroids. We could do that by deleting the big one and creating two small ones, or by converting the big one and creating one small one. I am inclined toward the former way, since then I don’t have to worry about mutating an asteroid with its surface and shape and all that.

I’m honestly not sure where to put the code. So let’s do it procedurally for now, and once we have it, decide where to put it.

I’ll do this: I’ll iterate over a copy of the asteroids, so that I can modify the actual collection. I’ll use the ship’s active flag to decide what to do, and I’ll early-out the iteration, so as not to kill more than one … which means that I don’t need to iterate over a copy after all.

Let’s code it:

Long delay …

I have a very strange thing happening. When I split my asteroids, I get two, but they are on top of one another and do not deviate from each other. Here’s the code:

def check_collisions():
    if ship.active:
        for asteroid in asteroids.copy():
            ship.collideWithAsteroid(asteroid)
            if not ship.active:
                print("colliding")
                asteroids.remove(asteroid)
                radius = asteroid.radius
                size = [16, 32, 64].index(radius)
                if size > 0:
                    a1 = Asteroid(size - 1, asteroid.mover.position)
                    asteroids.append(a1)
                    a2 = Asteroid(size - 1, asteroid.mover.position)
                    asteroids.append(a2)
                ship.active = True

I’ve added a raft of prints and taken ages to realize that I’ve made a rookie mistake, or two of them.

When I create the two asteroids, I pass the same input point to both of them, asteroid.mover.position. That is the same object! So that same object gets passed to both movers:

class Mover:
    def __init__(self, position, velocity):
        self.position = position
        self.velocity = velocity

    def move(self, deltaTime):
        self.position += self.velocity * deltaTime
        self.position.x = self.position.x % u.SCREEN_SIZE
        self.position.y = self.position.y % u.SCREEN_SIZE

And it gets updated, in place, twice, once for each copy. So we get two fast-moving asteroids, in the same identical position, because … their positions are identical!

If we just copy the position on the way in, that should resolve the issue. And it does.

We’re at a point where we can commit. The ship splits large asteroids, and destroys small ones. It doesn’t go away because my collision code above puts it right back. Let’s commit and then think about this. Commit: ship splits large asteroids, kills small one, never dies.

What Just Happened???

I really wonder why this has never affected me before in all my other versions of this program. Or, I wonder why, if it did, I don’t remember it.

In one of the Kotlin versions I have:

    private fun asSplit(): Asteroid =
        Asteroid(
            pos = position,
            splitCount = splitCount - 1
        )

class Asteroid(
    private var pos: Point,
    val velocity: Velocity = U.randomVelocity(U.ASTEROID_SPEED),
    private val splitCount: Int = 2,
    private val strategy: AsteroidCollisionStrategy = AsteroidCollisionStrategy()
) : SpaceObject, Collider by strategy {
    init {
        position = pos
        strategy.asteroid = this
    }
    ...
    override fun update(deltaTime: Double, trans: Transaction) {
        position = (position + velocity * deltaTime).cap()
    }

Ah. We don’t use +=, because Kotlin doesn’t support += anywhere. If we had done that in this program, I think we’d have been OK. Let’s try.

    def move(self, deltaTime):
        self.position = self.position + self.velocity * deltaTime
        self.position.x = self.position.x % u.SCREEN_SIZE
        self.position.y = self.position.y % u.SCREEN_SIZE

Yes, that works just fine as well.

This is a feature of PyGame that I do not like. It’s letting Vector2 act as a value object, and updating it in place under +=. Maybe there’s no way out of that if you’re going to provide +=, I’d have to think about that, but I do not like it. I think we’ll go belt and suspenders on this and copy all the vectors we use. Quite possibly we should build our own type or something, but I’ll have to think about that.

class Mover:
    def __init__(self, position, velocity):
        self.position = position.copy(())
        self.velocity = velocity.copy()

    def move(self, deltaTime):
        self.position = self.position + self.velocity * deltaTime
        self.position.x = self.position.x % u.SCREEN_SIZE
        self.position.y = self.position.y % u.SCREEN_SIZE

I’d be happier if I weren’t rewriting self.position there at the end. Let’s go seriously careful here.

    def move(self, deltaTime):
        position = self.position + self.velocity * deltaTime
        position.x = position.x % u.SCREEN_SIZE
        position.y = position.y % u.SCREEN_SIZE
        self.position = position

Now what about velocity, just in case? It’s copied here but we do update it:

    def power_on(self, dt):
        self.accelerating = True
        accel = dt * self.acceleration.rotate(-self.angle)
        self.mover.velocity += accel

Let’s pass off the responsibility to the mover:

class Ship ...
    def power_on(self, dt):
        self.accelerating = True
        accel = dt * self.acceleration.rotate(-self.angle)
        self.mover.accelerate_by(accel)

class Mover ...
    def accelerate_by(self, accel):
        self.velocity = self.velocity + accel

I’m going to search the code for += now.

    def turn_left(self, dt):
        self.angle -= u.SHIP_ROTATION_STEP * dt

    def turn_right(self, dt):
        self.angle += u.SHIP_ROTATION_STEP * dt

In for a penny, I’m going to write those out longhand. angle is just a number, but it’s still an object. Once burned etc.

    def turn_left(self, dt):
        self.angle = self.angle - u.SHIP_ROTATION_STEP * dt

    def turn_right(self, dt):
        self.angle = self.angle + u.SHIP_ROTATION_STEP * dt

Now let’s test, looking good, fly, looking good, commit: try to avoid updating objects in place.

splitting asteroids

Now let’s sum up. We’ve done some good here as well as some bad.

Summary

The point of all this was to start on collisions, and we have a decent start. The idea seems to be:

Game loop cycles possible collisions, sending messages like ship.collideWithAsteroid, which sends asteroid.withinRange to decide whether there’s a collision.

So far, so good. Then the ship deactivates itself, and the game loop takes over to split the asteroids. This is a bit questionable because we’d hope that the objects ship and asteroid would deal with collisions on their own hook. But that leads us to a transaction sort of interface, or to those objects knowing their collections, or to some other thing that I haven’t thought of. We’re going to go with this scheme for a while until we can see an idea that we like better.

I rather liked the Transaction scheme used in one or another of the Kotlin versions, so we might go that way but waiting is, as Valentine Michael Smith said2. Tomorrow, Tuesday, is FGNO3, so maybe I can ask the gang what they think, if I haven’t come up with anything really good by then.

Our main loop is getting pretty messy and needs some function extraction and the like. Maybe even a Game object would be nice.

And the rookie mistake. I really can’t think of the last time I made that one, and it took me a long time to recognize it. Interesting. I’ll need to think about how to have avoided that. At the moment, I just don’t know.

So it goes. All’s well that ends, and so on. We’re on the way to collisions. Next time, more fun.

See you then!



  1. Wrong. These values are diameters. It’ll come to me later. 

  2. Private communication. 

  3. Friday Geeks’ Night Out, every Tuesday evening.