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.