steering_behaviors 1.0.4 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Changelog for steering-behaviors
2
2
 
3
+ ## 1.0.5, 11 August 2013
4
+ * Added 'separate', a predictive collision-avoidance steering behavior
5
+
3
6
  ## 1.0.4, 8 August 2013
4
7
  * Rewrote 'arrive' to the more canonical behavior described by Reynolds
5
8
 
data/README.md CHANGED
@@ -12,7 +12,7 @@ The steering behaviors expect to operate on a 'kinematic' thing. That is, the st
12
12
 
13
13
  ## Supported stering behaviors
14
14
 
15
- The library supports these steering behaviors:
15
+ The library supports a variety of steering behaviors. Some of these are the 'canonical' steering behaviors documented at Craig Reynold's website (see References, below). Others are behaviors documented in game programming books or behaviors that I have found useful for my own game programming needs:
16
16
 
17
17
  * Seek: aim for and reach the specified point at top speed
18
18
  * Flee: head away from the specified point at top speed (the opposite of Seek)
@@ -20,11 +20,7 @@ The library supports these steering behaviors:
20
20
  * Pursue: given a moving target, anticipate its future position and intercept the target intelligently
21
21
  * Evade: given a moving target, anticipate its future position and avoid the target intelligently (the opposite of Pursue)
22
22
  * Wander: move about the plain in a 'random walk' way
23
-
24
- These are all standard steering behaviors documented at Craig Reynold's website (see References, below).
25
-
26
- Additionally, this library supports some other useful behaviors:
27
-
23
+ * Separate: predictively steer to avoid collision with another agent, based on the closest point of approach
28
24
  * Align: align my course with the target's course, without altering my speed
29
25
  * Match: match my course and speed to those of the target
30
26
  * Broadside: expose my side to the target in the manner of a ship's broadside; useful for orbiting or exposing weapons, say
@@ -12,6 +12,12 @@ require 'steering_behaviors/match.rb'
12
12
  require 'steering_behaviors/orthogonal.rb'
13
13
  require 'steering_behaviors/pursue.rb'
14
14
  require 'steering_behaviors/seek.rb'
15
+ require 'steering_behaviors/separation.rb'
15
16
  require 'steering_behaviors/steering.rb'
16
17
  require 'steering_behaviors/vector.rb'
17
18
  require 'steering_behaviors/wander.rb'
19
+
20
+
21
+ SteeringBehaviors::VEC_ZERO = SteeringBehaviors::Vector.new(0, 0)
22
+ SteeringBehaviors::VEC_UP_NEGY = SteeringBehaviors::Vector.new(0, -1)
23
+ SteeringBehaviors::VEC_UP_POSY = SteeringBehaviors::Vector.new(0, 1)
@@ -11,15 +11,15 @@ class SteeringBehaviors::Align
11
11
  # Align with a moving target by observing its course.
12
12
  #
13
13
  # * *Args* :
14
- # - +hunter_kinematic+ -> pursuing kinematic
15
- # - +quarry_kinematic+ -> kinematic of the target
14
+ # - +character_kinematic+ -> kinematic of "our" character that is moving and aligning
15
+ # - +other_kinematic+ -> kinematic of the thing to align with
16
16
  # * *Returns* :
17
- # -
17
+ # - a steering force
18
18
  #
19
- def self.steer(hunter_kinematic, quarry_kinematic)
20
- desired_velocity = quarry_kinematic.heading_vec * hunter_kinematic.velocity_vec.length
19
+ def self.steer(character_kinematic, other_kinematic)
20
+ desired_velocity = other_kinematic.heading_vec * character_kinematic.velocity_vec.length
21
21
 
22
- return desired_velocity - hunter_kinematic.velocity_vec
22
+ return desired_velocity - character_kinematic.velocity_vec
23
23
  end
24
24
 
25
25
  end
@@ -7,31 +7,30 @@
7
7
  # the terms found in the "LICENSE" file included with the framework.
8
8
 
9
9
  class SteeringBehaviors::Arrive
10
- TARGET_RADIUS=0
11
-
12
10
  # Arrive 'gently' at the goal position by decelerating smoothly.
13
11
  #
14
12
  # * *Args* :
15
- # - +kinematic+ -> the thing that is moving and arriving
16
- # - +goal_position+ -> a Vector of position
17
- # - +slow_radius+ -> don't begin decelerating until inside this radius; max speed outside
13
+ # - +character_kinematic+ -> kinematic of "our" character that is moving and arriving
14
+ # - +goal_position+ -> a Vector of position at which to arrive
15
+ # - +slow_radius+ -> don't begin decelerating until inside this radius; maintain max speed outside
16
+ # - +arrive_radius+ -> consider ourselves 'arrived' when inside this radius
18
17
  # * *Returns* :
19
18
  # - a steering force
20
19
  #
21
- def self.steer(kinematic, goal_position, slow_radius=200)
22
- to_target = goal_position - kinematic.position_vec
20
+ def self.steer(character_kinematic, goal_position, slow_radius, arrive_radius=0)
21
+ to_target = goal_position - character_kinematic.position_vec
23
22
  dist = to_target.length
24
23
 
25
- if dist < TARGET_RADIUS
26
- return SteeringBehaviors::Vector.new(0,0)
24
+ if dist < arrive_radius
25
+ return SteeringBehaviors::VEC_ZERO
27
26
  elsif dist > slow_radius
28
- desired_speed = kinematic.max_speed
27
+ desired_speed = character_kinematic.max_speed
29
28
  else
30
- desired_speed = kinematic.max_speed * dist / slow_radius
29
+ desired_speed = character_kinematic.max_speed * dist / slow_radius
31
30
  end
32
31
 
33
32
  desired_vel = to_target.normalize * desired_speed
34
33
 
35
- return desired_vel - kinematic.velocity_vec
34
+ return desired_vel - character_kinematic.velocity_vec
36
35
  end
37
36
  end
@@ -12,24 +12,24 @@ class SteeringBehaviors::Broadside
12
12
  # exposing weapons, orbiting, etc.
13
13
  #
14
14
  # * *Args* :
15
- # - +hunter_kinematic+ -> pursuing kinematic
16
- # - +quarry_kinematic+ -> kinematic of the target
15
+ # - +character_kinematic+ -> kinematic of "our" character that is moving and broadsiding
16
+ # - +other_kinematic+ -> kinematic of thing to broadside to
17
17
  # * *Returns* :
18
18
  # - a steering force
19
19
  #
20
- def self.steer(hunter_kinematic, quarry_kinematic)
21
- to_quarry = (quarry_kinematic.position_vec - hunter_kinematic.position_vec).normalize
20
+ def self.steer(character_kinematic, other_kinematic)
21
+ to_quarry = (other_kinematic.position_vec - character_kinematic.position_vec).normalize
22
22
  option_a = SteeringBehaviors::Vector.new(to_quarry.y, -to_quarry.x)
23
23
  option_b = SteeringBehaviors::Vector.new(-to_quarry.y, to_quarry.x)
24
24
 
25
- da = option_a.delta(hunter_kinematic.heading_vec)
26
- db = option_b.delta(hunter_kinematic.heading_vec)
25
+ da = option_a.delta(character_kinematic.heading_vec)
26
+ db = option_b.delta(character_kinematic.heading_vec)
27
27
 
28
28
  best_hdg_vec = (da < db ? option_a : option_b)
29
29
 
30
- desired_velocity = best_hdg_vec.normalize * hunter_kinematic.velocity_vec.length
30
+ desired_velocity = best_hdg_vec.normalize * character_kinematic.velocity_vec.length
31
31
 
32
- return desired_velocity - hunter_kinematic.velocity_vec
32
+ return desired_velocity - character_kinematic.velocity_vec
33
33
  end
34
34
 
35
35
  end
@@ -8,6 +8,67 @@
8
8
 
9
9
  module SteeringBehaviors::Common
10
10
 
11
+ # Given two kinematics, determine how many seconds until their Closest Point of Approach (CPA)
12
+ #
13
+ # * *Args* :
14
+ # - +character_kinematic+ -> kinematic of "our" character that is moving
15
+ # - +other_kinematic+ -> kinematic of the other mover
16
+ # * *Returns* :
17
+ # - time in seconds until CPA; may be negative (for 'in the past')
18
+ #
19
+ def compute_nearest_approach_time(character_kinematic, other_kinematic)
20
+ relative_vel_vec = other_kinematic.velocity_vec - character_kinematic.velocity_vec
21
+ relative_speed = relative_vel_vec.length
22
+
23
+ return 0 if relative_speed==0
24
+
25
+ relative_tangent_vec = relative_vel_vec / relative_speed
26
+ relative_position_vec = character_kinematic.position_vec - other_kinematic.position_vec
27
+
28
+ projection = relative_tangent_vec.dot(relative_position_vec)
29
+ return projection / relative_speed
30
+ end
31
+
32
+ # Given two kinematics, determine where they will be at the time of closest approach.
33
+ #
34
+ # * *Args* :
35
+ # - +character_kinematic+ -> kinematic of "our" character that is moving
36
+ # - +other_kinematic+ -> kinematic of the other mover
37
+ # * *Returns* :
38
+ # - a tuple consisting of (time until CPA), (character position at CPA), (other position at CPA)
39
+ #
40
+ def compute_nearest_approach(character_kinematic, other_kinematic)
41
+ # How long until it happens?
42
+ cpa_time = compute_nearest_approach_time(character_kinematic, other_kinematic)
43
+
44
+ # How far will the two kinematics go in that time?
45
+ char_travel_vec = character_kinematic.velocity_vec * cpa_time
46
+ other_travel_vec = other_kinematic.velocity_vec * cpa_time
47
+
48
+ # Project forward from current positions...
49
+ char_pos_vec = char_travel_vec + character_kinematic.position_vec
50
+ other_pos_vec = other_travel_vec + other_kinematic.position_vec
51
+
52
+ return [cpa_time, char_pos_vec, other_pos_vec]
53
+ end
54
+
55
+ # Given two kinematics, determine how close the two agents will be at their
56
+ # time & place of closest approach. Largely a convenience method, since you
57
+ # could call compute_nearest_approach() and do the same subtraction
58
+ # just as easily.
59
+ #
60
+ # * *Args* :
61
+ # - +character_kinematic+ -> kinematic of "our" character that is moving
62
+ # - +other_kinematic+ -> kinematic of the other mover
63
+ # * *Returns* :
64
+ # - the distance between the two agents at their closest approach
65
+ #
66
+ def compute_nearest_approach_distance(character_kinematic, other_kinematic)
67
+ cpa_time, char_pos_vec, other_pos_vec = compute_nearest_approach(character_kinematic, other_kinematic)
68
+
69
+ (char_pos_vec - other_pos_vec).length
70
+ end
71
+
11
72
  # A support routine used by Pursue and Evade. Observes how 'forward' the
12
73
  # target is, and how 'parallel' its course is to our own.
13
74
  #
@@ -14,29 +14,29 @@ class SteeringBehaviors::Evade
14
14
  # See http://www.red3d.com/cwr/steer/
15
15
  #
16
16
  # * *Args* :
17
- # - +evading_kinematic+ -> evading kinematic
18
- # - +enemy_kinematic+ -> kinematic of the thing to evade
17
+ # - +character_kinematic+ -> kinematic of "our" character that is moving and evading
18
+ # - +other_kinematic+ -> kinematic of the thing to evade
19
19
  # * *Returns* :
20
20
  # - a steering force
21
21
  #
22
- def self.steer(evading_kinematic, enemy_kinematic)
23
- offset = enemy_kinematic.position_vec - evading_kinematic.position_vec
22
+ def self.steer(character_kinematic, enemy_kinematic)
23
+ offset = enemy_kinematic.position_vec - character_kinematic.position_vec
24
24
  direct_distance = offset.length
25
25
  unit_offset = offset / direct_distance
26
26
 
27
- parallelness = evading_kinematic.heading_vec.dot(enemy_kinematic.heading_vec)
28
- forwardness = unit_offset.dot(evading_kinematic.heading_vec)
27
+ parallelness = character_kinematic.heading_vec.dot(enemy_kinematic.heading_vec)
28
+ forwardness = unit_offset.dot(character_kinematic.heading_vec)
29
29
 
30
30
  gen, tf = compute_time_factor(forwardness, parallelness)
31
31
 
32
- direct_travel_time = direct_distance / evading_kinematic.speed
33
- direct_travel_time_2 = direct_distance / (evading_kinematic.speed + enemy_kinematic.speed)
32
+ direct_travel_time = direct_distance / character_kinematic.speed
33
+ direct_travel_time_2 = direct_distance / (character_kinematic.speed + enemy_kinematic.speed)
34
34
  estimated_time_enroute = direct_travel_time_2 * tf
35
35
 
36
36
  # printf "#{ipos.entity}'s target #{qpos.entity} is '#{gen}'. fness: %0.3f pness: %0.3f f: %0.3f p: %0.3f tf: %0.3f\n", forwardness, parallelness, f, p, tf
37
37
  predicted_pos_vec = enemy_kinematic.position_vec + (enemy_kinematic.velocity_vec * estimated_time_enroute)
38
38
 
39
- return [predicted_pos_vec, SteeringBehaviors::Flee.steer(evading_kinematic, predicted_pos_vec)]
39
+ return [predicted_pos_vec, SteeringBehaviors::Flee.steer(character_kinematic, predicted_pos_vec)]
40
40
  end
41
41
 
42
42
  end
@@ -12,13 +12,13 @@ class SteeringBehaviors::Flee
12
12
  # See http://www.red3d.com/cwr/steer/
13
13
  #
14
14
  # * *Args* :
15
- # - +kinematic+ -> the thing that is fleeing
15
+ # - +character_kinematic+ -> kinematic of "our" character that is moving and fleeing
16
16
  # - +flee_position+ -> the position-vector that we want to flee from
17
17
  # * *Returns* :
18
18
  # - the calculated steering force
19
19
  #
20
- def self.steer(kinematic, flee_position)
21
- best_velocity_to_target = (kinematic.position_vec - flee_position).normalize * kinematic.max_speed
22
- best_velocity_to_target - kinematic.velocity_vec
20
+ def self.steer(character_kinematic, flee_position)
21
+ desired_velocity = (character_kinematic.position_vec - flee_position).normalize * character_kinematic.max_speed
22
+ force = (desired_velocity - character_kinematic.velocity_vec)
23
23
  end
24
24
  end
@@ -11,15 +11,15 @@ class SteeringBehaviors::Match
11
11
  # Matches the target's course and speed.
12
12
  #
13
13
  # * *Args* :
14
- # - +hunter_kinematic+ -> my moving thing
15
- # - +quarry_kinematic+ -> kinematic of the target
14
+ # - +character_kinematic+ -> kinematic of "our" character that is moving and matching
15
+ # - +other_kinematic+ -> kinematic of the thing to match
16
16
  # * *Returns* :
17
17
  # - the steering force
18
18
  #
19
- def self.steer(hunter_kinematic, quarry_kinematic)
20
- desired_velocity = quarry_kinematic.heading_vec * quarry_kinematic.velocity_vec.length
19
+ def self.steer(character_kinematic, other_kinematic)
20
+ desired_velocity = other_kinematic.heading_vec * other_kinematic.velocity_vec.length
21
21
 
22
- return desired_velocity - hunter_kinematic.velocity_vec
22
+ return desired_velocity - character_kinematic.velocity_vec
23
23
  end
24
24
 
25
25
  end
@@ -11,23 +11,23 @@ class SteeringBehaviors::Orthogonal
11
11
  # Moves at a 90-degree angle to the target's course.
12
12
  #
13
13
  # * *Args* :
14
- # - +hunter_kinematic+ -> my moving thing
15
- # - +quarry_kinematic+ -> kinematic of the target
14
+ # - +character_kinematic+ -> kinematic of "our" character that is moving
15
+ # - +other_kinematic+ -> kinematic of the thing to match
16
16
  # * *Returns* :
17
- # -
17
+ # - a steering force
18
18
  #
19
- def self.steer(hunter_kinematic, quarry_kinematic)
20
- option_a = SteeringBehaviors::Vector.new(quarry_kinematic.heading_vec.y, -quarry_kinematic.heading_vec.x)
21
- option_b = SteeringBehaviors::Vector.new(-quarry_kinematic.heading_vec.y, quarry_kinematic.heading_vec.x)
19
+ def self.steer(character_kinematic, other_kinematic)
20
+ option_a = SteeringBehaviors::Vector.new(other_kinematic.heading_vec.y, -other_kinematic.heading_vec.x)
21
+ option_b = SteeringBehaviors::Vector.new(-other_kinematic.heading_vec.y, other_kinematic.heading_vec.x)
22
22
 
23
- da = option_a.delta(hunter_kinematic.heading_vec)
24
- db = option_b.delta(hunter_kinematic.heading_vec)
23
+ da = option_a.delta(character_kinematic.heading_vec)
24
+ db = option_b.delta(character_kinematic.heading_vec)
25
25
 
26
26
  best_hdg_vec = (da < db ? option_a : option_b)
27
27
 
28
- desired_velocity = best_hdg_vec * hunter_kinematic.velocity_vec.length
28
+ desired_velocity = best_hdg_vec * character_kinematic.velocity_vec.length
29
29
 
30
- return desired_velocity - hunter_kinematic.velocity_vec
30
+ return desired_velocity - character_kinematic.velocity_vec
31
31
  end
32
32
 
33
33
  end
@@ -14,31 +14,29 @@ class SteeringBehaviors::Pursue
14
14
  # See http://www.red3d.com/cwr/steer/
15
15
  #
16
16
  # * *Args* :
17
- # - +hunter_kinematic+ -> pursuing kinematic
18
- # - +quarry_kinematic+ -> kinematic of the target
17
+ # - +character_kinematic+ -> kinematic of "our" character that is moving and pursuing
18
+ # - +other_kinematic+ -> kinematic of the thing to pursue
19
19
  # * *Returns* :
20
- # -
21
- # * *Raises* :
22
- # - ++ ->
20
+ # - a steering force
23
21
  #
24
- def self.steer(hunter_kinematic, quarry_kinematic)
25
- offset = quarry_kinematic.position_vec - hunter_kinematic.position_vec
22
+ def self.steer(character_kinematic, other_kinematic)
23
+ offset = other_kinematic.position_vec - character_kinematic.position_vec
26
24
  direct_distance = offset.length
27
25
  unit_offset = offset / direct_distance
28
26
 
29
- parallelness = hunter_kinematic.heading_vec.dot(quarry_kinematic.heading_vec)
30
- forwardness = unit_offset.dot(hunter_kinematic.heading_vec)
27
+ parallelness = character_kinematic.heading_vec.dot(other_kinematic.heading_vec)
28
+ forwardness = unit_offset.dot(character_kinematic.heading_vec)
31
29
 
32
30
  gen, tf = compute_time_factor(forwardness, parallelness)
33
31
 
34
- direct_travel_time = direct_distance / hunter_kinematic.speed
35
- direct_travel_time_2 = direct_distance / (hunter_kinematic.speed + quarry_kinematic.speed)
32
+ direct_travel_time = direct_distance / character_kinematic.speed
33
+ direct_travel_time_2 = direct_distance / (character_kinematic.speed + other_kinematic.speed)
36
34
  estimated_time_enroute = direct_travel_time_2 * tf
37
-
38
35
  # printf "#{ipos.entity}'s target #{qpos.entity} is '#{gen}'. fness: %0.3f pness: %0.3f f: %0.3f p: %0.3f tf: %0.3f\n", forwardness, parallelness, f, p, tf
39
- predicted_pos_vec = quarry_kinematic.position_vec + (quarry_kinematic.velocity_vec * estimated_time_enroute)
36
+ predicted_pos_vec = other_kinematic.position_vec + (other_kinematic.velocity_vec * estimated_time_enroute)
40
37
 
41
- return [predicted_pos_vec, SteeringBehaviors::Seek.steer(hunter_kinematic, predicted_pos_vec)]
38
+ # puts "#{compute_nearest_approach_distance(character_kinematic, other_kinematic)} in #{compute_nearest_approach_time(character_kinematic, other_kinematic)} sec"
39
+ return [predicted_pos_vec, SteeringBehaviors::Seek.steer(character_kinematic, predicted_pos_vec)]
42
40
  end
43
41
 
44
42
 
@@ -12,13 +12,13 @@ class SteeringBehaviors::Seek
12
12
  # See http://www.red3d.com/cwr/steer/
13
13
  #
14
14
  # * *Args* :
15
- # - +kinematic+ -> my seeking thing
16
- # - +goal_position+ -> the position-vector where we want to go
15
+ # - +character_kinematic+ -> kinematic of "our" character that is moving and seeking
16
+ # - +goal_position+ -> the position-vector that we want to seek
17
17
  # * *Returns* :
18
- # - the calculated steering force
18
+ # - the steering force
19
19
  #
20
- def self.steer(kinematic, goal_position)
21
- desired_velocity = (goal_position - kinematic.position_vec).normalize * kinematic.max_speed
22
- desired_velocity - kinematic.velocity_vec
20
+ def self.steer(character_kinematic, goal_position)
21
+ desired_velocity = (goal_position - character_kinematic.position_vec).normalize * character_kinematic.max_speed
22
+ desired_velocity - character_kinematic.velocity_vec
23
23
  end
24
24
  end
@@ -0,0 +1,60 @@
1
+ ##
2
+ # Copyright 2013, Prylis Incorporated.
3
+ #
4
+ # This file is part of The Ruby Steering Behaviors Library.
5
+ # http://github.com/cpowell/steering-behaviors
6
+ # You can redistribute and/or modify this software only in accordance with
7
+ # the terms found in the "LICENSE" file included with the framework.
8
+
9
+ class SteeringBehaviors::Separation
10
+ extend SteeringBehaviors::Common
11
+
12
+ # Steer to avoid another kinematic by a certain radius.
13
+ #
14
+ # * *Args* :
15
+ # - +character_kinematic+ -> kinematic of "our" character that is moving and separating
16
+ # - +other_kinematic+ -> kinematic of the thing to separate from
17
+ # - +danger_radius+ -> attempt to avoid closest approach of less than this value
18
+ # * *Returns* :
19
+ # - a steering force
20
+ #
21
+ def self.steer(character_kinematic, other_kinematic, danger_radius)
22
+ cpa_time, char_pos_at_cpa, other_pos_at_cpa = compute_nearest_approach(character_kinematic, other_kinematic)
23
+
24
+ # Do nothing if the CPA is in the past, or if we won't breach the danger radius
25
+ return SteeringBehaviors::VEC_ZERO if cpa_time < 0
26
+ cpa_dist = (char_pos_at_cpa - other_pos_at_cpa).length
27
+ return SteeringBehaviors::VEC_ZERO if cpa_dist > danger_radius
28
+
29
+ parallelness = character_kinematic.heading_vec.dot(other_kinematic.heading_vec)
30
+
31
+ side_vec = character_kinematic.heading_vec.perpendicular
32
+ val = 0
33
+
34
+ if parallelness < -0.707
35
+ # anti-parallel, head-on paths
36
+ # steer away from the threat's future posn
37
+ offset_vec = other_pos_at_cpa - character_kinematic.position_vec
38
+ side_dot = offset_vec.dot(side_vec)
39
+ val = (side_dot > 0 ? -1.0 : 1.0)
40
+ # puts "Head-on, steering away from future pos"
41
+ elsif parallelness > 0.707
42
+ # parallel paths; steer away from threat
43
+ offset_vec = other_kinematic.position_vec - character_kinematic.position_vec
44
+ side_dot = offset_vec.dot(side_vec)
45
+ val = (side_dot > 0 ? -1.0 : 1.0)
46
+ # puts "Parallel, steering away from other guy"
47
+ else
48
+ # perpendicular paths; steer behind threat
49
+ if other_kinematic.speed <= character_kinematic.speed
50
+ side_dot = side_vec.dot(other_kinematic.velocity_vec)
51
+ val = (side_dot > 0 ? -1.0 : 1.0)
52
+ # puts "Perpendicular, steering behind other guy"
53
+ end
54
+ end
55
+
56
+ return side_vec * val
57
+
58
+ end
59
+
60
+ end
@@ -13,41 +13,43 @@ class SteeringBehaviors::Steering
13
13
  # directly alters the provided Mobile component.
14
14
  #
15
15
  # * *Args* :
16
- # - +kinematic+ -> the kinematic thing
16
+ # - +character_kinematic+ -> "our" character that is moving
17
17
  # - +steering_force+ -> force vector supplied by a steering behavior
18
18
  # - +delta+ -> time delta (in seconds) used for scaling the result
19
19
  #
20
- def self.feel_the_force(kinematic, steering_force, delta)
21
- acceleration = steering_force / kinematic.mass
20
+ def self.feel_the_force(character_kinematic, steering_force, delta, accelerative=true)
21
+ return if steering_force.nil?
22
22
 
23
- # Compute the new, proposed velocity vector.
24
- desired_velocity = kinematic.velocity_vec + (acceleration * delta)
23
+ acceleration = steering_force / character_kinematic.mass
25
24
 
26
- desired_velocity.truncate!(kinematic.max_speed)
25
+ # Compute the new, proposed velocity vector.
26
+ desired_velocity = character_kinematic.velocity_vec + (acceleration * delta)
27
+ desired_velocity.truncate!(character_kinematic.speed) if !accelerative
28
+ desired_velocity.truncate!(character_kinematic.max_speed)
27
29
 
28
30
  # If this timeslice's proposed velocity-vector exceeds the turn rate,
29
31
  # come up with a revised velociy-vec that doesn't exceed the rate -- and use that.
30
- angle = Math.acos kinematic.heading_vec.dot(desired_velocity.normalize)
31
- max_course_change = kinematic.max_turn * delta
32
+ angle = Math.acos character_kinematic.heading_vec.clamped_dot(desired_velocity.normalize)
33
+ max_course_change = character_kinematic.max_turn * delta
32
34
 
33
35
  if angle.abs > max_course_change
34
- direction = SteeringBehaviors::Vector.sign(kinematic.velocity_vec, desired_velocity) # -1==CCW, 1==CW
35
- limited_crse = kinematic.heading_vec.radians - max_course_change * direction
36
+ direction = SteeringBehaviors::Vector.sign(character_kinematic.velocity_vec, desired_velocity) # -1==CCW, 1==CW
37
+ limited_crse = character_kinematic.heading_vec.radians - max_course_change * direction
36
38
 
37
39
  # printf "Current %0.4f. Angle %0.4f %s exceeds max change %0.4f. Desired course [%0.4f], limited course [%0.4f]\n",
38
- # kinematic.heading_vec.radians,
40
+ # character_kinematic.heading_vec.radians,
39
41
  # angle,
40
42
  # (direction==1 ? 'CW' : 'CCW'),
41
43
  # max_course_change,
42
44
  # desired_velocity.radians,
43
45
  # limited_crse
44
46
 
45
- kinematic.velocity_vec = SteeringBehaviors::Vector.new(
46
- Math.sin(limited_crse) * kinematic.speed,
47
- Math.cos(limited_crse) * kinematic.speed
47
+ character_kinematic.velocity_vec = SteeringBehaviors::Vector.new(
48
+ Math.sin(limited_crse) * character_kinematic.speed,
49
+ Math.cos(limited_crse) * character_kinematic.speed
48
50
  )
49
51
  else
50
- kinematic.velocity_vec = desired_velocity
52
+ character_kinematic.velocity_vec = desired_velocity
51
53
  end
52
54
  end
53
55
 
@@ -90,9 +90,19 @@ class SteeringBehaviors::Vector
90
90
  self
91
91
  end
92
92
 
93
+ # A pure and correct dot product routine.
93
94
  # A · B = A.x * B.x + A.y * B.y
94
95
  def dot(b)
95
96
  val = @x*b.x + @y*b.y
97
+ end
98
+
99
+ # Unlike the 'pure' dot product above, this one ensures a -1.0..1.0 return value.
100
+ # If you're doing dot products of normalized vectors and expecting a pure -1.0..1.0
101
+ # return value for Math.acos() purposes, use this routine to avoid "Numerical argument
102
+ # is out of domain" errors that creep in due to rounding errors.
103
+ def clamped_dot(b)
104
+ val = self.dot(b)
105
+
96
106
  val = 1.0 if val > 1.0
97
107
  val = -1.0 if val < -1.0
98
108
 
@@ -104,13 +114,9 @@ class SteeringBehaviors::Vector
104
114
  end
105
115
 
106
116
  def compass_bearing(y_down_more_positive=false)
107
- if y_down_more_positive
108
- up = SteeringBehaviors::Vector.new(0, -1)
109
- else
110
- up = SteeringBehaviors::Vector.new(0, 1)
111
- end
117
+ up = ( y_down_more_positive ? SteeringBehaviors::VEC_UP_NEGY : SteeringBehaviors::VEC_UP_POSY)
112
118
 
113
- theta = Math.acos(self.normalize.dot(up))
119
+ theta = Math.acos(self.normalize.clamped_dot(up))
114
120
 
115
121
  theta *= -1 if @x < 0
116
122
  degs = SteeringBehaviors::Vector.rad2deg(theta)
@@ -197,5 +203,4 @@ class SteeringBehaviors::Vector
197
203
  def to_s
198
204
  format("Vector {[%.7f, %.7f] len %0.7f}", @x, @y, length)
199
205
  end
200
-
201
206
  end
@@ -12,15 +12,15 @@ class SteeringBehaviors::Wander
12
12
  # See http://www.red3d.com/cwr/steer/
13
13
  #
14
14
  # * *Args* :
15
- # - +kinematic+ -> the wandering thing
15
+ # - +character_kinematic+ -> kinematic of "our" character that is moving and wandering
16
16
  # - +erraticism+ -> how erratic the wandering effect will be
17
17
  # * *Returns* :
18
- # - the calculated steering force
18
+ # - the steering force
19
19
  #
20
- def self.steer(kinematic, erraticism)
21
- kinematic.steering_target += SteeringBehaviors::Vector.new(rand(-1.0..1.0)*erraticism, rand(-1.0..1.0)*erraticism)
22
- kinematic.steering_target.normalize!
20
+ def self.steer(character_kinematic, erraticism)
21
+ character_kinematic.steering_target += SteeringBehaviors::Vector.new(rand(-1.0..1.0)*erraticism, rand(-1.0..1.0)*erraticism)
22
+ character_kinematic.steering_target.normalize!
23
23
 
24
- kinematic.steering_target.rotate(kinematic.heading_vec.radians)
24
+ character_kinematic.steering_target.rotate(character_kinematic.heading_vec.radians)
25
25
  end
26
26
  end
data/test/vector_test.rb CHANGED
@@ -196,6 +196,14 @@ class VectorTest < MiniTest::Unit::TestCase
196
196
  @v2 = SteeringBehaviors::Vector.new(1.0, 0.5)
197
197
  assert_equal(1.0, @v1.dot(@v2))
198
198
 
199
+ @v1 = SteeringBehaviors::Vector.new(1.0, 1.0)
200
+ @v2 = SteeringBehaviors::Vector.new(1.0, 1.0)
201
+ assert_equal(2.0, @v1.dot(@v2))
202
+
203
+ @v1 = SteeringBehaviors::Vector.new(0.708, 0.707)
204
+ @v2 = SteeringBehaviors::Vector.new(0.707, 0.707)
205
+ assert_equal(1.000405, @v1.dot(@v2))
206
+
199
207
  @v1 = SteeringBehaviors::Vector.new(1.0, 0)
200
208
  @v2 = SteeringBehaviors::Vector.new(0, 1.0)
201
209
  assert_equal(0, @v1.dot(@v2))
@@ -224,8 +232,52 @@ class VectorTest < MiniTest::Unit::TestCase
224
232
  @v2 = SteeringBehaviors::Vector.new(0.866, 0.5)
225
233
  @v2.normalize!
226
234
  assert_equal(0.86603810567665, @v1.dot(@v2))
235
+
236
+ @v1 = SteeringBehaviors::Vector.new(500, 500)
237
+ @v2 = SteeringBehaviors::Vector.new(-14, 75)
238
+ assert_equal(30500, @v1.dot(@v2))
239
+
240
+ @v1 = SteeringBehaviors::Vector.new(-0.5, -0.5)
241
+ @v2 = SteeringBehaviors::Vector.new(2.2, 2.2)
242
+ assert_equal(-2.2, @v1.dot(@v2))
243
+ end
244
+
245
+ def test_clamped_dot_product
246
+ @v1 = SteeringBehaviors::Vector.new(0.5, 1)
247
+ @v2 = SteeringBehaviors::Vector.new(1.0, 0.5)
248
+ assert_equal(1.0, @v1.clamped_dot(@v2))
249
+
250
+ @v1 = SteeringBehaviors::Vector.new(0.708, 0.707)
251
+ @v2 = SteeringBehaviors::Vector.new(0.707, 0.707)
252
+ assert_equal(1.0, @v1.clamped_dot(@v2))
253
+
254
+ @v1 = SteeringBehaviors::Vector.new(0, 1.0)
255
+ @v2 = SteeringBehaviors::Vector.new(-1.0, 0)
256
+ assert_equal(0, @v1.clamped_dot(@v2))
257
+
258
+ @v1 = SteeringBehaviors::Vector.new(0, 1.0)
259
+ @v2 = SteeringBehaviors::Vector.new(0.707, 0.707)
260
+ @v2.normalize!
261
+ assert_equal(0.7071067811865476, @v1.clamped_dot(@v2))
262
+
263
+ @v1 = SteeringBehaviors::Vector.new(1.0, 0)
264
+ @v2 = SteeringBehaviors::Vector.new(-0.707, 0.707)
265
+ @v2.normalize!
266
+ assert_equal(-0.7071067811865476, @v1.clamped_dot(@v2))
267
+
268
+ @v1 = SteeringBehaviors::Vector.new(1.0, 0)
269
+ @v2 = SteeringBehaviors::Vector.new(-0.707, -0.707)
270
+ @v2.normalize!
271
+ assert_equal(-0.7071067811865476, @v1.clamped_dot(@v2))
272
+
273
+ @v1 = SteeringBehaviors::Vector.new(0.5, 0.866)
274
+ @v1.normalize!
275
+ @v2 = SteeringBehaviors::Vector.new(0.866, 0.5)
276
+ @v2.normalize!
277
+ assert_equal(0.86603810567665, @v1.clamped_dot(@v2))
227
278
  end
228
279
 
280
+
229
281
  def test_perpendicular
230
282
  @v2 = @v.perpendicular
231
283
  assert_equal(1.0, @v2.x)
metadata CHANGED
@@ -2,14 +2,14 @@
2
2
  name: steering_behaviors
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 1.0.4
5
+ version: 1.0.5
6
6
  platform: ruby
7
7
  authors:
8
8
  - Chris Powell
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-08-05 00:00:00.000000000 Z
12
+ date: 2013-08-11 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: |
15
15
  If you're building a game, you need your game agents and characters to move on their own. A standard way of doing this is with 'steering behaviors'. The seminal paper by Craig Reynolds established a core set of steering behaviors that could be utilized for a variety of common movement tasks and natural behaviors. This Ruby library can accomplish many/most of those tasks for your Ruby / JRuby game. The basic behaviors can be layered for more complicated and advanced behaviors, such as flocking and crowd movement.
@@ -29,6 +29,7 @@ files:
29
29
  - lib/steering_behaviors/orthogonal.rb
30
30
  - lib/steering_behaviors/pursue.rb
31
31
  - lib/steering_behaviors/seek.rb
32
+ - lib/steering_behaviors/separation.rb
32
33
  - lib/steering_behaviors/steering.rb
33
34
  - lib/steering_behaviors/vector.rb
34
35
  - lib/steering_behaviors/wander.rb