Python 78 - Position?
What if there was some sort of Position object?
In the previous article I was mentioning the common moving code:
class Ship(Flyer):
def __init__(self, position):
super().__init__()
self.position = position.copy()
self.velocity = Vector2(0, 0)
...
def move(self, delta_time, _ships):
position = self.position + self.velocity * delta_time
position.x = position.x % u.SCREEN_SIZE
position.y = position.y % u.SCREEN_SIZE
self.position = position
I wonder whether there could be a handy little object to do this job for us.
- Aside
- I should mention the Flyer inheritance there. That’s a thing we were trying in the Zoom meeting, to see whether we could induce PyCharm to do a Change Signature refactoring correctly. Giving the classes a common superclass did help. I don’t intend for the classes to inherit from anyone, so I’ll probably remove that soon. Not worth writing home about.
Let’s TDD up a MovablePosition object.
class TestMovablePosition:
def test_creation(self):
position = Vector2(0, 0)
velocity = Vector2(100, 200)
mp = MovablePosition(position, velocity)
class MovablePosition:
def __init__(self, position, velocity):
self.position = position
self.velocity = velocity
Green. Commit: Initial MovablePosition class.
class TestMovablePosition:
def test_motion(self):
position = Vector2(0, 0)
velocity = Vector2(100, 200)
mp = MovablePosition(position, velocity)
mp.move(0.25)
assert mp.position == Vector2(25, 50)
class MovablePosition:
def move(self, delta_time):
self.position += self.velocity*delta_time
Green. Commit: initial move method.
def test_motion_wraps(self):
position = Vector2(990, 990)
velocity = Vector2(100, 200)
mp = MovablePosition(position, velocity, 1000)
mp.move(0.25)
assert mp.position == Vector2(15, 40)
I’m positing that you can provide the world size with a vector parameter. I also plan to default it to u.CENTER
.
class MovablePosition:
def __init__(self, position, velocity, size=u.SCREEN_SIZE):
self.position = position
self.velocity = velocity
self.size = size
def move(self, delta_time):
position = self.position + self.velocity*delta_time
position.x = position.x % self.size
position.y = position.y % self.size
self.position = position
I’ll trust the default to work, I guess.
Commit: move implemented with wrap.
I think that’s all we need for an object like Asteroid to use one of these things. Let’s see how it might plug in.
class Asteroid(Flyer):
def __init__(self, size=2, position=None):
super().__init__()
self.size = size
if self.size not in [0, 1, 2]:
self.size = 2
self.radius = [16, 32, 64][self.size]
self.position = position if position is not None else Vector2(0, random.randrange(0, u.SCREEN_SIZE))
angle_of_travel = random.randint(0, 360)
self.velocity = u.ASTEROID_SPEED.rotate(angle_of_travel)
self.offset = Vector2(self.radius, self.radius)
self.surface = SurfaceMaker.asteroid_surface(self.radius * 2)
Here we’ll need to make position and velocity temps and then set a position object after we compute them both.
There are a few references to self.position
that want to be the Vector2. Let’s rename our class to movable location. Then we can have a self.location
for the object and a property for self.position
.
Let’s just plug it in and see if we can make it work.
class Asteroid(Flyer):
def __init__(self, size=2, position=None):
super().__init__()
self.size = size
if self.size not in [0, 1, 2]:
self.size = 2
self.radius = [16, 32, 64][self.size]
position = position if position is not None else Vector2(0, random.randrange(0, u.SCREEN_SIZE))
angle_of_travel = random.randint(0, 360)
velocity = u.ASTEROID_SPEED.rotate(angle_of_travel)
self.location = MovableLocation(position, velocity)
self.offset = Vector2(self.radius, self.radius)
self.surface = SurfaceMaker.asteroid_surface(self.radius * 2)
This breaks some things. Let’s make properties for position
and velocity
:
@property
def position(self):
return self.location.position
@position.setter
def position(self, position):
self.location.position = position
@property
def velocity(self):
return self.location.velocity
The tests are all running. Of course, we aren’t using the thing yet. But:
def move(self, delta_time, _asteroids):
position = self.position + self.velocity * delta_time
position.x = position.x % u.SCREEN_SIZE
position.y = position.y % u.SCREEN_SIZE
self.position = position
Becomes:
def move(self, delta_time, _asteroids):
self.location.move(delta_time)
And we are green and the game is good. Commit: Asteroid moves via MovableLocation object, position and velocity are virtual.
Summary
That went nicely, didn’t it? Using MovableLocation in our object inits should reduce the duplication of those lines in move
. We’ll need some more operations on the class, I think, but it seems like a useful improvement so far.
We’ll want to take a look at our uses of position
and velocity
, to see if we can get by without the properties. At least some of them can probably be handled by adding distance methods and such to MovableLocation. We’ll see. Maybe tomorrow when I’m more fresh.
See you next time!