Filled all scripts with comments.

This commit is contained in:
Francois Belair 2019-07-01 14:07:27 -04:00
parent 7f12ca3f0a
commit 92ebb8ca04
14 changed files with 193 additions and 21 deletions

View file

@ -1,4 +1,10 @@
extends Node
"""
Sample AI controller. It gets and sets the player in its `_ready` function as a target to various
behaviours, and picks whichever one it should be calculating on. It also updates the controller
with information for the behaviours to use. Then, based on what is calculated, it applies
the calculated velocity to itself.
"""
var _target: Node2D
var _motion: = SteeringMotion2D.new()
@ -54,6 +60,6 @@ func _process(delta):
get_parent().rotate(_rotation_velocity)
else:
_rotation_velocity = 0
_controller.set_rotation_velocity(0)
_controller.current_rotation_velocity = 0
_last_target_position = target_position

View file

@ -1,4 +1,7 @@
extends Node2D
"""
Simple script that lets the user move around with the arrow keys.
"""
var speed: float = 125

View file

@ -37,7 +37,6 @@ deceleration_radius = 150.0
script = ExtResource( 5 )
deceleration_radius = 45.0
alignment_tolerance = 2.0
time_to_target = 1.0
[node name="Sprite" type="Sprite" parent="AI"]
texture = ExtResource( 6 )

View file

@ -1,5 +1,13 @@
extends Node
class_name BehaviourController2D
"""
Meant to be the parent to behaviours. It is a helper to provide information to its children behaviours.
The controller is the hub through which behaviours access various information about themselves, and
their targets. It holds the maximum speeds and acceleration amounts, and has space to hold current
velocities that can be used in more predictive behaviours. However, the controller is naive and
needs to be fed this information; where the information comes from should be determined by the programmer.
"""
export(NodePath) var steerable_path: NodePath
export(float) var max_linear_speed: float
@ -15,23 +23,25 @@ var target_current_rotation_velocity: float = 0
onready var steerable: Node2D = get_node(steerable_path)
"""
Returns true if a Node2D was successfully put into the controller.
"""
func valid() -> bool:
return steerable != null
"""
Sets the current linear velocity by way of X and Y, rather than copying or creating new vectors.
"""
func set_linear_velocity(x: float, y: float) -> void:
current_linear_velocity.x = x
current_linear_velocity.y = y
func set_rotation_velocity(rotational_velocity: float) -> void:
current_rotation_velocity = rotational_velocity
"""
Sets the target's current linear velocity by way of X and Y, rather than copying or creating new vectors.
Not all behaviours have a target that moves, so match requirements accordingly.
"""
func set_target_linear_velocity(x: float, y: float) -> void:
target_current_linear_velocity.x = x
target_current_linear_velocity.y = y
func set_target_rotation_velocity(rotational_velocity: float) -> void:
target_current_rotation_velocity = rotational_velocity
target_current_linear_velocity.y = y

View file

@ -1,10 +1,30 @@
extends SteeringBehaviour2D
class_name BlendedBehaviour2D
"""
A behaviour that runs through all of its children and blends all motions together by a defined weight.
The blended behaviour will first calculate a child's motion, will multiply iy by its weight, then add it
to the motion buffer before returning it. It will be clamped to the controller's maximums.
The weights are not relative to 1, and instead act as a multiplier on the strength of the behaviour.
Notes
-----
More complex behaviours can be built with a blended behaviour than an indivudal one without writing
a complex, custom behaviour, but it is more costly to run since all children will be evaluated. Figuring
out the amount of weight to give each behaviour can also be a non-trivial, manual problem.
It can also lead to conflicts of force which may cause unusual behaviour.
The general workflow should be to have a priority steering with a set of blended behaviours, based on need.
"""
export(float) var weight: float = 1
var internal_motion: = SteeringMotion2D.new()
var _internal_motion: = SteeringMotion2D.new()
"""
Returns the steering motion with a blend of all of the behaviour's children, capped by the controller's maximums.
"""
func _calculate_steering_internal(motion: SteeringMotion2D) -> SteeringMotion2D:
motion.zero()
@ -13,9 +33,9 @@ func _calculate_steering_internal(motion: SteeringMotion2D) -> SteeringMotion2D:
continue
var steering: = child as SteeringBehaviour2D
steering.calculate_steering(internal_motion)
motion.linear_motion += (internal_motion.linear_motion * weight)
motion.rotational_motion += (internal_motion.rotational_motion * weight)
steering.calculate_steering(_internal_motion)
motion.linear_motion += (_internal_motion.linear_motion * weight)
motion.rotational_motion += (_internal_motion.rotational_motion * weight)
motion.linear_motion.clamped(controller.max_linear_acceleration)
motion.rotational_motion = min(controller.max_rotational_acceleration, motion.rotational_motion)

View file

@ -1,5 +1,16 @@
extends SteeringBehaviour2D
class_name AlignBehaviour2D
"""
A behaviour that tries to rotate itself to match the target's own rotation.
The `alignment_tolerance` is there to prevent flicker, so as to not constantly overshoot
and start rotating back the other way. The `deceleration_radius` is the interval of degrees
to begin slowing down the act of rotating, and the `time_to_target` is the amount of time
in a fixed time scale to weight the amount of rotation by. It should be a small value, and
defaults to 0.1.
The only target supported is Node2D and should be fed in ahead of time.
"""
export(float) var deceleration_radius: float
export(float) var alignment_tolerance: float
@ -7,12 +18,20 @@ export(float) var time_to_target: float = 0.1
var target: Node2D
"""
Returns a steering motion that has no linear acceleration but a rotational acceleration that will
match its own to its target's orientation.
"""
func _calculate_steering_internal(motion: SteeringMotion2D) -> SteeringMotion2D:
if target == null:
return motion.zero()
return _align(motion, target.rotation)
"""
The internal function that calculates the alignment based on the radians passed into the function.
Returns the steering motion with the rotational acceleration that will match its own to the rotation value.
"""
func _align(motion: SteeringMotion2D, desired_rotation: float) -> SteeringMotion2D:
var rotation_size: = abs(desired_rotation - steerable().rotation)
var alignment_tolerance_rad: = deg2rad(alignment_tolerance)

View file

@ -1,5 +1,16 @@
extends SteeringBehaviour2D
class_name ArriveBehaviour2D
"""
A behaviour that aims to arrive at where the target is, smoothly and within an acceptable distance threshold.
`arrival_tolerance` is the distance away from the target that will be good enough to stop moving.
`deceleration_radius` is the distance away from the target that the AI should begin weighing down its acceleration
and slow down.
`time_to_target` is a fixed time scale value of time by which it should weigh its velocity by to arrive smoothly.
This value should be small and defaults to 0.1.
Supported targets are Vector2 and Node2D.
"""
export(float) var arrival_tolerance: float
export(float) var deceleration_radius: float
@ -7,6 +18,10 @@ export(float) var time_to_target: float = 0.1
var target
"""
Returns the steering motion filled with a linear acceleration amount that will take it towards the target with a
smooth deceleration, or zero if it is already inside of an arrival threshold.
"""
func _calculate_steering_internal(motion: SteeringMotion2D) -> SteeringMotion2D:
if target == null:
return motion.zero()
@ -30,10 +45,9 @@ func _calculate_steering_internal(motion: SteeringMotion2D) -> SteeringMotion2D:
return motion
func _arrive_vector(motion: SteeringMotion2D) -> SteeringMotion2D:
return motion.zero()
"""
Returns the target's position.
"""
func _to_target() -> Vector2:
if target is Vector2:
return target - steerable().position

View file

@ -1,6 +1,16 @@
extends InterceptBehaviour2D
class_name EvadeBehaviour2D
"""
A behaviour that acts as the inverse of Intercept. It will move away from the predicted point where
the target will be. See `InterceptBehaviour2D` for more information.
Supported targets are Vector2 and Node2D.
"""
"""
Returns a steering motion filled with a linear acceleration amount that will move it away from the
point where the target will be in `max_prediction_time`.
"""
func _calculate_steering_internal(motion: SteeringMotion2D) -> SteeringMotion2D:
._calculate_steering_internal(motion)
motion.linear_motion *= -1

View file

@ -1,5 +1,10 @@
extends SeekBehaviour2D
class_name FleeBehaviour2D
"""
A behaviour that acts like an inverse of Seek - it will constantly accelerate away from the target.
Supported targets are Node2D and Vector2.
"""
func _calculate_steering_internal(motion: SteeringMotion2D) -> SteeringMotion2D:
._calculate_steering_internal(motion)

View file

@ -1,10 +1,24 @@
extends SteeringBehaviour2D
class_name InterceptBehaviour2D
"""
A behaviour that will have the target attempt to predict where the target is headed, and point
its acceleration towards that point.
`max_prediction_time` is how far into the future in fixed time scale the AI will use to predict
the heading of the target.
Supported targets are Node2D and Vector2.
"""
export(float) var max_prediction_time: float
var target
"""
Returns the steering motion filled with a linear acceleration amount that will send it on an
intercept course with wherever the target is headed so that its arrival will cross with the
target's.
"""
func _calculate_steering_internal(motion: SteeringMotion2D) -> SteeringMotion2D:
if target == null:
return motion.zero()
@ -27,6 +41,9 @@ func _calculate_steering_internal(motion: SteeringMotion2D) -> SteeringMotion2D:
return motion
"""
Returns the target's position.
"""
func _target() -> Vector2:
if target is Vector2:
return target

View file

@ -1,8 +1,17 @@
extends SteeringBehaviour2D
class_name SeekBehaviour2D
"""
A behaviour that will constantly accelerate towards the target as fast as it can accelerate.
Supported targets are Node2D and Vector2.
"""
var target
"""
Returns the steering motion filled with a linear acceleration amount that will make it go to where
the target currently is, as fast as it is allowed by the controller.
"""
func _calculate_steering_internal(motion: SteeringMotion2D) -> SteeringMotion2D:
if target == null:
return motion.zero()
@ -12,6 +21,9 @@ func _calculate_steering_internal(motion: SteeringMotion2D) -> SteeringMotion2D:
return motion
"""
Returns the target's position.
"""
func _target() -> Vector2:
if target is Vector2:
return target

View file

@ -1,20 +1,40 @@
extends SteeringBehaviour2D
class_name PrioritySteering2D
"""
An AI behaviour that runs through its children and stops as soon as one returns a non-zero motion.
Runs iteratively from top to bottom down its child-list and stops once one of them returns non-zero motion.
Certain behaviours will always return an acceleration amount, while others may not - in many cases, you
usually want to act on it (for instance, evading an obstacle to avoid impact.)
"""
var last_selected_child_idx: int
"""
Returns the child whose motion was non-zero. Returns null if no children produced an acceleration.
"""
func get_last_selected_child() -> SteeringBehaviour2D:
if last_selected_child_idx >= 0:
return get_child(last_selected_child_idx) as SteeringBehaviour2D
return null
"""
Fills the steering motion object with the first non-zero calculation it can find. The priority goes from
top to bottom.
"""
func _calculate_steering_internal(motion: SteeringMotion2D) -> SteeringMotion2D:
last_selected_child_idx = -1
var size: = get_child_count()
for i in range(0, size):
last_selected_child_idx = i
var child: = get_child(i) as SteeringBehaviour2D
if child == null:
continue
child.calculate_steering(motion)
if motion.linear_motion.length_squared() != 0 or motion.rotational_motion != 0:
if !motion.is_zero():
last_selected_child_idx = i
break
if size > 0:

View file

@ -1,5 +1,13 @@
extends Node
class_name SteeringBehaviour2D
"""
The base class for a behaviour in 2D space. Subclasses of this class should only
override `_calculate_steering_internal`.
Calling `calculate_steering` on a class that extends this should fill the provided `SteeringMotion2D`
with an amount of linear and rotational acceleration. It is up to the caller to then use that
information to actually move the AI actor.
"""
var enabled: = true
var controller: BehaviourController2D
@ -8,6 +16,10 @@ func _ready() -> void:
_get_controller(self)
"""
The public face of the steering calculation.
Returns the motion with the linear and/or rotational acceleration filled in, based on the behaviour.
"""
func calculate_steering(motion: SteeringMotion2D) -> SteeringMotion2D:
if !enabled or controller == null or !controller.valid():
return motion.zero()
@ -15,16 +27,26 @@ func calculate_steering(motion: SteeringMotion2D) -> SteeringMotion2D:
return _calculate_steering_internal(motion)
"""
Returns the actor upon which the behaviours are acting, to access their position in space.
"""
func steerable() -> Node2D:
if controller == null:
return null
return controller.steerable
"""
The privade, overriderable face of the steering calculation.
Returns the motion with the linear and/or rotational acceleration filled in, based on the behaviour.
"""
func _calculate_steering_internal(motion: SteeringMotion2D) -> SteeringMotion2D:
return motion
"""
Recursively searches its parent to find the behaviour controller that will feed it information.
"""
func _get_controller(current_node: Node) -> void:
if current_node is BehaviourController2D:
controller = current_node

View file

@ -1,22 +1,37 @@
extends Reference
class_name SteeringMotion2D
"""
A container that holds a linear and rotational amount of acceleration generated by steering behaviours.
"""
var linear_motion: = Vector2()
var rotational_motion: float = 0
"""
Sets the linear and rotational acceleration amounts to zero
"""
func zero() -> void:
linear_motion.x = 0
linear_motion.y = 0
rotational_motion = 0
"""
Returns true if both the linear and rotational acceleration amounts are zero
"""
func is_zero() -> bool:
return linear_is_zero() and rotational_is_zero()
"""
Returns true if the linear acceleration amount is zero
"""
func linear_is_zero() -> bool:
return linear_motion.x == 0 and linear_motion.y == 0
"""
Returns true if the rotational acceleration amount is zero
"""
func rotational_is_zero() -> bool:
return rotational_motion == 0