In my last post we made an arrow move around the screen, in this post we'll look to extend things so it's easy to make many things move around the screen.
This will make the code a little more complex, but as usual it makes things simpler later on.
Note:
This python code runs in shoebot, planar.py is used to handle coordinates
https://github.com/shoebot
planar.py
At the end we'll have two arrows, a blue one controlled with the keyboard and a pink one that moves on it's own:
At the moment all movement behaviour is in the draw function (see below) - we're going to move all the movement behaviour into seperate functions.
Current draw function:
``` {: ."brush:python} def draw(): global pos, angle, velocity transform(mode=CENTER) if keydown: if keycode == KEY_UP: velocity += 0.2 elif keycode == KEY_DOWN: velocity -= 0.2 elif keycode == KEY_LEFT: angle -= 5 elif keycode == KEY_RIGHT: angle += 5 elif keycode == KEY_SPACE: velocity = 0.9 elif keycode == KEY_RETURN: pos = Vec2(WIDTH / 2, HEIGHT / 2) velocity = 0.0 angle = 0 else: velocity = 0.99
angle = angle % 360
pos += Vec2.polar(angle = angle, length = velocity)
if pos.x < 10 or pos.x > WIDTH:
angle = 180 - angle
if pos.y < 10 or pos.y > HEIGHT - 10:
angle = - angle
translate(pos.x, pos.y)
rotate(-angle) # nodebox1/shoebot rotation is anticlockwise
arrow(40, 40, 80, fill=(0, 0, 1, 0.2))
```
We'll seperate each behaviour into a 'controller' function:
``` {: .brush:python} def key_controller(pos, angle, velocity): if keydown: if keycode == KEY_UP: velocity += 0.2 elif keycode == KEY_DOWN: velocity -= 0.2 elif keycode == KEY_LEFT: angle -= 5 elif keycode == KEY_RIGHT: angle += 5 elif keycode == KEY_SPACE: velocity *= 0.9 elif keycode == KEY_RETURN: pos = Vec2(WIDTH / 2, HEIGHT / 2) velocity = 0.0 angle = 0 return pos, angle, velocity
def inertia_controller(pos, angle, velocity): return pos, angle, velocity * 0.99
def bounds_controller(pos, angle, velocity): if pos.x < 10 or pos.x > WIDTH: angle = 180 - angle if pos.y < 10 or pos.y > HEIGHT - 10: angle = - angle return pos, angle, velocity ```
Then make a class to manage these, along with initial position + velocity - along with a custom drawing function:
``` {: .brush:python} class Moveable(object): """ Moveable object.
Controller functions manage the movement and are called on 'update'
"""
def __init__(self, pos = None, velocity = None, angle = None, controller = None, controllers = None, draw_func = None):
self.pos = pos or Vec2(WIDTH / 2, HEIGHT / 2)
self.velocity = velocity or 0.0
self.angle = angle or 0
self.draw_func = draw_func
if controller:
self.controllers = [ controller ]
else:
self.controllers = []
if controllers:
self.controllers.extend(controllers)
def update(self):
"""
Call all the controllers to update coordinates.
Angles are always wrapped to 360 degrees.
"""
pos, angle, velocity = self.pos, self.angle, self.velocity
for controller in self.controllers:
pos, angle, velocity = controller(pos, angle % 360, velocity)
self.pos, self.angle, self.velocity = pos, angle % 360, velocity
self.pos += Vec2.polar(angle = angle, length = velocity)
def draw(self):
push()
translate(self.pos.x, self.pos.y)
rotate(-self.angle) # nodebox1/shoebot rotation is anticlockwise
self.draw_func()
pop()
```
Since I just want the draw function to draw two different colour arrows, a closure is used to generate these functions:
{: .brush:python}
def draw_arrow(fill=None):
def do_draw():
arrow(40, 40, 80, fill=fill)
return do_draw
By passing the output of draw_func = draw_arrow(fill=(1,0,0)) a red arrow is will be drawn every time 'draw_func' is called.
Using all of the above, the setup() function to create the two arrows is fairly straightforward:
``` {: .brush:python} def setup(): global auto_arrow, driven_arrow speed(60) size(800, 600) transform(mode=CENTER) driven_arrow = Moveable( controllers = [key_controller, bounds_controller, inertia_controller], draw_func = draw_arrow(fill=(0, 0, 1, 0.2)))
auto_arrow = Moveable(
velocity = 5.0,
angle = 45,
controller = bounds_controller,
draw_func = draw_arrow(fill=(1, 0, 0, 0.2)))
```
driven_arrow and auto_arrow are both instances of 'Moveable', they have different behaviours set by their controller functions and their draw functions will draw them in a transparent red and blue colours.
The final draw function, simply calls 'update' and 'draw' on auto_arrow and driven_arrow:
``` {: .brush:python} def draw(): global auto_arrow, driven_arrow
driven_arrow.update()
auto_arrow.update()
driven_arrow.draw()
auto_arrow.draw()
```
Using this system it's easy to add custom behaviours (controllers) and drawing functions.
This can be extended into a simple particle system - Particle is just a Moveable with a lifecycle and a factory function (emitter) needs to be added to create the particles, along with a little management code.
Next time - extending this into a simple particle system.
Full listing:
```
In this version of the file the concept of a controller is introduced.
A controller takes the position, angle and velocity of the thing to be moved
and returns a new position, angle and velocity.
The machinery to manage this is wrapper up in the Movable class
from planar.py import Vec2, Affine from collections import namedtuple
class Moveable(object): """ Moveable object.
Controller functions manage the movement and are called on 'update'
"""
def __init__(self, pos = None, velocity = None, angle = None, controller = None, controllers = None, draw_func = None):
self.pos = pos or Vec2(WIDTH / 2, HEIGHT / 2)
self.velocity = velocity or 0.0
self.angle = angle or 0
self.draw_func = draw_func
if controller:
self.controllers = [ controller ]
else:
self.controllers = []
if controllers:
self.controllers.extend(controllers)
def update(self):
"""
Call all the controllers to update coordinates.
Angles are always wrapped to 360 degrees.
"""
pos, angle, velocity = self.pos, self.angle, self.velocity
for controller in self.controllers:
pos, angle, velocity = controller(pos, angle % 360, velocity)
self.pos, self.angle, self.velocity = pos, angle % 360, velocity
self.pos += Vec2.polar(angle = angle, length = velocity)
def draw(self):
push()
translate(self.pos.x, self.pos.y)
rotate(-self.angle) # nodebox1/shoebot rotation is anticlockwise
self.draw_func()
pop()
def key_controller(pos, angle, velocity): if keydown: if keycode == KEY_UP: velocity += 0.2 elif keycode == KEY_DOWN: velocity -= 0.2 elif keycode == KEY_LEFT: angle -= 5 elif keycode == KEY_RIGHT: angle += 5 elif keycode == KEY_SPACE: velocity *= 0.9 elif keycode == KEY_RETURN: pos = Vec2(WIDTH / 2, HEIGHT / 2) velocity = 0.0 angle = 0 return pos, angle, velocity
def inertia_controller(pos, angle, velocity): return pos, angle, velocity * 0.99
def bounds_controller(pos, angle, velocity): if pos.x < 10 or pos.x > WIDTH: angle = 180 - angle if pos.y < 10 or pos.y > HEIGHT - 10: angle = - angle return pos, angle, velocity
def draw_arrow(fill=None): def do_draw(): arrow(40, 40, 80, fill=fill) return do_draw
def setup(): global auto_arrow, driven_arrow speed(60) size(800, 600) transform(mode=CENTER) driven_arrow = Moveable( controllers = [key_controller, bounds_controller, inertia_controller], draw_func = draw_arrow(fill=(0, 0, 1, 0.2)))
auto_arrow = Moveable(
velocity = 5.0,
angle = 45,
controller = bounds_controller,
draw_func = draw_arrow(fill=(1, 0, 0, 0.2)))
def draw(): global auto_arrow, driven_arrow
driven_arrow.update()
auto_arrow.update()
driven_arrow.draw()
auto_arrow.draw()
```
Running in shoebot:
{: .brush:bash}
sbot -w arrow_controlled.bot