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:

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:

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:

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:

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:

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:

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:

sbot -w arrow_controlled.bot