Moving things in shoebot, adding different behaviours..

Tue 19 March 2013
By stu

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