Seek With Cocos2D

This is an implementation of Craig Reynold's flocking behavior based on the book Python Game Programming By Example. Reynolds mentions many variations for flocking. This will implement a basic boid that flocks to the mouse pointer.

Imports

# third party
from cocos.cocosnode import CocosNode
from cocos.euclid import Vector2
import cocos
import cocos.particle_systems as particle_system

Constants

CHASE = 1
RUN_AWAY = -1
CHANGE_BEHAVIOR = -1

The Boid

Boid Settings

class BoidSettings(object):
    """a settings object"""
    def __init__(self):
	self.position = None
	self.velocity = None
	self.speed = None
	self.max_force = None
	self.slowing_radius = None
	self._attributes = None
	return

    @property
    def attributes(self):
	"""list of required attributes"""
	if self._attributes is None:
	    self._attributes = ("position",
				"velocity",
				"speed",
				"slowing_radius",
				"max_force")
	return self._attributes

    def x_y(self, position):
	"""sets the initial x, y coordinates
	Parameters
	----------

	position: tuple
	  (x, y) coordinates
	"""
	self.position = position
	self.check_types(position, "position", (list, tuple))
	for item in position:
	    self.check_numeric(item, "position")
	return self

    def velocity_vector(self, velocity):
	"""initial velocity

	Parameters
	----------

	velocity: Vector2
	  2-d vector representing boid's velocity
	"""
	self.velocity = velocity
	self.check_type(velocity, "velocity", Vector2)
	return self

    def speed_scalar(self, speed):
	"""scalar for the velocity vector

	Parameters
	----------

	speed: number
	  pixels per frame to scale velocity
	"""
	self.speed = speed
	self.check_numeric(speed, "speed")
	return self

    def slow_down_distance(self, slowing_radius):
	"""denominator to create ratio to slow down

	Used to reduce steering-force (distance/radius)

	Parameters
	----------

	slowing_radius: numeric
	  steering-force reduction factor
	"""
	self.slowing_radius = slowing_radius
	self.check_numeric(slowing_radius, "slowing radius")
	return self

    def maximum_force(self, max_force):
	"""sets the max force magnitude

	Parameters
	----------
	max_force: numeric
	  upper-bound for the steering force
	"""
	self.max_force = max_force
	self.check_numeric(max_force, "max force")
	return self

    def maximum_velocity(self, max_velocity):
	"""sets the max-velocity

	Parameters
	----------

	max_velocity: numeric
	  upper-bound for magnitude of velocity
	"""
	self.max_velocity = max_velocity
	self.check_numeric(max_velocity, "max velocity")
	return self

    def check_numeric(self, value, identifier):
	"""checks value is numeric

	Parameters
	----------

	value: object
	  item to check

	identifier: string
	  name for error message

	Raises
	------

	TypeError if value is not int or float
	"""
	self.check_types(value, identifier, (int, float))
	return

    def check_types(self, value, identifier, expected):
	"""checks type of value
	Parameters
	----------

	value: object
	  the thing to check
	identifier: string
	  id for error message
	expected: collection
	  types to check if value is one of them

	Raises
	------

	TypeError if type of value not in expected
	"""
	if type(value) not in expected:
	    raise TypeError("{0} must be one of {1}, not {2}".format(
		identifier,
		expected,
		value))
	return

    def check_type(self, value, identifier, expected):
	"""checks type of the value
	Parameters
	----------

	value: object
	  thing to check
	identifier: string
	  id for error messages
	expected: type
	  what the value should be

	Raises
	------
	TypeError if type of value is not expected
	"""
	if not isinstance(value, expected):
	    raise TypeError("{0} must be {1} not {2}".format(identifier,
							     expected,
							     value))
	return

    def __call__(self):
	"""checks all the attributes are set

	Raises
	------

	TypeError if an attribute is None
	"""
	for attribute in self.attributes:
	    if getattr(self, attribute) is None:
		raise TypeError("{0} must be set, not None".format(attribute))
	return self

Boid Node

the Constructor

class Boid(CocosNode):
    """represents a boid
    Parameters
    ----------

    settings: BoidSettings
      settings for this node
    """
    def __init__(self, settings):
	super(Boid, self).__init__()
	self.settings = settings
	self.position = settings.position
	self.velocity = Vector2(0, 0)
	self.speed = settings.speed
	self.slowing_radius = settings.slowing_radius
	self.max_force = settings.max_force
	self.max_velocity = settings.max_velocity
	self.target = None

I'm not a fan of method calls in the constructor, but these next two lines help set up the node.

self.add(particle_system.Sun())
self.schedule(self.update)
return

The add method sets the Sun instance as a child of the Boid node and the schedule method sets the Boid's update method to be called once per frame.

The Current Position

This is just a convenience attribute. It probably takes a performance hit, but the original code was a little obscure so I thought I'd pull it out to document it.

@property
def current_position(self):
    """this node's position

    Returns
    -------

    Vector2: the current of this node
    """
    return Vector2(self.x, self.y)

I had to look it up, since there's no setting of self.x or self.y here - these two attributes are built into the CocosNode object and are always the current values.

The Update Method

def update(self, delta):
    """updates the current position

    Parameters
    ----------

    delta: float
      seconds since the last clock tick
    """
    if self.target is None:
	return

The target is going to be set when the mouse is move. Because of this it's initially not set, so we need to short circuit if that's the case.

distance = self.target - self.current_position
ramp_down = min(distance.magnitude()/self.slowing_radius, 1)
steering_force = distance * self.speed * ramp_down - self.velocity
steering_force = self.limit(steering_force, self.max_force)
self.velocity = self.limit(self.velocity + steering_force,
			   self.max_velocity)
self.position += self.velocity * delta
return

In the snippet above, distance is a vector with the tail on our current position and the head on the target. The steering-force is created by scaling the distance by our speed and then subtracting our current velocity, creating a vector that over-compensates to turn us toward the target. The new velocity is our old velocity plus the steering-force and our position is updated to be our new velocity times the elapsed time. That doesn't look like it's doing anything, but position is another special attribute on the CocosNode.

This next method reduces a vector if its magnitude is above a given threshold.

def limit(self, vector, upper_bound):
    """limits magnitude of vector

    Re-scales all values in the vector if the magnitude exceeds limit

    Parameters
    ----------

    vector:
      vector to check

    upper_bound: number
      upper limit for magnitude of vector

    Returns
    -------

    vector whose magnitude is no greater than upper_bound
    """
    try:
	magnitude = vector.magnitude()
    except OverflowError:
	print(vector)
	raise
    return (vector if magnitude <= upper_bound
	    else vector*(upper_bound/magnitude))

Main Layer

This is the class to act as the event handler.

class MainLayer(cocos.layer.Layer):
    """sets up the interaction

    Parameters
    ----------

    boids: collection
      boids to maintain
    """
    is_event_handler = True

That value (is_event_handler) has to be true or the Layer class doesn't handle events.

def __init__(self, boids):
    super(MainLayer, self).__init__()
    self.boids = boids
    for boid in boids:
	self.add(boid)
    return

def on_mouse_motion(self, x, y, dx, dy):
    """sets the boids' targets

    Parameters
    ----------

    x, y:
      current mouse cursor position

    dx, dy:
      change in position since the last report
    """
    for boid in self.boids:
	boid.target = Vector2(x, y)
    return

def on_mouse_press(self, x, y, button, modifiers):
    """handles mouse-clicks"""
    for boid in self.boids:
	boid.speed *= CHANGE_BEHAVIOR
    return

The on_mouse_motion method is a pass through to pyglet so the method is documented there more than on the cocos2d site.

if __name__ == "__main__":
    boid_settings = (BoidSettings()
		     .x_y((300, 200))
		     .velocity_vector(Vector2())
		     .speed_scalar(10)
		     .slow_down_distance(200)
		     .maximum_force(5)
		     .maximum_velocity(2000)())
    cocos.director.director.init(caption="Seeker")
    boid = Boid(boid_settings)
    scene = cocos.scene.Scene(MainLayer([boid]))
    cocos.director.director.run(scene)

The first thing to note is that the cocos.director.init function has to be called before any of the other cocos2D objects are created. If you move the instantiation of the Boid above that line, for instance, it will crash with a AttributeError: 'Director' object has no attribute '_window_virtual_width' error.