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 : GSAITargetAcceleration = null onready var current_health := health_max # GSAISteeringAgent holds our agent's position, orientation, maximum speed and acceleration. var agent : GSAISteeringAgent = null var player: Node = null # This assumes that our player class will keep its own agent updated. var player_agent : GSAISteeringAgent = null # 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. var proximity : GSAIRadiusProximity = null # 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. var flee_blend : GSAIBlend = null var pursue_blend : GSAIBlend = null # 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. var priority : GSAIPriority = null func _ready() -> void: acceleration = GSAITargetAcceleration.new() agent = GSAISteeringAgent.new() player = get_tree().get_nodes_in_group("Player")[0] player_agent = player.agent proximity = GSAIRadiusProximity.new() proximity.agent = agent proximity.agents = [ player_agent ] proximity.radius = 100 flee_blend = GSAIBlend.new() flee_blend.agent = agent pursue_blend = GSAIBlend.new() pursue_blend.agent = agent priority = GSAIPriority.new() priority.agent = agent # ---------- 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 : GSAIPursue = GSAIPursue.new() pursue.agent = agent pursue.target = 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 = GSAIFlee.new() flee.agent = agent flee.target = 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 = GSAIAvoidCollisions.new() avoid.agent = agent avoid.proximity = 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 = GSAIFace.new() face.agent = agent face.target = 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(60) # 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 = GSAILookWhereYouGo.new() look.agent = 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(60) # Behaviors that are not enabled produce 0 acceleration. # Adding our fleeing behaviors to a blend. The order does not matter. flee_blend.is_enabled = false flee_blend.add_behavior(look, 1) flee_blend.add_behavior(flee, 1) # Adding our pursuit behaviors to a blend. The order does not matter. pursue_blend.add_behavior(face, 1) pursue_blend.add_behavior(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_behavior(avoid) priority.add_behavior(flee_blend) priority.add_behavior(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.is_enabled = false flee_blend.is_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) * delta).limit_length( 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 * delta, -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()