diff --git a/docs/mvp.md b/docs/mvp.md deleted file mode 100644 index 059d985..0000000 --- a/docs/mvp.md +++ /dev/null @@ -1,25 +0,0 @@ -# First Release # - -## System ## - -- Standard types -- Combinations: Blended/Priority -- Seek/Flee -- Arrive -- Pursue/Evade -- Face -- LookWhereYouGo -- FollowPath -- Separation -- AvoidCollisions -- Should work in 2D and in 3D - -## Demos ## - -- 1 toy demo for each behavior -- 1 in context game demo that features at least two types of agents - -## Documentation ## - -- Getting started manual -- Quick reference for the behaviors diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..c95036e --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,206 @@ +# Godot Steering Toolkit # + +In the 1990s, [Craig Reynolds](http://www.red3d.com/cwr/) developed algorithms for common AI behaviors. They allowed AI agents to seek out or flee from a target, follow a pre-defined path, or face in a particular direction. They were simple, repeatable tasks that could be broken down into a programming algorithms which made them easy to reuse, maintain, combine and extend. + +While an AI agent's next action is based on decision making and planning algorithms, steering behaviors dictate how it will move from one frame to the next. They use available information and calculate where to move at that moment. + +Joining these systems together can give complex and graceful movement while also being more efficient than complex path finding algorithms like A\*. + +## Summary ## + +This toolkit is a framework for the [Godot 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. + +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. + +## 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. + +## Example usage ## + +The fastest way to get started is to look at a sample class that makes use of the toolkit. + +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 agent’s health is low, it will flee from the player directly. The agent will keep facing the player while it’s chasing them, but will look where it's going while it’s fleeing. + +Our game will be in 2D and assumed to be a top-down spaceship game. + +You can see the demo in action by opening the `demos/QuickStartDemo.tscn` file in Godot. The agent approaches the player and hovers near them until the agent is shot enough times, at which point it will try to flee. + +More details about how the various steering behaviors function can be found in the [Reference](https://github.com/GDQuest/godot-steering-toolkit/wiki/Manual-API-reference-draft) wiki page. + +```ruby +extends KinematicBody2D + + +# Maximum possible linear velocity +export var speed_max := 450.0 +# Maximum change in linear velocity +export var acceleration_max := 50.0 +# Maximum rotation velocity represented in degrees +export var angular_speed_max := 240 +# Maximum change in rotation velocity represented in degrees +export var angular_acceleration_max := 40 + +export var health_max := 100 +export var flee_health_threshold := 20 + +var velocity := Vector2.ZERO +var angular_velocity := 0.0 +var linear_drag := 0.1 +var angular_drag := 0.1 + +# Holds the linear and angular components calculated by our steering behaviors. +var acceleration := GSTTargetAcceleration.new() + +onready var current_health := health_max + +# GSTSteeringAgent holds our agent's position, orientation, maximum speed and acceleration. +onready var agent := GSTSteeringAgent.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: GSTSteeringAgent = 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 := GSTRadiusProximity.new(agent, [player_agent], 100). + +# GSTBlend 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 := GSTBlend.new(agent) +onready var pursue_blend := GSTBlend.new(agent) + +# GSTPriority 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 := GSTPriority.new(agent) + + +func _ready() -> void: + # ---------- Configuration for our agent ---------- + agent.linear_speed_max = speed_max + agent.linear_acceleration_max = acceleration_max + agent.angular_speed_max = deg2rad(angular_speed_max) + agent.angular_acceleration_max = deg2rad(angular_acceleration_max) + agent.bounding_radius = calculate_radius($CollisionPolygon2D.polygon) + update_agent() + + # ---------- Configuration for our behaviors ---------- + # 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 := GSTPursue.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 := GSTFlee.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 := GSTAvoidCollisions.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 := GSTFace.new(agent, player_agent) + + # We use deg2rad because the math in the toolkit assumes radians. + # How close for the agent to be 'aligned', if not exact. + face.alignment_tolerance = deg2rad(5) + # When to start slowing down. + face.deceleration_radius = deg2rad(45) + + # 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 := GSTLookWhereYouGo.new(agent) + # How close for the agent to be 'aligned', if not exact. + look.alignment_tolerance = deg2rad(5) + # When to start slowing down. + look.deceleration_radius = deg2rad(45) + + # Behaviors that are not enabled produce 0 acceleration. + # Adding our fleeing behaviors to a blend. The order does not matter. + flee_blend.enabled = false + flee_blend.add(look, 1) + flee_blend.add(flee, 1) + + # Adding our pursuit behaviors to a blend. The order does not matter. + pursue_blend.add(face, 1) + pursue_blend.add(pursue, 1) + + # Adding our final behaviors to the main priority behavior. The order does matter here. + # We want to avoid collision with the player first, flee from the player second when enabled, + # and pursue the player last when enabled. + priority.add(avoid) + priority.add(flee_blend) + priority.add(pursue_blend) + + +func _physics_process(delta: float) -> void: + # Make sure any change in position and speed has been recorded. + update_agent() + + if current_health <= flee_health_threshold: + pursue_blend.enabled = false + flee_blend.enabled = true + + # Calculate the desired acceleration. + priority.calculate_steering(acceleration) + + # We add the discovered acceleration to our linear velocity. The toolkit does not limit + # velocity, just acceleration, so we clamp the result ourselves here. + velocity = (velocity + Vector2( + acceleration.linear.x, acceleration.linear.y) + ).clamped(agent.linear_speed_max) + + # This applies drag on the agent's motion, helping it to slow down naturally. + velocity = velocity.linear_interpolate(Vector2.ZERO, linear_drag) + + # And since we're using a KinematicBody2D, we use Godot's excellent move_and_slide to actually + # apply the final movement, and record any change in velocity the physics engine discovered. + velocity = move_and_slide(velocity) + + # We then do something similar to apply our agent's rotational speed. + angular_velocity = clamp( + angular_velocity + acceleration.angular, + -agent.angular_speed_max, + agent.angular_speed_max + ) + # This applies drag on the agent's rotation, helping it slow down naturally. + angular_velocity = lerp(angular_velocity, 0, angular_drag) + rotation += angular_velocity * delta + + +# In order to support both 2D and 3D, the toolkit 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 + agent.position.y = global_position.y + agent.orientation = rotation + agent.linear_velocity.x = velocity.x + agent.linear_velocity.y = velocity.y + agent.angular_velocity = angular_velocity + + +# We calculate the radius from the collision shape - this will approximate the agent's size in the +# game world, to avoid collisions with the player. +func calculate_radius(polygon: PoolVector2Array) -> float: + var furthest_point := Vector2(-INF, -INF) + for p in polygon: + if abs(p.x) > furthest_point.x: + furthest_point.x = p.x + if abs(p.y) > furthest_point.y: + furthest_point.y = p.y + return furthest_point.length() + + +func damage(amount: int) -> void: + current_health -= amount + if current_health <= 0: + queue_free() + +``` + diff --git a/docs/techdetails.md b/docs/techdetails.md deleted file mode 100644 index c94cac8..0000000 --- a/docs/techdetails.md +++ /dev/null @@ -1,183 +0,0 @@ -# Steering Behavior Toolkit # - -This document describes an evolution to the design of the Steering Behavior AI system that was produced for the Hook! game. In that iteration, behaviors and agents were nodes, requiring tree iteration and holding node references. This version, on the other hand, is built entirely around Reference types which could be held on the root node of an actor. Once again, it is greatly inspired by the excellent GDX-AI module for LibGDX. - -This document will be changed to a more thorough set of documentation for the actual system once it's implemented. - -## Types ## - -### Agent ### - -_extends Reference_ - -The agent is the agent from which information is derived from to determine where the actor is, how fast it's currently going and rotating, as well as its mass and speed/velocity limits, and anything else the various behaviors use to calculate the result. It is the programmer's job to keep the Agent's values updated every frame. - -### Proximity ### - -_extends Reference_ - -Defines an area that is used by group behaviors to determine who is or isn't within the owner's neighbors. It is the programmer's job to make sure all relevant members of the group are within the Proximity so that no member is not accounted for. - -### TargetAcceleration ### - -_extends Reference_ - -A type that holds the desired increase to linear and angular velocities. Its contents get replaced by the behaviors. - -### Behaviors ### -#### Behavior #### - -_extends Reference_ - -The base type for steering behaviors. This will have a public facing calculate_steering, and a private facing version that should will be overriden. - -#### Combination Behaviors #### -##### Blended ##### - -_extends Behavior_ - -Blended combines any number of behaviors, each one having a certain weight that indicates how strongly it affects the end velocities. For instance, a Seek blended with a 2x force AvoidCollision. - -##### Priority ##### - -_extends Behavior_ - -Contains any number of behaviors, then iterates through them in order until one of them produces non-zero acceleration. Then it uses that one and skips the rest. - -#### Individual Behaviors #### - -##### Seek ##### - -_extends Behavior_ - -Given a target Agent, the math will produce a linear acceleration that will move it directly towards where the target is at this present time. - -##### Flee ##### - -_extends Seek_ - -Given a target Agent, the math will produce a linear acceleration that will move it directly away where the target is at this present time. - -##### Arrive ##### - -_extends Behavior_ - -Given a target Agent, the math will produce a linear acceleration that will move it directly towards where the target is at this present time, but aim to arrive there with zero velocity within a set amount of time. - -##### MatchOrientation ##### - -_extends Behavior_ - -Given a target Agent, the math will produce an angular acceleration that will rotate the agent until its degree of rotation matches the target's, aiming to have zero rotation by the time it reaches it. - -##### Pursue ##### - -_extends Behavior_ - -Given a target Agent, the math will produce a linear acceleration that will move it towards where the target will be by the time the agent reaches it, up to a maximum prediction time. - -##### Evade ##### - -_extends Pursue_ - -Given a target Agent, the math will produce a linear acceleration that will move it away from where the target will be by the time the agent would reach it, up to a maximum prediction time. - -##### Face ##### - -_extends MatchOrientation_ - -Given a target Agent, the math will produce an angular acceleration that will rotate the agent until it is facing its target, aiming to have zero rotation by the time it reaches it. - -##### LookWhereYouAreGoing ##### - -_extends MatchOrientation_ - -The math will produce an angular acceleration that will rotate the agent until it is facing its current direction of linear travel, or no change if it is not moving. - -##### FollowPath ##### - -_extends Arrive_ - -Given a target Array of locations making up a path, the math will produce a linear acceleration that will steer the agent along the path. Providing a non zero prediction time can make it cut corners, but appear to move more naturally. - -##### Intersect ##### - -_extends Arrive_ - -Given two target Agents and a ratio of distance between them, the math will produce a linear acceleration that will steer the agent to reach the destination between them, cutting through an imaginary line between them. - -##### MatchVelocity ##### - -_extends Behavior_ - -Given a target Agent, the math will produce a linear acceleration that will make its velocity the same as the target's. - -##### Jump ##### - -_extends MatchVelocity_ - -Given a jump starting point, a target landing point, and information about gravity, the math will produce an acceleration that will make the agent reach the starting point at the velocity required to successfully jump and land at the target landing point. - -#### Group Behaviors #### - -##### GroupBehavior ##### - -_extends Behavior_ - -Base class for steering behaviors that take other agents in the world into consideration within an area around the owner. - -##### Separation ##### - -_extends GroupBehavior_ - -Given a Proximity, the math will produce an acceleration that will keep the agent a minimum distance away from the proximity's owner. - -##### Alignment ##### - -_extends GroupBehavior_ - -Given a Proximity, the math will produce an angular acceleration that will turn the agent to face along with the proximity's owner. - -##### Cohesion ##### - -_extends GroupBehavior_ - -Given a Proximity, the math will produce a linear acceleration that will move the agent towards the center-of-mass of the Proximity group. - -##### Hide ##### - -_extends Arrive_ - -Given a Proximity of obstacles and a target Agent, the math will produce a linear acceleration that will move the agent to the nearest hiding point to hide from the target behind an obstacle. - -##### AvoidCollision ##### - -_extends GroupBehavior_ - -Given a Proximity of obstacles, the math will produce a linear acceleration that will move the agent away from the nearest obstacle in its proximity group. - -##### RaycastAvoidCollision ##### - -_extends Behavior_ - -Given a configuration of Raycasts to perform, the math will produce a linear acceleration that will steer the agent away from anything the raycasts happen to hit. - -## Usage ## - -Instead of creating a complex array of nodes in a tree, instead the programmer will create the behaviors they need with `Reference.new()`, configuring required fields and calling behaviors' calculate_steering as they need from where they need. For example: - - //#KinematicBody2D - var seek: = Seek.new() - - func _ready() -> void: - configure - - - //#StateMachine - //#Follow - var seek: Seek = owner.seek - var accel: = TargetAcceleration.new() - - func physics_process(delta: float) -> void: - accel = seek.calculate_steering(accel) - owner.move_and_slide(accel.linear) diff --git a/project/demos/Quickstart/Agent.gd b/project/demos/Quickstart/Agent.gd new file mode 100644 index 0000000..26e2704 --- /dev/null +++ b/project/demos/Quickstart/Agent.gd @@ -0,0 +1,170 @@ +extends KinematicBody2D + + +# Maximum possible linear velocity +export var speed_max := 450.0 +# Maximum change in linear velocity +export var acceleration_max := 50.0 +# Maximum rotation velocity represented in degrees +export var angular_speed_max := 240 +# Maximum change in rotation velocity represented in degrees +export var angular_acceleration_max := 40 + +export var health_max := 100 +export var flee_health_threshold := 20 + +var velocity := Vector2.ZERO +var angular_velocity := 0.0 +var linear_drag := 0.1 +var angular_drag := 0.1 + +# Holds the linear and angular components calculated by our steering behaviors. +var acceleration := GSTTargetAcceleration.new() + +onready var current_health := health_max + +# GSTSteeringAgent holds our agent's position, orientation, maximum speed and acceleration. +onready var agent := GSTSteeringAgent.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: GSTSteeringAgent = 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 := GSTRadiusProximity.new(agent, [player_agent], 100) + +# GSTBlend 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 := GSTBlend.new(agent) +onready var pursue_blend := GSTBlend.new(agent) + +# GSTPriority 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 := GSTPriority.new(agent) + + +func _ready() -> void: + # ---------- Configuration for our agent ---------- + agent.linear_speed_max = speed_max + agent.linear_acceleration_max = acceleration_max + agent.angular_speed_max = deg2rad(angular_speed_max) + agent.angular_acceleration_max = deg2rad(angular_acceleration_max) + agent.bounding_radius = calculate_radius($CollisionPolygon2D.polygon) + update_agent() + + # ---------- Configuration for our behaviors ---------- + # Pursue will happen while the agent 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 := GSTPursue.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 := GSTFlee.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 := GSTAvoidCollisions.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 := GSTFace.new(agent, player_agent) + + # We use deg2rad because the math in the toolkit assumes radians. + # How close for the agent to be 'aligned', if not exact. + face.alignment_tolerance = deg2rad(5) + # When to start slowing down + face.deceleration_radius = deg2rad(45) + + # 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 := GSTLookWhereYouGo.new(agent) + # How close for the agent to be 'aligned', if not exact + look.alignment_tolerance = deg2rad(5) + # When to start slowing down. + look.deceleration_radius = deg2rad(45) + + # Behaviors that are not enabled produce 0 acceleration. + # Adding our fleeing behaviors to a blend. The order does not matter. + flee_blend.enabled = false + flee_blend.add(look, 1) + flee_blend.add(flee, 1) + + # Adding our pursuit behaviors to a blend. The order does not matter. + pursue_blend.add(face, 1) + pursue_blend.add(pursue, 1) + + # Adding our final behaviors to the main priority behavior. The order does matter here. + # We want to avoid collision with the player first, flee from the player second when enabled, + # and pursue the player last when enabled. + priority.add(avoid) + priority.add(flee_blend) + priority.add(pursue_blend) + + +func _physics_process(delta: float) -> void: + # Make sure any change in position and speed has been recorded. + update_agent() + + if current_health <= flee_health_threshold: + pursue_blend.enabled = false + flee_blend.enabled = true + + # Calculate the desired acceleration. + priority.calculate_steering(acceleration) + + # We add the discovered acceleration to our linear velocity. The toolkit does not limit + # velocity, just acceleration, so we clamp the result ourselves here. + velocity = (velocity + Vector2( + acceleration.linear.x, acceleration.linear.y) + ).clamped(agent.linear_speed_max) + + # This applies drag on the agent's motion, helping it to slow down naturally. + velocity = velocity.linear_interpolate(Vector2.ZERO, linear_drag) + + # And since we're using a KinematicBody2D, we use Godot's excellent move_and_slide to actually + # apply the final movement, and record any change in velocity the physics engine discovered. + velocity = move_and_slide(velocity) + + # We then do something similar to apply our agent's rotational speed. + angular_velocity = clamp( + angular_velocity + acceleration.angular, + -agent.angular_speed_max, + agent.angular_speed_max + ) + # This applies drag on the agent's rotation, helping it slow down naturally. + angular_velocity = lerp(angular_velocity, 0, angular_drag) + rotation += angular_velocity * delta + + +# In order to support both 2D and 3D, the toolkit 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 + agent.position.y = global_position.y + agent.orientation = rotation + agent.linear_velocity.x = velocity.x + agent.linear_velocity.y = velocity.y + agent.angular_velocity = angular_velocity + + +# We calculate the radius from the collision shape - this will approximate the agent's size in the +# game world, to avoid collisions with the player. +func calculate_radius(polygon: PoolVector2Array) -> float: + var furthest_point := Vector2(-INF, -INF) + for p in polygon: + if abs(p.x) > furthest_point.x: + furthest_point.x = p.x + if abs(p.y) > furthest_point.y: + furthest_point.y = p.y + return furthest_point.length() + + +func damage(amount: int) -> void: + current_health -= amount + if current_health <= 0: + queue_free() diff --git a/project/demos/Quickstart/Bullet.gd b/project/demos/Quickstart/Bullet.gd new file mode 100644 index 0000000..9236f4c --- /dev/null +++ b/project/demos/Quickstart/Bullet.gd @@ -0,0 +1,34 @@ +extends KinematicBody2D + + +export var speed := 1500.0 + +var velocity := Vector2.ZERO +var player: Node + +onready var timer := $Lifetime + + +func _ready() -> void: + timer.connect("timeout", self, "_on_Lifetime_timeout") + timer.start() + + +func _physics_process(delta: float) -> void: + var collision := move_and_collide(velocity * delta) + if collision: + timer.stop() + clear() + collision.collider.damage(10) + + +func start(direction: Vector2) -> void: + velocity = direction * speed + + +func clear() -> void: + queue_free() + + +func _on_Lifetime_timeout() -> void: + clear() diff --git a/project/demos/Quickstart/Bullet.tscn b/project/demos/Quickstart/Bullet.tscn new file mode 100644 index 0000000..d3b4a89 --- /dev/null +++ b/project/demos/Quickstart/Bullet.tscn @@ -0,0 +1,24 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://assets/sprites/small_circle.png" type="Texture" id=1] +[ext_resource path="res://demos/Quickstart/Bullet.gd" type="Script" id=2] + +[sub_resource type="CircleShape2D" id=1] +radius = 4.0 + +[node name="Bullet" type="KinematicBody2D"] +collision_layer = 4 +collision_mask = 2 +script = ExtResource( 2 ) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource( 1 ) + +[node name="Sprite" type="Sprite" parent="."] +modulate = Color( 0.141176, 0.188235, 0.901961, 1 ) +scale = Vector2( 0.25, 0.25 ) +texture = ExtResource( 1 ) + +[node name="Lifetime" type="Timer" parent="."] +process_mode = 0 +wait_time = 3.0 diff --git a/project/demos/Quickstart/Player.gd b/project/demos/Quickstart/Player.gd new file mode 100644 index 0000000..403a8e1 --- /dev/null +++ b/project/demos/Quickstart/Player.gd @@ -0,0 +1,90 @@ +extends KinematicBody2D + + +export var speed_max := 650.0 +export var acceleration_max := 70.0 +export var rotation_speed_max := 240 +export var rotation_accel_max := 40 +export var bullet: PackedScene + +var velocity := Vector2.ZERO +var angular_velocity := 0.0 +var direction := Vector2.RIGHT + +onready var agent := GSTSteeringAgent.new() +onready var proxy_target := GSTAgentLocation.new() +onready var face := GSTFace.new(agent, proxy_target) +onready var accel := GSTTargetAcceleration.new() +onready var bullets := owner.get_node("Bullets") + + +func _ready() -> void: + agent.linear_speed_max = speed_max + agent.linear_acceleration_max = acceleration_max + agent.angular_speed_max = deg2rad(rotation_speed_max) + agent.angular_acceleration_max = deg2rad(rotation_accel_max) + agent.bounding_radius = calculate_radius($CollisionPolygon2D.polygon) + update_agent() + + var mouse_pos := get_global_mouse_position() + proxy_target.position.x = mouse_pos.x + proxy_target.position.y = mouse_pos.y + + face.alignment_tolerance = deg2rad(5) + face.deceleration_radius = deg2rad(45) + + +func _physics_process(delta: float) -> void: + update_agent() + + var movement := get_movement() + + direction = Vector2(sin(-rotation), cos(rotation)) + + velocity += direction * acceleration_max * movement + velocity = velocity.clamped(speed_max) + velocity = velocity.linear_interpolate(Vector2.ZERO, 0.1) + velocity = move_and_slide(velocity) + + face.calculate_steering(accel) + angular_velocity += accel.angular + angular_velocity = clamp(angular_velocity, -agent.angular_speed_max, agent.angular_speed_max) + angular_velocity = lerp(angular_velocity, 0, 0.1) + rotation += angular_velocity * delta + + +func _unhandled_input(event: InputEvent) -> void: + if event is InputEventMouseMotion: + var mouse_pos: Vector2 = event.position + proxy_target.position.x = mouse_pos.x + proxy_target.position.y = mouse_pos.y + elif event is InputEventMouseButton: + if event.button_index == BUTTON_LEFT and event.pressed: + var next_bullet: = bullet.instance() + next_bullet.global_position = global_position - direction * (agent.bounding_radius-5) + next_bullet.player = self + next_bullet.start(-direction) + bullets.add_child(next_bullet) + + +func get_movement() -> float: + return Input.get_action_strength("sf_down") - Input.get_action_strength("sf_up") + + +func update_agent() -> void: + agent.position.x = global_position.x + agent.position.y = global_position.y + agent.orientation = rotation + agent.linear_velocity.x = velocity.x + agent.linear_velocity.y = velocity.y + agent.angular_velocity = angular_velocity + + +func calculate_radius(polygon: PoolVector2Array) -> float: + var furthest_point := Vector2(-INF, -INF) + for p in polygon: + if abs(p.x) > furthest_point.x: + furthest_point.x = p.x + if abs(p.y) > furthest_point.y: + furthest_point.y = p.y + return furthest_point.length() diff --git a/project/demos/Quickstart/QuickStartDemo.tscn b/project/demos/Quickstart/QuickStartDemo.tscn new file mode 100644 index 0000000..13f70bf --- /dev/null +++ b/project/demos/Quickstart/QuickStartDemo.tscn @@ -0,0 +1,40 @@ +[gd_scene load_steps=5 format=2] + +[ext_resource path="res://assets/sprites/triangle.png" type="Texture" id=1] +[ext_resource path="res://demos/Quickstart/Agent.gd" type="Script" id=2] +[ext_resource path="res://demos/Quickstart/Player.gd" type="Script" id=3] +[ext_resource path="res://demos/Quickstart/Bullet.tscn" type="PackedScene" id=4] + +[node name="QuickStartDemo" type="Node2D"] + +[node name="Player" type="KinematicBody2D" parent="." groups=[ +"Player", +]] +position = Vector2( 235.469, 449.34 ) +rotation = 1.5708 +collision_mask = 2 +script = ExtResource( 3 ) +bullet = ExtResource( 4 ) + +[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="Player"] +polygon = PoolVector2Array( 0, -32, -24, 32, 24, 32 ) + +[node name="Sprite" type="Sprite" parent="Player"] +modulate = Color( 0.968627, 0.188235, 0.0352941, 1 ) +texture = ExtResource( 1 ) + +[node name="Agent" type="KinematicBody2D" parent="."] +position = Vector2( 807.798, 141.773 ) +rotation = 1.5708 +collision_layer = 2 +collision_mask = 5 +script = ExtResource( 2 ) + +[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="Agent"] +polygon = PoolVector2Array( 0, -32, -24, 32, 24, 32 ) + +[node name="Sprite" type="Sprite" parent="Agent"] +modulate = Color( 0.478431, 0.87451, 0.0784314, 1 ) +texture = ExtResource( 1 ) + +[node name="Bullets" type="Node2D" parent="."] diff --git a/project/demos/SeekFlee/SeekFleeDemo.gd b/project/demos/SeekFlee/SeekFleeDemo.gd index 8a19aa2..88d5da6 100644 --- a/project/demos/SeekFlee/SeekFleeDemo.gd +++ b/project/demos/SeekFlee/SeekFleeDemo.gd @@ -39,6 +39,7 @@ func _ready() -> void: entity.player_agent = player.agent entity.start_speed = linear_speed_max entity.start_accel = linear_accel_max + entity.use_seek = behavior_mode == Mode.SEEK spawner.add_child(entity) diff --git a/project/project.godot b/project/project.godot index d615c5b..dfc6310 100644 --- a/project/project.godot +++ b/project/project.godot @@ -161,10 +161,6 @@ _global_script_class_icons={ config/name="SteeringToolkit" config/icon="res://icon.png" -[display] - -window/size/always_on_top=true - [input] sf_left={