# -*- coding: utf-8 -*-
""" Kinematic movement
This module implements a series of classes and methods that emulate
the behavior of objects moving in a 2D space in a kinematic way
(involving acceleration)
Notes
-----
This might need a slightly better explaination
"""
import math
import random
import pygame
import pygame.gfxdraw
from pygame_ai import colors
from pygame_ai.utils import math_utils
from pygame_ai.utils.list_utils import remove_if_exists
from pygame_ai.gameobject import DummyGameObject
[docs]class SteeringOutput(object):
""" Container for Steering data
This class is used as a container for the output of the
:py:class:`KinematicSteeringBehavior` algorithms.
These objects can be added, multiplied, and compared to eachother. Each
of these operations will be executed element-wise
Parameters
----------
linear : :pgmath:`Vector2`, optional
Linear acceleration, defaults to (0, 0)
angular : int, optional
Angular acceleration, defaults to 0
Attributes
----------
linear : :pgmath:`Vector2`
Linear acceleration
angular : int
Angular acceleration
"""
def __init__(self, linear = None, angular = None):
if linear is None:
linear = pygame.Vector2(0, 0)
self.linear = linear
if angular is None:
angular = 0
self.angular = angular
[docs] def update(self, gameobject, tick):
""" Update a :py:class:`~gameobject.GameObject`'s velocity and rotation
This method should be called once per loop, it updates the given
:py:class:`~gameobject.GameObject`'s velocity and rotation based
on this :py:class:`SteeringOutput`'s acceleration request
Parameters
----------
gameobject : :py:class:`~gameobject.GameObject`
The Game Objectthat will be updated
tick : int
Time transcurred since last loop
"""
gameobject.velocity += self.linear * tick
gameobject.rotation += self.angular * tick
if gameobject.velocity.length() > gameobject.max_speed:
gameobject.velocity.normalize_ip()
gameobject.velocity *= gameobject.max_speed
def copy(self):
selfcopy = SteeringOutput()
selfcopy.linear = pygame.Vector2(self.linear[0], self.linear[1])
selfcopy.angular = self.angular
return selfcopy
def reset(self):
self.linear[0], self.linear[1] = 0, 0
self.angular = 0
def __repr__(self):
return 'linear: {} angular: {}'.format(self.linear, self.angular)
def __eq__(self, other):
return self.linear == other.linear and self.angular == other.angular
# These might need to be modified
def __mul__(self, number):
new_steering = SteeringOutput()
new_steering.linear = pygame.Vector2(self.linear[0], self.linear[1]) * number
new_steering.angular = self.angular * number
return new_steering
def __rmul__(self, number):
return self.__mul__(number)
def __add__(self, other):
new_steering = SteeringOutput()
new_steering.linear += self.linear + other.linear
new_steering.angular += self.angular + other.angular
return new_steering
def __neg__(self):
new_steering = SteeringOutput()
new_steering.linear = -self.linear
new_steering.angular = -self.angular
return new_steering
null_steering = SteeringOutput(linear = pygame.Vector2(0, 0), angular = 0)
""":py:class:`SteeringOutput` : Constant with 0 linear acceleration and 0 angular acceleration """
def negative_steering(linear, angular):
""" Returns a steering request opposite to the linear and
angular accelerations provided.
Parameters
----------
linear : :pgmath:`Vector2`
Linear acceleration
angular : int
Angular acceleration
Returns
-------
:py:class:`SteeringOutput`
"""
neg_steering = SteeringOutput()
neg_steering.linear = -linear
neg_steering.angular = -angular
return neg_steering
[docs]class KinematicSteeringBehavior(object):
""" Template KinematicSteeringBehavior class
This class is a template to supply base methods for KinematicSteeringBehaviors.
This class is meant to be subclassed since the methods here are just placeholders
"""
def __repr__(self):
""" If not overriden, returns class name """
return type(self).__name__
[docs] def draw_indicators(self, screen, offset = (lambda pos: pos)):
""" Draws appropiate indicators for each :py:class:`KinematicSteeringBehavior`
Parameters
----------
screen: :pgsurf:`Surface`
Surface in which to draw indicators, normally this would be the screen Surface
offset: function, optional
Function that applies an offset to the object's position
This is meant to be used together with scrolling cameras,
leave empty if your game doesn't implement one,it defaults
to a linear function f(pos) -> pos
"""
pass
[docs] def get_steering(self):
""" Returns a steering request
Returns
-------
:py:class:`SteeringOutput`
Requested steering
"""
return null_steering.copy()
[docs]class Seek(KinematicSteeringBehavior):
""" :py:class:`KinematicSteeringBehavior` that makes the character **Seek** a target
Parameters
----------
character: :py:class:`~gameobject.GameObject`
Character with this behavior
target: :py:class:`~gameobject.GameObject`
Target to **Seek**
"""
def __init__(self, character, target):
self.character = character
self.target = target
self.steering = SteeringOutput()
def draw_indicators(self, screen, offset = lambda pos: pos):
start = offset(self.character.position)
end = start + self.steering.linear
pygame.draw.line(screen, colors.GREEN, start, end)
def get_steering(self):
# Get direction to the target
self.steering.linear = self.target.position - self.character.position
# Velocity is along this direction at full speed
if(math_utils.is_not_null(self.steering.linear)):
self.steering.linear.normalize_ip()
self.steering.linear *= self.character.max_accel
self.steering.angular = 0
return self.steering
[docs]class Flee(KinematicSteeringBehavior):
""" :py:class:`KinematicSteeringBehavior` that makes the character **Flee** from a target
Parameters
----------
character: :py:class:`~gameobject.GameObject`
Character with this behavior
target: :py:class:`~gameobject.GameObject`
Target to **Flee** from
"""
def __init__(self, character, target):
self.character = character
self.target = target
self.steering = SteeringOutput()
def draw_indicators(self, screen, offset = lambda pos: pos):
start = offset(self.character.position)
end = start + self.steering.linear
pygame.draw.line(screen, colors.GREEN, start, end)
def get_steering(self):
# Get direction to the target
self.steering.linear = self.character.position - self.target.position
# Velocity is along this direction at full speed
if(math_utils.is_not_null(self.steering.linear)):
self.steering.linear.normalize_ip()
self.steering.linear *= self.character.max_accel
return self.steering
[docs]class Arrive(KinematicSteeringBehavior):
""" :py:class:`KinematicSteeringBehavior` that makes the character **Arrive** at a target
Parameters
----------
character: :py:class:`~gameobject.GameObject`
Character with this behavior
target: :py:class:`~gameobject.GameObject`
Target to **Arrive** at
target_radius: int, optional
Distance from the center of the target at which the character will stop
slow_radius: int, optional
Distance from the center of the target at which the character will start to slow down
time_to_target: float, optional
Estimated time, in seconds, to **Arrive** at the target
"""
def __init__(self, character, target, target_radius = None, slow_radius = None, time_to_target = 0.2):
# Complete unprovided values
if target_radius is None:
target_radius = int(math.sqrt((target.rect.height/2)**2 + (target.rect.width/2)**2)*2)
if slow_radius is None:
slow_radius = target_radius * 5
self.character = character
self.target = target
self.time_to_target = time_to_target
self.target_radius = target_radius
self.slow_radius = slow_radius
self.steering = SteeringOutput()
def draw_indicators(self, screen, offset = lambda pos: pos):
start = offset(self.character.position)
end = start + self.steering.linear
pygame.draw.line(screen, colors.GREEN, start, end)
x, y = offset(self.target.position)
pygame.gfxdraw.aacircle(screen, int(x), int(y), self.target_radius, (255, 0, 0))
pygame.gfxdraw.aacircle(screen, int(x), int(y), self.slow_radius, (0, 0, 255))
def get_steering(self):
# Direction to target
direction = self.target.position - self.character.position
distance = direction.length()
# If we are outside slow radius, go at max_speed
if distance > self.slow_radius:
target_speed = self.character.max_speed
# If we are within target radius, make speed 0
elif distance <= self.target_radius:
self.steering = null_steering.copy()
return self.steering
# Determine 'slow' speed based on distance
else:
target_speed = self.character.max_speed*distance/self.slow_radius
# Target velocity combines target_speed and direction
target_velocity = direction
if math_utils.is_not_null(target_velocity):
target_velocity.normalize_ip()
target_velocity *= target_speed
# Finally, acceleration tries to get to target_velocity
self.steering.linear = target_velocity - self.character.velocity
self.steering.linear /= self.time_to_target
# Clip acceleration
if self.steering.linear.length() > self.character.max_accel:
self.steering.linear.normalize_ip()
self.steering.linear *= self.character.max_accel
return self.steering
[docs]class Align(KinematicSteeringBehavior):
""" :py:class:`KinematicSteeringBehavior` that makes the character **Align** with the target's orientation
Parameters
----------
character: :py:class:`~gameobject.GameObject`
Character with this behavior
target: :py:class:`~gameobject.GameObject`
Target to **Align** with it's orientation at
target_radius: int, optional
Distance, in degrees, from the target orientation at which the character will stop rotation
slow_radius: int, optional
Distance, in degrees, from the target orientation at which the character will start to slow rotation
time_to_target: float, optional
Estimated time, in seconds, to Align with the target's orientation
"""
def __init__(self, character, target, target_radius = 1, slow_radius = 20, time_to_target = 0.1):
self.character = character
self.target = target
self.time_to_target = time_to_target
self.target_radius = target_radius
self.slow_radius = slow_radius
self.steering = SteeringOutput()
def draw_indicators(self, screen, offset = (lambda pos: pos)):
start = offset(self.character.position)
factor = math_utils.get_bound_radius(self.character.rect) + 10
end = start + math_utils.orientation_asvector(self.character.orientation + self.steering.angular)*factor
pygame.draw.line(screen, colors.BLUE, start, end)
def get_steering(self):
# Get naive direction to target orientation
rotation = self.target.orientation - self.character.orientation
# Map result to (-pi, pi)
rotation = math_utils.map_to_range(rotation)
rotation_size = abs(rotation)
# If we're there, return no steering
if rotation_size < self.target_radius:
target_rotation = 0
# If outside slow_radius, use max_rotation
elif rotation_size > self.slow_radius:
target_rotation = self.character.max_rotation
target_rotation *= rotation / rotation_size
# Otherwise calculate appropiate target rotation
else:
target_rotation = self.character.max_rotation * rotation_size / self.slow_radius
target_rotation *= rotation / rotation_size
# Calculate aprropiate angular accel to reach this rotation
self.steering.angular = target_rotation - self.character.rotation / self.time_to_target
# Clip if it's too large
angular_accel = abs(self.steering.angular)
if angular_accel > self.character.max_angular_accel:
self.steering.angular /= angular_accel
self.steering.angular *= self.character.max_angular_accel
return self.steering
[docs]class VelocityMatch(KinematicSteeringBehavior):
""" :py:class:`KinematicSteeringBehavior` that makes the character match the velocity of the target
Parameters
----------
character: :py:class:`~gameobject.GameObject`
Character with this behavior
target: :py:class:`~gameobject.GameObject`
Target to match it's velocity
time_to_target: float, optional
Estimated time, in seconds, to reach the target's velocity
"""
def __init__(self, character, target, time_to_target = 0.1):
self.character = character
self.target = target
self.time_to_target = time_to_target
self.steering = SteeringOutput()
def draw_indicators(self, screen, offset = lambda pos: pos):
start = offset(self.character.position)
end = start + self.steering.linear
pygame.draw.line(screen, colors.GREEN, start, end)
def get_steering(self):
# Acceleration tries to get to the target velocity
self.steering.linear = self.target.velocity - self.character.velocity
self.steering.linear /= self.time_to_target
# Clip acceleration if it's too large
if self.steering.linear.length() > self.character.max_accel:
self.steering.linear.normalize_ip()
self.steering.linear *= self.character.max_accel
return self.steering
[docs]class Pursue(KinematicSteeringBehavior):
""" :py:class:`KinematicSteeringBehavior` that makes the character **Purse** the target
This behavior tries to predict the target's future position based on
the direction it is currently moving, and then :py:class:`Seek` s that
Parameters
----------
character: :py:class:`~gameobject.GameObject`
Character with this behavior
target: :py:class:`~gameobject.GameObject`
Target to **Pursue**
max_prediction_time: float, optional
Maximum time, in seconds, to look ahead while predicting future position
"""
def __init__(self, character, target, max_prediction_time = 0.2):
self.character = character
self.target = target
self.max_prediction_time = max_prediction_time
self.seek = Seek(self.character, DummyGameObject())
def draw_indicators(self, screen, offset = lambda pos: pos):
self.seek.draw_indicators(screen, offset)
def get_steering(self):
# Calculate target to delegate to Seek
# Calculate distance to target
direction = self.target.position - self.character.position
distance = direction.length()
# Calculate current speed
speed = self.character.velocity.length()
# Check if speed is too small to give reasonable prediction
if speed <= distance/self.max_prediction_time:
prediction_time = self.max_prediction_time
else:
prediction_time = distance/speed
# Update target coordinates
self.seek.target.position = self.target.position
self.seek.target.position += self.target.velocity * prediction_time
return self.seek.get_steering()
[docs]class Evade(KinematicSteeringBehavior):
""" :py:class:`KinematicSteeringBehavior` that makes the character **Evade** the target
This behavior tries to predict the target's future position based on
the direction it is currently moving, and then :py:class:`Flee` s from that
Parameters
----------
character: :py:class:`~gameobject.GameObject`
Character with this behavior
target: :py:class:`~gameobject.GameObject`
Target to **Evade**
max_prediction_time: float, optional
Maximum time, in seconds, to look ahead while predicting future position
"""
def __init__(self, character, target, max_prediction_time = 0.2):
self.character = character
self.target = target
self.max_prediction_time = max_prediction_time
self.flee = Flee(self.character, DummyGameObject())
def draw_indicators(self, screen, offset = lambda pos: pos):
self.flee.draw_indicators(screen, offset)
def get_steering(self):
# Calculate target to delegate to Seek
# Calculate distance to target
direction = self.target.position - self.character.position
distance = direction.length()
# Calculate current speed
speed = self.character.velocity.length()
# Check if speed is too small to give reasonable prediction
if speed <= distance/self.max_prediction_time:
prediction_time = self.max_prediction_time
else:
prediction_time = distance/speed
# Update target coordinates
self.flee.target.position = self.target.position
self.flee.target.position += self.target.velocity * prediction_time
return self.flee.get_steering()
[docs]class Face(KinematicSteeringBehavior):
""" :py:class:`KinematicSteeringBehavior` that makes the character **Face** the target
This behavior creates a :py:class:`~gameobject.DummyGameObject` that
is looking in the direction of the target and then :py:class:`Align` s
with that dummy's orientation
Parameters
----------
character: :py:class:`~gameobject.GameObject`
Character with this behavior
target: :py:class:`~gameobject.GameObject`
Target to **Face**
target_radius: int, optional
Distance, in degrees, from the target orientation at which the character will stop rotation
slow_radius: int, optional
Distance, in degrees, from the target orientation at which the character will start to slow rotation
time_to_target: float, optional
Estimated time, in seconds, to **Face** the target
"""
def __init__(self, character, target, target_radius = 1, slow_radius = 10, time_to_target = 0.1):
self.character = character
self.target = target
self.align = Align(self.character, DummyGameObject(), target_radius, slow_radius, time_to_target)
def draw_indicators(self, screen, offset = (lambda pos: pos)):
self.align.draw_indicators(screen, offset)
def get_steering(self):
# Calculate target to delegate to Align
# Calculate direction to target
direction = self.target.position - self.character.position
# If distance is 0, set dummy target to original target position
if direction.length() == 0:
self.align.target.orientation = self.target.orientation
# Otherwise calculate orientation based in target direction
else:
self.align.target.orientation = math_utils.get_angle_from_vector(direction)
return self.align.get_steering()
[docs]class LookWhereYoureGoing(KinematicSteeringBehavior):
""" :py:class:`KinematicSteeringBehavior` that makes the character **Look Where He's Going**
This behavior makes the character face in the direction it's moving
by creating a :py:class:`~gameobject.DummyGameObject` that
is looking in the direction of the character's velocity and then
it :py:class:`Align` s with that.
This behavior is meant to be used in combination with other behaviors,
see :py:class:`steering.blended.BlendedSteering` .
Parameters
----------
character: :py:class:`~gameobject.GameObject`
Character with this behavior
target_radius: int, optional
Distance, in degrees, from the target orientation at which the character will stop rotation
slow_radius: int, optional
Distance, in degrees, from the target orientation at which the character will start to slow rotation
time_to_target: float, optional
Estimated time, in seconds, to **LookWhereYoureGoing**
"""
def __init__(self, character, target_radius = 1, slow_radius = 20, time_to_target = 0.1):
self.character = character
self.align = Align(self.character, DummyGameObject(), target_radius, slow_radius, time_to_target)
def get_steering(self):
# If no velocity, return null steering
if self.character.velocity.length() == 0:
return null_steering.copy()
# Calculate target orientation based on character velocity
self.align.target.orientation = math_utils.get_angle_from_vector(self.character.velocity)
return self.align.get_steering()
[docs]class Wander(KinematicSteeringBehavior):
""" :py:class:`KinematicSteeringBehavior` that makes the character **Wander**
This behavior makes the character move with it's maximum speed in a
random direction that feels smooth, meaning that it does not rotate
too abruptly. This generates a target in front of the character and
:py:class:`Seek` s it while applying `:py:class:`LookWhereYoureGoing`,
you can use the :py:meth:`KinematicSteeringBehavior.draw_indicators()`
to see how the target is generated. This Behavior also uses
:py:class:`LookWhereYoureGoing`.
Parameters
----------
character: :py:class:`~gameobject.GameObject`
Character with this behavior
wander_offset: int, optional
Distance in front of the character to generate target to :py:class:`Seek`
wander_radius: int, optional
Radius of the circumference in front of the character in which the target will generated
wander_rate: int, optional
Angles, in degrees, that the target is allowed to move along the circumference
align_target_radius: int, optional
Distance, in degrees, from the target orientation at which the character will stop rotation
slow_radius: int, optional
Distance, in degrees, from the target orientation at which the character will start to slow rotation
align_time: float, optional
Estimated time, in seconds, to **LookWhereYoureGoing**
"""
def __init__(self, character, wander_offset = 50, wander_radius = 15, wander_rate = 20):
#align_target_radius = 1, align_slow_radius = 10, align_time = 0.2,
#draw = True):
self.character = character
self.wander_offset = wander_offset
self.wander_radius = wander_radius
self.wander_rate = wander_rate
self.wander_orientation = self.character.orientation
self.seek = Seek(self.character, DummyGameObject())
def draw_indicators(self, screen, offset = lambda pos: pos):
x, y = offset(self.seek.target.position)
pygame.gfxdraw.filled_circle(screen, int(x), int(y), 2, (255, 0, 0))
x, y = offset(self.character.position + (math_utils.orientation_asvector(self.character.orientation) * self.wander_offset))
pygame.gfxdraw.aacircle(screen, int(x), int(y), self.wander_radius, (0, 255, 0))
def get_steering(self):
# Calculate target to delegate to seek
# Update wander orientation
self.wander_orientation += math_utils.random_binomial() * self.wander_rate
# Calculate combined target_orientation
target_orientation = self.wander_orientation + self.character.orientation
# Calculate center of wander circle
target_position = self.character.position + (math_utils.orientation_asvector(self.character.orientation) * self.wander_offset)
# Calculate target location
target_position += math_utils.orientation_asvector(target_orientation) * self.wander_radius
# Delegate to seek
self.seek.target.position = target_position
return self.seek.get_steering()
[docs]class FollowPath(KinematicSteeringBehavior):
""" :py:class:`KinematicSteeringBehavior` that makes the character **Follow a Path**
This behavior makes the character follow a particular
:py:class:`~.path.Path`. It will do so until the character
has traversed all points in it.
Parameters
----------
character: :py:class:`~gameobject.GameObject`
Character with this behavior
path: :py:class:`steering.path.Path`
Path that will be Followed
"""
def __init__(self, character, path):
self.character = character
self.path = path
self.seek = Seek(self.character, DummyGameObject())
self.seek.target.position = next(self.path)
def draw_indicators(self, screen, offset = lambda pos: pos):
self.seek.draw_indicators(screen, offset)
for point in self.path.as_list():
x, y = offset(point)
pygame.gfxdraw.filled_circle(screen, int(x), int(y), 2, (0, 0, 255))
x, y = offset(self.seek.target.position)
pygame.gfxdraw.filled_circle(screen, int(x), int(y), 3, (255, 0, 0))
def __repr__(self):
return 'FollowPath ' + str(self.path)
def get_steering(self):
# Calculate to delegate to seek
self.seek.target.position = self.path.current()
distance = (self.character.position - self.seek.target.position).length()
if distance < 50: # THIS MIGHT NEED SOME REVISIOn
try:
self.seek.target.position = next(self.path)
except StopIteration:
return null_steering.copy()
# Delegate to Seek
return self.seek.get_steering()
[docs]class Separation(KinematicSteeringBehavior):
""" :py:class:`KinematicSteeringBehavior` that makes the character **Separate** itself from a list of targets
Parameters
----------
character: :py:class:`~gameobject.GameObject`
Character with this behavior
targets: list(:py:class:`~gameobject.GameObject`)
Targets to stay separated from
treshold : int, optional
Distance from any of the targets at which the character will start separate from them
"""
def __init__(self, character, targets, treshold = None):
# Complete unprovided values
if treshold is None:
treshold = int(math.sqrt((character.rect.height/2)**2 + (character.rect.width/2)**2)*3)
self.character = character
self.targets = targets
self.treshold = treshold
self.steering = SteeringOutput()
def draw_indicators(self, screen, offset = lambda pos: pos):
start = offset(self.character.position)
end = start + self.steering.linear
pygame.draw.line(screen, colors.GREEN, start, end)
x, y = offset(self.character.position)
pygame.gfxdraw.aacircle(screen, int(x), int(y), self.treshold, (255, 0, 0))
def get_steering(self):
for target in self.targets:
self.steering = SteeringOutput()
# Check if target is close
direction = self.character.position - target.position
distance = direction.length()
# Make sure there's a direction
if distance == 0:
direction[1] = -1
if distance < self.treshold:
# Calculate strength of repulsion
strength = self.character.max_accel * (self.treshold - distance) / self.treshold
# Add the acceleration
direction.normalize_ip()
self.steering.linear += strength * direction
return self.steering
[docs]class CollisionAvoidance(KinematicSteeringBehavior):
""" :py:class:`KinematicSteeringBehavior` that makes the character **Avoid Collision** with a list of targets
This behavior looks at the velocities of the character and the targets
to determine if they will collide in the next few loops, and if they will,
it accelerates away from the collision point
This behavior is meant to be used in combination with other behaviors,
see :py:class:`steering.blended.BlendedSteering` .
Parameters
----------
character: :py:class:`~gameobject.GameObject`
Character with this behavior
targets: list(:py:class:`~gameobject.GameObject`)
Targets to avoid collision with
radius : int, optional
Distance at which the future positions of the character and any
target are are considered as *colliding*
"""
def __init__(self, character, targets, radius = None):
# Complete unprovided values
if radius is None:
radius = int(math_utils.get_bound_radius(character.rect)*2)
self.character = character
self.targets = targets
self.radius = radius
self.steering = SteeringOutput()
def draw_indicators(self, screen, offset = lambda pos: pos):
start = offset(self.character.position)
end = start + self.steering.linear
pygame.draw.line(screen, colors.GREEN, start, end)
x, y = offset(self.character.position + self.character.velocity)
pygame.gfxdraw.aacircle(screen, int(x), int(y), self.radius, (255, 0, 0))
for target in self.targets:
x, y = offset(target.position + target.velocity)
pygame.gfxdraw.aacircle(screen, int(x), int(y), self.radius, (255, 0, 0))
def get_steering(self):
# Calculate future character pos
char_future_pos = self.character.position + self.character.velocity
closest_target = None
# See if any target comes close enough
min_distance = float('inf')
for target in self.targets:
target_future_pos = target.position + target.velocity
relative_pos = char_future_pos - target_future_pos
distance = relative_pos.length()
if distance > self.radius:
continue
if distance < min_distance:
closest_target = target
closest_relative_pos = relative_pos
min_distance = distance
# If no target is close enough, return no sterring
if closest_target is None:
self.steering = null_steering.copy()
return self.steering
# Otherwise, calculate avoidance path
if math_utils.is_not_null(closest_relative_pos):
closest_relative_pos.normalize_ip()
self.steering.linear = closest_relative_pos * self.character.max_accel
return self.steering
[docs]class ObstacleAvoidance(KinematicSteeringBehavior):
""" :py:class:`KinematicSteeringBehavior` that makes the character **Avoid Obstacles**
This behavior looks ahead in the current direction the character is
moving to see if it will collide with any obstacle, and if it does,
creates a target *away* from the collision point and :py:class:`Seek` s that.
The difference between this and :py:class:`CollisionAvoidance` is that
the **Obstacles** are considered to be a rectangular shape of a any size,
while the targets are normally almost-square-sized.
This behavior is meant to be used in combination with other behaviors,
see :py:class:`steering.blended.BlendedSteering` .
Parameters
----------
character: :py:class:`~gameobject.GameObject`
Character with this behavior
obstacles: list(:py:class:`~gameobject.GameObject`)
Obstacles to avoid collision with
avoid_distance: int, optional
Distance from the collision point at which the target that
the algorithm uses to avoid collision will be generated
lookahead: int, optional
Distance to *look ahead* in the direction of the player's velocity
"""
def __init__(self, character, obstacles, avoid_distance = None, lookahead = None):
# Complete unprovided values
if lookahead is None:
lookahead = int(math_utils.get_bound_radius(character.rect)*4)
if avoid_distance is None:
avoid_distance = int(math_utils.get_bound_radius(character.rect)*3)
self.character = character
self.obstacles = obstacles
self.avoid_distance = avoid_distance
self.lookahead = lookahead
self.seek = Seek(self.character, DummyGameObject())
# For indicator drawing
self.closest_intersection = None
def draw_indicators(self, screen, offset = lambda pos: pos):
# Velocity
self.seek.draw_indicators(screen, offset)
# Ray vector
start = offset(self.character.position)
if math_utils.is_not_null(self.character.velocity):
end = start + self.character.velocity.normalize() * self.lookahead
else:
end = start + self.character.velocity * self.lookahead
pygame.draw.line(screen, colors.YELLOW, start, end)
# Intersection point
if self.closest_intersection:
x, y = offset(self.closest_intersection)
pygame.gfxdraw.filled_circle(screen, int(x), int(y), 2, colors.RED)
# Seek target
x, y = offset(self.seek.target.position)
pygame.gfxdraw.filled_circle(screen, int(x), int(y), 2, colors.BLUE)
def get_steering(self):
# Cast ray vector
ray_vector = pygame.Vector2(self.character.velocity)
if math_utils.is_not_null(ray_vector):
ray_vector.normalize_ip()
ray_vector *= self.lookahead
closest_intersection = None
# Look for the closest collision
closest_distance = float('inf')
closest_intersection = None
closest_intersection_line = None
for obstacle in self.obstacles:
for line in obstacle.get_lines():
ray_line = [self.character.position, self.character.position + ray_vector]
intersection = math_utils.lines_intersect(ray_line, line)
if intersection is not None:
distance = (intersection - self.character.position).length()
if distance < closest_distance:
closest_distance = distance
closest_intersection = intersection
closest_intersection_line = line
self.closest_intersection = closest_intersection
# If there was no collision return null steering
if closest_intersection is None:
return null_steering.copy()
# Otherwise, calculate target to delegate to Seek
# Get RHS and LHS perpendicular points and see which is closer
# to the player
p1, p2 = math_utils.get_perpendicular(closest_intersection_line)
p1.normalize_ip()
p2.normalize_ip()
p1 = closest_intersection + p1*self.avoid_distance
p2 = closest_intersection + p2*self.avoid_distance
dis1 = (p1 - self.character.position).length()
dis2 = (p2 - self.character.position).length()
closest = p1 if dis1 < dis2 else p2
# Delegate to seek
self.seek.target.position = closest
return self.seek.get_steering()
[docs]class NullSteering(KinematicSteeringBehavior):
""" :py:class:`KinematicSteeringBehavior` that makes the character **Stay Still** """
def get_steering(self):
return null_steering.copy()
class Stationary(KinematicSteeringBehavior):
def __init__(self, character):
self.character = character
self.steering = SteeringOutput()
def get_steering(self):
self.steering.linear = -self.character.velocity
if math_utils.is_not_null(self.steering.linear):
self.steering.linear.normalize_ip()
self.steering.linear *= self.character.max_accel
return self.steering
[docs]class Drag(KinematicSteeringBehavior):
""" :py:class:`KinematicSteeringBehavior` that applies a **Drag** to the character
This behavior should be applied to every :py:class:`~gameobject.GameObject`
in every loop (unless it's meant to be permanently stationary). It
applies an acceleration contrary to it's current linear and angular
velocity.
Parameters
----------
strenght: float, optional
The strength of the drag to apply, should be a number in the
range (0, 1], any number outside of that range will have
unexpected behavior.
"""
def __init__(self, linear_strength = 10, angular_strength = 1):
self.linear_strength = linear_strength
self.angular_strength = angular_strength
self.steering = SteeringOutput()
def get_steering(self, character):
""" Returns steering with a character's drag
Parameters
----------
character: :py:class:`~gameobject.GameObject`
"""
if math_utils.is_not_null(character.velocity):
linear_direction = -character.velocity
linear_direction.normalize_ip()
self.steering.linear = linear_direction * self.linear_strength
else:
self.steering.linear = pygame.Vector2(0, 0)
if character.rotation == 0:
self.steering.angular = 0
else:
angular_direction = -character.rotation / abs(character.rotation)
self.steering.angular = angular_direction*self.angular_strength
return self.steering