diff --git a/prototypes/5.ai-steer/LAI.gd b/prototypes/5.ai-steer/LAI.gd index 8112e08..65d70b2 100644 --- a/prototypes/5.ai-steer/LAI.gd +++ b/prototypes/5.ai-steer/LAI.gd @@ -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 \ No newline at end of file diff --git a/prototypes/5.ai-steer/Player.gd b/prototypes/5.ai-steer/Player.gd index f084849..80d1aa3 100644 --- a/prototypes/5.ai-steer/Player.gd +++ b/prototypes/5.ai-steer/Player.gd @@ -1,4 +1,7 @@ extends Node2D +""" +Simple script that lets the user move around with the arrow keys. +""" var speed: float = 125 diff --git a/prototypes/5.ai-steer/root.tscn b/prototypes/5.ai-steer/root.tscn index 78d86d2..28b6927 100644 --- a/prototypes/5.ai-steer/root.tscn +++ b/prototypes/5.ai-steer/root.tscn @@ -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 ) diff --git a/prototypes/5.ai-steer/src/Behaviours/2D/BehaviourController2D.gd b/prototypes/5.ai-steer/src/Behaviours/2D/BehaviourController2D.gd index 57c54fc..2e7a94e 100644 --- a/prototypes/5.ai-steer/src/Behaviours/2D/BehaviourController2D.gd +++ b/prototypes/5.ai-steer/src/Behaviours/2D/BehaviourController2D.gd @@ -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 \ No newline at end of file + target_current_linear_velocity.y = y \ No newline at end of file diff --git a/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/BlendedBehaviour2D.gd b/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/BlendedBehaviour2D.gd index d61ea26..c463475 100644 --- a/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/BlendedBehaviour2D.gd +++ b/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/BlendedBehaviour2D.gd @@ -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) diff --git a/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/AlignBehaviour2D.gd b/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/AlignBehaviour2D.gd index e809bfe..0d473ee 100644 --- a/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/AlignBehaviour2D.gd +++ b/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/AlignBehaviour2D.gd @@ -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) diff --git a/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/ArriveBehaviour2D.gd b/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/ArriveBehaviour2D.gd index 1a8bc42..1059dd4 100644 --- a/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/ArriveBehaviour2D.gd +++ b/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/ArriveBehaviour2D.gd @@ -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 diff --git a/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/EvadeBehaviour2D.gd b/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/EvadeBehaviour2D.gd index b3a6162..7f1a6d0 100644 --- a/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/EvadeBehaviour2D.gd +++ b/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/EvadeBehaviour2D.gd @@ -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 diff --git a/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/FleeBehaviour2D.gd b/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/FleeBehaviour2D.gd index 7a8032d..1988279 100644 --- a/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/FleeBehaviour2D.gd +++ b/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/FleeBehaviour2D.gd @@ -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) diff --git a/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/InterceptBehaviour2D.gd b/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/InterceptBehaviour2D.gd index 6cc34e7..f06704d 100644 --- a/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/InterceptBehaviour2D.gd +++ b/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/InterceptBehaviour2D.gd @@ -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 diff --git a/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/SeekBehaviour2D.gd b/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/SeekBehaviour2D.gd index bd6ec71..0c8a82c 100644 --- a/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/SeekBehaviour2D.gd +++ b/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/Individual/SeekBehaviour2D.gd @@ -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 diff --git a/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/PrioritySteering2D.gd b/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/PrioritySteering2D.gd index 0e96885..47c31ec 100644 --- a/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/PrioritySteering2D.gd +++ b/prototypes/5.ai-steer/src/Behaviours/2D/Behaviours/PrioritySteering2D.gd @@ -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: diff --git a/prototypes/5.ai-steer/src/Behaviours/2D/SteeringBehaviour2D.gd b/prototypes/5.ai-steer/src/Behaviours/2D/SteeringBehaviour2D.gd index 3e1ea51..b6f2e24 100644 --- a/prototypes/5.ai-steer/src/Behaviours/2D/SteeringBehaviour2D.gd +++ b/prototypes/5.ai-steer/src/Behaviours/2D/SteeringBehaviour2D.gd @@ -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 diff --git a/prototypes/5.ai-steer/src/Behaviours/2D/SteeringMotion2D.gd b/prototypes/5.ai-steer/src/Behaviours/2D/SteeringMotion2D.gd index 7937c16..76ef88a 100644 --- a/prototypes/5.ai-steer/src/Behaviours/2D/SteeringMotion2D.gd +++ b/prototypes/5.ai-steer/src/Behaviours/2D/SteeringMotion2D.gd @@ -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 \ No newline at end of file