Add 3D demo and fix 3D agents

Closes #30, closes #31, closes #33
This commit is contained in:
Francois Belair 2020-02-13 03:33:03 -05:00
parent 534ade7282
commit ca1839c633
20 changed files with 371 additions and 48 deletions

View File

@ -2,6 +2,24 @@
This document lists new features, improvements, changes, and bug fixes in every release of the add-on.
## Godot Steering AI Framework 2.1.0 ##
### Features ###
- There is now a `Arrive3d` demo for a 3D usecase
### Changes ###
- `GSAIUtils.vector3_to_angle` now uses the vector's X and Z components to determine angle. Use `GSAIUtils.vector2_to_angle` for 2D use cases.
- `GSAIMatchOrientation` and its subclasses like `GSAIFace` and `GSAILookWhereYouGo` now include a `use_z` property. It should be `true` when using 3D so that facing will be done with the X and Z components.
- The README now mentions a simple way to install the framework.
- Exposed `agent_count` inside the `AvoidCollisionsDemo`.
### Bug fixes ###
- Fixed `GSAIKinematicBody3DAgent` and `GSAIRigidBody3DAgent` trying to use `global_position` instead of `transform.origin`.
- The `SeekFleeDemo`'s boundaries will now match the size of the screen.
## Godot Steering AI Framework 2.0.0 ##
This release brings one new feature and bug fix, and breaking changes to the framework as we renamed all the classes.

View File

@ -8,10 +8,11 @@ It supports all essential steering behaviors like flee, follow, look at, but als
<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
**Table of Contents**
- [Introduction](#introduction)
- [The framework](#the-framework)
- [Installation](#installation)
- [Getting Started](#getting-started)
- [More information and resources](#more-information-and-resources)
- [Example usage](#example-usage)
<!-- markdown-toc end -->
@ -28,23 +29,27 @@ Joining these systems together can give complex and graceful movement while also
This project is a framework for the [Godot game engine](https://godotengine.org/). It takes a lot of inspiration from the excellent [GDX-AI](https://github.com/libgdx/gdx-ai) framework for the [LibGDX](https://libgdx.badlogicgames.com/) java-based framework.
Every class in the toolkit is based on Godot's [Reference](https://docs.godotengine.org/en/latest/classes/class_reference.html) type. There is no need to have a complex scene tree; everything that has to do with the AI's movement can be contained inside movement-oriented classes.
Every class in the framework is based on Godot's [Reference](https://docs.godotengine.org/en/latest/classes/class_reference.html) type. There is no need to have a complex scene tree; everything that has to do with the AI's movement can be contained inside movement-oriented classes.
As a short overview, a character is represented by a steering agent; it stores its position, orientation, maximum speeds and current velocity. A steering behavior is associated with a steering agent and calculates a linear and/or angular change in velocity based on its information. The coder then applies that acceleration in whatever ways is appropriate to the character to change its velocity, like RigidBody's apply_impulse, or a KinematicBody's move_and_slide.
## Installation ##
The cleanest way to install the framework onto your project is to take the `/src/` folder and copy it into your project. I recommend something like `res://systems/godot-steering-framework/`.
## Getting Started ##
The framework's documentation and code reference are both available on the [GDQuest](https://www.gdquest.com/docs/godot-steering-toolkit/getting-started/) website's documents.
The framework's documentation and code reference are both available on the [GDQuest](https://www.gdquest.com/docs/godot-steering-ai-framework/getting-started) website's documents.
### More information and resources ###
- [Understanding Steering Behaviors](https://gamedevelopment.tutsplus.com/series/understanding-steering-behaviors--gamedev-12732): Breakdowns of various behaviors by Fernando Bevilacqua with graphics and in-depth explanations.
- [GDX-AI Wiki](https://github.com/libgdx/gdx-ai/wiki/Steering-Behaviors): Descriptions of how LibGDX's AI submodule uses steering behaviors with a few graphics. Since this toolkit uses it for inspiration, there will be some similarities.
- [RedBlobGames](https://www.redblobgames.com/) - An excellent resources for complex pathfinding like A\*, graph theory, and other algorithms that are game-development related. Steering behaviors are not covered, but for anyone looking to study and bulk up on their algorithms, this is a great place.
- [GDX-AI Wiki](https://github.com/libgdx/gdx-ai/wiki/Steering-Behaviors): Descriptions of how LibGDX's AI submodule uses steering behaviors with a few graphics. Since this framework uses it for inspiration, there will be some similarities.
- [RedBlobGames](https://www.redblobgames.com/): An excellent resources for complex pathfinding like A\*, graph theory, and other algorithms that are game-development related. Steering behaviors are not covered, but for anyone looking to study and bulk up on their algorithms, this is a great place.
## Example usage ##
The fastest way to get started is to look at a sample class that makes use of the toolkit.
The fastest way to get started is to look at a sample class that makes use of the framework.
The goal of this class is to show how an agent can chase a player and predict where the player *will* be while also maintaining a distance from them. When the agents health is low, it will flee from the player directly. The agent will keep facing the player while its chasing them, but will look where it's going while its fleeing.
@ -76,32 +81,42 @@ var angular_drag := 0.1
# Holds the linear and angular components calculated by our steering behaviors.
var acceleration := GSAITargetAcceleration.new()
onready var current_health := health_max
# GSAISteeringAgent holds our agent's position, orientation, maximum speed and acceleration.
onready var agent := GSAISteeringAgent.new()
onready var player: Node = get_tree().get_nodes_in_group("Player")[0]
# This assumes that our player class will keep its own agent updated.
onready var player_agent: GSAISteeringAgent = player.agent
# Proximities represent an area with which an agent can identify where neighbors in its relevant
# group are. In our case, the group will feature the player, which will be used to avoid a
# collision with them. We use a radius proximity so the player is only relevant inside 100 pixels
onready var proximity := GSAIRadiusProximity.new(agent, [player_agent], 100).
# GSAIBlend combines behaviors together, calculating all of their acceleration together and adding
# them together, multiplied by a strength. We will have one for fleeing, and one for pursuing,
# toggling them depending on the agent's health. Since we want the agent to rotate AND move, then
# we aim to blend them together.
onready var flee_blend := GSAIBlend.new(agent)
onready var pursue_blend := GSAIBlend.new(agent)
# GSAIPriority will be the main steering behavior we use. It holds sub-behaviors and will pick the
# first one that returns non-zero acceleration, ignoring any afterwards.
onready var priority := GSAIPriority.new(agent)
func _ready() -> void:
# ---------- Configuration for our agent ----------
agent.linear_speed_max = speed_max
@ -115,21 +130,25 @@ func _ready() -> void:
# Pursue will happen while the player is in good health. It produces acceleration that takes
# the agent on an intercept course with the target, predicting its position in the future.
var pursue := GSAIPursue.new(agent, player_agent)
pursue.predict_time_max = 1.5
# Flee will happen while the agent is in bad health, so will start disabled. It produces
# acceleration that takes the agent directly away from the target with no prediction.
var flee := GSAIFlee.new(agent, player_agent)
# AvoidCollision tries to keep the agent from running into any of the neighbors found in its
# proximity group. In our case, this will be the player if they are close enough.
var avoid := GSAIAvoidCollisions.new(agent, proximity)
# Face turns the agent to keep looking towards its target. It will be enabled while the agent
# is not fleeing due to low health. It tries to arrive 'on alignment' with 0 remaining velocity.
var face := GSAIFace.new(agent, player_agent)
# We use deg2rad because the math in the toolkit assumes radians.
# We use deg2rad because the math in the framework assumes radians.
# How close for the agent to be 'aligned', if not exact.
face.alignment_tolerance = deg2rad(5)
# When to start slowing down.
@ -138,6 +157,7 @@ func _ready() -> void:
# LookWhereYouGo turns the agent to keep looking towards its direction of travel. It will only
# be enabled while the agent is at low health.
var look := GSAILookWhereYouGo.new(agent)
# How close for the agent to be 'aligned', if not exact.
look.alignment_tolerance = deg2rad(5)
# When to start slowing down.
@ -175,7 +195,7 @@ func _physics_process(delta: float) -> void:
# Calculate the desired acceleration.
priority.calculate_steering(acceleration)
# We add the discovered acceleration to our linear velocity. The toolkit does not limit
# We add the discovered acceleration to our linear velocity. The framework does not limit
# velocity, just acceleration, so we clamp the result ourselves here.
velocity = (velocity + Vector2(
acceleration.linear.x, acceleration.linear.y)
@ -199,7 +219,7 @@ func _physics_process(delta: float) -> void:
rotation += angular_velocity * delta
# In order to support both 2D and 3D, the toolkit uses Vector3, so the conversion is required
# In order to support both 2D and 3D, the framework uses Vector3, so the conversion is required
# when using 2D nodes. The Z component can be left to 0 safely.
func update_agent() -> void:
agent.position.x = global_position.x

View File

@ -0,0 +1,91 @@
[gd_scene load_steps=14 format=2]
[ext_resource path="res://demos/Utils/DemoInterface.tscn" type="PackedScene" id=1]
[ext_resource path="res://demos/Arrive3d/Camera.gd" type="Script" id=2]
[ext_resource path="res://demos/Arrive3d/Seek3dDemo.gd" type="Script" id=3]
[ext_resource path="res://demos/Arrive3d/Seeker.gd" type="Script" id=4]
[ext_resource path="res://demos/Arrive3d/SeekerMat.tres" type="Material" id=5]
[sub_resource type="CapsuleShape" id=1]
[sub_resource type="CapsuleMesh" id=2]
[sub_resource type="CubeMesh" id=3]
material = ExtResource( 5 )
size = Vector3( 0.5, 0.5, 1 )
[sub_resource type="CylinderMesh" id=4]
top_radius = 2.0
bottom_radius = 2.0
height = 0.1
[sub_resource type="SpatialMaterial" id=5]
albedo_color = Color( 0.945098, 0.85098, 0.0745098, 1 )
[sub_resource type="BoxShape" id=6]
extents = Vector3( 1000, 0.1, 1000 )
[sub_resource type="PlaneMesh" id=7]
size = Vector2( 250, 250 )
[sub_resource type="SpatialMaterial" id=8]
albedo_color = Color( 0.0941176, 0.235294, 0.486275, 1 )
[node name="Arrive3dDemo" type="Node"]
script = ExtResource( 3 )
[node name="Arriver" type="KinematicBody" parent="."]
script = ExtResource( 4 )
[node name="CollisionShape" type="CollisionShape" parent="Arriver"]
transform = Transform( 1, 0, 0, 0, -1.62921e-07, 1, 0, -1, -1.62921e-07, 0, 1.5, 0 )
shape = SubResource( 1 )
[node name="Capsule" type="MeshInstance" parent="Arriver"]
transform = Transform( 1, 0, 0, 0, -1.62921e-07, 1, 0, -1, -1.62921e-07, 0, 1.5, 0 )
mesh = SubResource( 2 )
material/0 = ExtResource( 5 )
[node name="Nose" type="MeshInstance" parent="Arriver"]
transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 1.25 )
mesh = SubResource( 3 )
material/0 = null
[node name="Camera" type="Camera" parent="."]
transform = Transform( 0.989952, 0.0720094, -0.121693, 0.0339305, 0.714503, 0.69881, 0.137271, -0.695917, 0.70488, -7.68317, 14.1265, 25.616 )
current = true
script = ExtResource( 2 )
[node name="RayCast" type="RayCast" parent="Camera"]
enabled = true
cast_to = Vector3( -627, 200, -777 )
collision_mask = 2
[node name="MouseTarget" type="Spatial" parent="."]
transform = Transform( 1, 0, 7.45058e-09, 0, 1, 0, 7.45058e-09, 0, 1, -4.76837e-07, 9.53674e-07, 1.90735e-06 )
[node name="MeshInstance" type="MeshInstance" parent="MouseTarget"]
mesh = SubResource( 4 )
material/0 = SubResource( 5 )
[node name="StaticBody" type="StaticBody" parent="."]
transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.1, 0 )
collision_layer = 2
collision_mask = 2
[node name="CollisionShape" type="CollisionShape" parent="StaticBody"]
shape = SubResource( 6 )
[node name="Ground" type="MeshInstance" parent="."]
mesh = SubResource( 7 )
material/0 = SubResource( 8 )
[node name="DirectionalLight" type="DirectionalLight" parent="."]
transform = Transform( -0.588165, 0.462179, -0.663666, -0.804031, -0.245728, 0.541436, 0.087159, 0.852061, 0.516134, -17.6076, 12.1748, 0 )
light_energy = 0.5
shadow_enabled = true
[node name="DemoInterface" parent="." instance=ExtResource( 1 )]
mouse_filter = 2
text_bbcode = "3D Arrive Demo
Move the mouse about the field to have the agent turn towards and smoothly arrive at the target marker."

View File

@ -0,0 +1,25 @@
extends Camera
var target: Spatial
onready var ray := $RayCast
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
_set_target_position(event.position)
func setup(_target: Spatial) -> void:
self.target = _target
_set_target_position(get_viewport().get_mouse_position())
func _set_target_position(position: Vector2) -> void:
var to = project_local_ray_normal(position) * 10000
ray.cast_to = to
ray.force_raycast_update()
if ray.is_colliding():
var point = ray.get_collision_point()
target.transform.origin = point

View File

@ -0,0 +1,93 @@
extends Node
export(float, 0, 50, 0.1) var linear_speed_max := 10.0 setget set_linear_speed_max
export(float, 0, 50, 0.1) var linear_acceleration_max := 1.0 setget set_linear_acceleration_max
export(float, 0, 50, 0.1) var arrival_tolerance := 0.5 setget set_arrival_tolerance
export(float, 0, 50, 0.1) var deceleration_radius := 5.0 setget set_deceleration_radius
export(int, 0, 359, 2) var angular_speed_max := 270 setget set_angular_speed_max
export(int, 0, 359, 2) var angular_accel_max := 45 setget set_angular_accel_max
export(int, 0, 180, 2) var align_tolerance := 5 setget set_align_tolerance
export(int, 0, 359, 2) var angular_deceleration_radius := 45 setget set_angular_deceleration_radius
onready var target := $MouseTarget
onready var arriver := $Arriver
func _ready() -> void:
arriver.setup(
deg2rad(align_tolerance),
deg2rad(angular_deceleration_radius),
deg2rad(angular_accel_max),
deg2rad(angular_speed_max),
deceleration_radius,
arrival_tolerance,
linear_acceleration_max,
linear_speed_max,
target
)
$Camera.setup(target)
func set_align_tolerance(value: int) -> void:
align_tolerance = value
if not is_inside_tree():
return
arriver.face.alignment_tolerance = deg2rad(value)
func set_angular_deceleration_radius(value: int) -> void:
deceleration_radius = value
if not is_inside_tree():
return
arriver.face.deceleration_radius = deg2rad(value)
func set_angular_accel_max(value: int) -> void:
angular_accel_max = value
if not is_inside_tree():
return
arriver.agent.angular_acceleration_max = deg2rad(value)
func set_angular_speed_max(value: int) -> void:
angular_speed_max = value
if not is_inside_tree():
return
arriver.agent.angular_speed_max = deg2rad(value)
func set_arrival_tolerance(value: float) -> void:
arrival_tolerance = value
if not is_inside_tree():
return
arriver.arrive.arrival_tolerance = value
func set_deceleration_radius(value: float) -> void:
deceleration_radius = value
if not is_inside_tree():
return
arriver.arrive.deceleration_radius = value
func set_linear_speed_max(value: float) -> void:
linear_speed_max = value
if not is_inside_tree():
return
arriver.agent.linear_speed_max = value
func set_linear_acceleration_max(value: float) -> void:
linear_acceleration_max = value
if not is_inside_tree():
return
arriver.agent.linear_acceleration_max = value

View File

@ -0,0 +1,48 @@
extends KinematicBody
var target_node: Spatial
onready var agent := GSAIKinematicBody3DAgent.new(self)
onready var target := GSAIAgentLocation.new()
onready var accel := GSAITargetAcceleration.new()
onready var blend := GSAIBlend.new(agent)
onready var face := GSAIFace.new(agent, target, true)
onready var arrive := GSAIArrive.new(agent, target)
func _physics_process(delta: float) -> void:
target.position = target_node.transform.origin
target.position.y = transform.origin.y
blend.calculate_steering(accel)
agent._apply_steering(accel, delta)
func setup(
align_tolerance: float,
angular_deceleration_radius: float,
angular_accel_max: float,
angular_speed_max: float,
deceleration_radius: float,
arrival_tolerance: float,
linear_acceleration_max: float,
linear_speed_max: float,
_target: Spatial
) -> void:
agent.linear_speed_max = linear_speed_max
agent.linear_acceleration_max = linear_acceleration_max
agent.linear_drag_percentage = 0.05
agent.angular_acceleration_max = angular_accel_max
agent.angular_speed_max = angular_speed_max
agent.angular_drag_percentage = 0.1
arrive.arrival_tolerance = arrival_tolerance
arrive.deceleration_radius = deceleration_radius
face.alignment_tolerance = align_tolerance
face.deceleration_radius = angular_deceleration_radius
target_node = _target
self.target.position = target_node.transform.origin
blend.add(arrive, 1)
blend.add(face, 1)

View File

@ -0,0 +1,4 @@
[gd_resource type="SpatialMaterial" format=2]
[resource]
albedo_color = Color( 0.152941, 0.764706, 0.247059, 1 )

View File

@ -17,6 +17,7 @@ script = ExtResource( 1 )
avoider_template = ExtResource( 3 )
inner_color = Color( 0.235294, 0.639216, 0.439216, 1 )
outer_color = Color( 0.560784, 0.870588, 0.364706, 1 )
agent_count = 80
[node name="DemoInterface" parent="." instance=ExtResource( 4 )]
text_bbcode = "Avoid Collisions Demo

View File

@ -4,6 +4,7 @@ extends Node2D
export var avoider_template: PackedScene
export var inner_color := Color()
export var outer_color := Color()
export var agent_count := 60
var boundaries: Vector2
@ -15,7 +16,7 @@ func _ready() -> void:
var rng: = RandomNumberGenerator.new()
var avoiders := []
var avoider_agents := []
for i in range(60):
for i in range(agent_count):
var avoider := avoider_template.instance()
add_child(avoider)
avoider.setup(
@ -33,6 +34,8 @@ func _ready() -> void:
avoider.collision.inner_color = inner_color
avoider.collision.outer_color = outer_color
avoiders.append(avoider)
if i % 10 == 0:
yield(get_tree(), "idle_frame")
for child in get_children():
child.set_proximity_agents(avoider_agents)

View File

@ -29,7 +29,7 @@ func _unhandled_input(event: InputEvent) -> void:
func _draw() -> void:
if is_drawing:
for point in active_points:
draw_circle(point, 1, Color.red)
draw_circle(point, 2, Color.red)
else:
if active_points.size() > 0:
draw_circle(active_points.front(), 2, Color.red)

View File

@ -6,6 +6,8 @@ const COLOR := Color("8fde5d")
func _ready() -> void:
get_tree().root.connect("size_changed", self, "_on_SceneTree_size_changed")
_on_SceneTree_size_changed()
func _draw() -> void:
@ -15,7 +17,10 @@ func _draw() -> void:
func _on_SceneTree_size_changed() -> void:
var size := get_tree().root.size
var size := Vector2(
ProjectSettings["display/window/size/width"],
ProjectSettings["display/window/size/height"]
)
for b in get_children():
var boundary: String = b.name.rsplit("Boundary")[0]
match boundary:

View File

@ -39,7 +39,7 @@ stroke = 4.0
script = ExtResource( 9 )
[node name="LeftBoundary" type="StaticBody2D" parent="Boundaries"]
position = Vector2( 0, 300 )
position = Vector2( 0, 540 )
collision_layer = 2
collision_mask = 5
@ -47,7 +47,7 @@ collision_mask = 5
shape = SubResource( 2 )
[node name="RightBoundary" type="StaticBody2D" parent="Boundaries"]
position = Vector2( 1024, 300 )
position = Vector2( 1920, 540 )
collision_layer = 2
collision_mask = 5
@ -55,7 +55,7 @@ collision_mask = 5
shape = SubResource( 2 )
[node name="TopBoundary" type="StaticBody2D" parent="Boundaries"]
position = Vector2( 512, 2.00002 )
position = Vector2( 960, 0 )
collision_layer = 2
collision_mask = 5
@ -63,7 +63,7 @@ collision_mask = 5
shape = SubResource( 3 )
[node name="BottomBoundary" type="StaticBody2D" parent="Boundaries"]
position = Vector2( 512, 600 )
position = Vector2( 960, 1080 )
collision_layer = 2
collision_mask = 5

View File

@ -99,8 +99,8 @@ func _set_body(value: KinematicBody2D) -> void:
func _on_SceneTree_physics_frame() -> void:
var current_position: Vector2 = body.global_position
var current_orientation: float = body.rotation
var current_position := body.global_position
var current_orientation := body.rotation
position = GSAIUtils.to_vector3(current_position)
orientation = current_orientation

View File

@ -90,7 +90,7 @@ func _apply_orientation_steering(angular_acceleration: float, delta: float) -> v
func _set_body(value: KinematicBody) -> void:
body = value
_last_position = body.global_position
_last_position = body.transform.origin
_last_orientation = body.rotation.y
position = _last_position
@ -98,8 +98,8 @@ func _set_body(value: KinematicBody) -> void:
func _on_SceneTree_physics_frame() -> void:
var current_position: Vector3 = body.global_position
var current_orientation: float = body.rotation.y
var current_position := body.transform.origin
var current_orientation := body.rotation.y
position = current_position
orientation = current_orientation

View File

@ -45,8 +45,8 @@ func _on_body_ready() -> void:
func _on_SceneTree_frame() -> void:
var current_position: Vector2 = body.global_position
var current_orientation: float = body.rotation
var current_position := body.global_position
var current_orientation := body.rotation
position = GSAIUtils.to_vector3(current_position)
orientation = current_orientation

View File

@ -34,7 +34,7 @@ func _apply_steering(acceleration: GSAITargetAcceleration, _delta: float) -> voi
func _set_body(value: RigidBody) -> void:
body = value
_last_position = body.global_position
_last_position = body.transform.origin
_last_orientation = body.rotation.y
position = _last_position
@ -48,8 +48,8 @@ func _on_body_ready() -> void:
func _on_SceneTree_frame() -> void:
var current_position: Vector3 = body.global_position
var current_orientation: float = body.rotation.y
var current_position := body.transform.origin
var current_orientation := body.rotation.y
position = current_position
orientation = current_orientation

View File

@ -4,7 +4,7 @@ class_name GSAIFace
extends GSAIMatchOrientation
func _init(agent: GSAISteeringAgent, target: GSAIAgentLocation).(agent, target) -> void:
func _init(agent: GSAISteeringAgent, target: GSAIAgentLocation, use_z := false).(agent, target, use_z) -> void:
pass
@ -15,7 +15,10 @@ func _face(acceleration: GSAITargetAcceleration, target_position: Vector3) -> vo
if distance_squared < agent.zero_linear_speed_threshold:
acceleration.set_zero()
else:
var orientation = GSAIUtils.vector3_to_angle(to_target)
var orientation = (GSAIUtils.vector3_to_angle(to_target)
if use_z
else GSAIUtils.vector2_to_angle(GSAIUtils.to_vector2(to_target))
)
_match_orientation(acceleration, orientation)

View File

@ -4,7 +4,7 @@ class_name GSAILookWhereYouGo
extends GSAIMatchOrientation
func _init(agent: GSAISteeringAgent).(agent, null) -> void:
func _init(agent: GSAISteeringAgent, use_z := false).(agent, null, use_z) -> void:
pass
@ -12,5 +12,8 @@ func _calculate_steering(accel: GSAITargetAcceleration) -> void:
if agent.linear_velocity.length_squared() < agent.zero_linear_speed_threshold:
accel.set_zero()
else:
var orientation := GSAIUtils.vector3_to_angle(agent.linear_velocity)
var orientation := (GSAIUtils.vector3_to_angle(agent.linear_velocity)
if use_z
else GSAIUtils.vector2_to_angle(GSAIUtils.to_vector2(agent.linear_velocity))
)
_match_orientation(accel, orientation)

View File

@ -14,9 +14,13 @@ var alignment_tolerance: float
var deceleration_radius: float
# The amount of time to reach the target velocity
var time_to_reach: float = 0.1
# Whether to use the X and Z components instead of X and Y components when
# determining angles. X and Z should be used in 3D.
var use_z: bool
func _init(agent: GSAISteeringAgent, _target: GSAIAgentLocation).(agent) -> void:
func _init(agent: GSAISteeringAgent, _target: GSAIAgentLocation, _use_z := false).(agent) -> void:
self.use_z = _use_z
self.target = _target

View File

@ -13,9 +13,14 @@ static func clampedv3(vector: Vector3, limit: float) -> Vector3:
# Returns an angle in radians between the positive X axis and the `vector`.
#
# This assumes orientation for 2D agents or 3D agents that are upright and
# rotate around the Y axis.
# This assumes orientation for 3D agents that are upright and rotate
# around the Y axis.
static func vector3_to_angle(vector: Vector3) -> float:
return atan2(vector.x, vector.z)
# Returns an angle in radians between the positive X axis and the `vector`.
static func vector2_to_angle(vector: Vector2) -> float:
return atan2(vector.x, -vector.y)