Grunkbeast behavior tree

This commit is contained in:
Rob Kelly 2025-04-17 13:41:06 -06:00
parent bb545bcbcd
commit e105601c1b
36 changed files with 503 additions and 170 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,9 @@
[gd_scene load_steps=12 format=3 uid="uid://cbxlfnlmgdvsq"] [gd_scene load_steps=12 format=3 uid="uid://cbxlfnlmgdvsq"]
[ext_resource type="PackedScene" uid="uid://d2664rpg4losx" path="res://src/world/procedural_grunk_beast/procedural_grunk_beast.tscn" id="1_6yv42"] [ext_resource type="Script" uid="uid://b1tbovuphat7d" path="res://levels/grunkbeast_test/procedural_grunkbeast_test.gd" id="1_ixhpa"]
[ext_resource type="Script" uid="uid://bukihqt1lybnx" path="res://src/util/frame_skipper.gd" id="1_eco5q"] [ext_resource type="Script" uid="uid://bukihqt1lybnx" path="res://src/util/frame_skipper.gd" id="2_82w0n"]
[ext_resource type="Script" uid="uid://b1tbovuphat7d" path="res://levels/grunkbeast_test/grunkbeast_test.gd" id="1_ovhaj"] [ext_resource type="PackedScene" uid="uid://d2664rpg4losx" path="res://src/world/procedural_grunk_beast/procedural_grunk_beast.tscn" id="3_b4iwh"]
[ext_resource type="Script" uid="uid://cpt8dy0csa3eu" path="res://levels/grunkbeast_test/fixed_camera.gd" id="2_77sam"] [ext_resource type="Script" uid="uid://cpt8dy0csa3eu" path="res://levels/grunkbeast_test/fixed_camera.gd" id="4_32imj"]
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_goufh"] [sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_goufh"]
@ -28,8 +28,8 @@ height = 1.0
height = 1.0 height = 1.0
radius = 2.0 radius = 2.0
[node name="GrunkbeastTest" type="Node3D"] [node name="ProceduralGrunkbeastTest" type="Node3D"]
script = ExtResource("1_ovhaj") script = ExtResource("1_ixhpa")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] [node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.866025, -0.156955, -0.474726, 0.5, 0.271854, 0.82225, 0, -0.949453, 0.31391, 0, 0, 0) transform = Transform3D(0.866025, -0.156955, -0.474726, 0.5, 0.271854, 0.82225, 0, -0.949453, 0.31391, 0, 0, 0)
@ -56,15 +56,15 @@ mesh = SubResource("CylinderMesh_4n07c")
shape = SubResource("CylinderShape3D_6yv42") shape = SubResource("CylinderShape3D_6yv42")
[node name="FrameSkipper" type="Node" parent="."] [node name="FrameSkipper" type="Node" parent="."]
script = ExtResource("1_eco5q") script = ExtResource("2_82w0n")
frame_skip = 3 frame_skip = 3
[node name="GrunkBeast" parent="FrameSkipper" instance=ExtResource("1_6yv42")] [node name="GrunkBeast" parent="FrameSkipper" instance=ExtResource("3_b4iwh")]
unique_name_in_owner = true unique_name_in_owner = true
move_speed = 8.0 move_speed = 8.0
step_time = 0.06 step_time = 0.06
[node name="Camera3D" type="Camera3D" parent="." node_paths=PackedStringArray("target")] [node name="Camera3D" type="Camera3D" parent="." node_paths=PackedStringArray("target")]
transform = Transform3D(0.252101, -0.522855, 0.81429, 0, 0.841469, 0.540306, -0.967701, -0.136212, 0.212135, 6.63551, 7.43415, 1.1904) transform = Transform3D(0.252101, -0.522855, 0.81429, 0, 0.841469, 0.540306, -0.967701, -0.136212, 0.212135, 6.63551, 7.43415, 1.1904)
script = ExtResource("2_77sam") script = ExtResource("4_32imj")
target = NodePath("../FrameSkipper/GrunkBeast") target = NodePath("../FrameSkipper/GrunkBeast")

View File

@ -37,11 +37,12 @@ background_mode = 2
sky = SubResource("Sky_pka60") sky = SubResource("Sky_pka60")
[sub_resource type="NavigationMesh" id="NavigationMesh_gyhlh"] [sub_resource type="NavigationMesh" id="NavigationMesh_gyhlh"]
vertices = PackedVector3Array(-26.25, 0.519, -29.25, -26.5, 0.519, -30.75, -27.5, 0.519, -30.75, -27.5, 0.519, -17.25, -26.75, 0.519, -18.25, -26.25, 0.519, -19, -0.25, 0.519, -17.5, 0.75, 0.519, -18, 0.75, 0.519, -19.75, -0.75, 0.519, -19, -1.25, 0.519, -18.5, -1, 0.519, -17.25, -3.25, 0.519, -24.25, -3.25, 0.519, -20.25, -1.25, 0.519, -20.25, 1.75, 0.519, -20.25, 1.75, 0.519, -24.25, -25.75, 0.519, -18.5, -25.25, 0.519, -18.25, -19.75, 0.519, -17.5, -24.75, 0.519, -17.25, -18, 0.519, -17.5, -0.5, 0.519, -16.75, -20.25, 0.519, -18.5, -0.5, 0.519, -6, 0.75, 0.519, -6, 0.75, 0.519, -12.75, 0, 0.519, -13.25, -17, 0.519, -18.5, -24.25, 0.519, -24.25, -24.25, 0.519, -20.25, 2.5, 0.519, -19.75, 24.25, 0.519, -24.25, 2.5, 0.519, -5.75, 24.25, 0.519, -5.5, -2.25, 0.519, -4.25, -2.25, 0.519, -15.5, -24.25, 0.519, -15.5, -1.5, 0.519, -3.75, -24.25, 0.519, 24.25, -1.5, 0.519, 24.25, 0.75, 0.519, -4.25, -0.5, 0.519, -4.25, 1.75, 0.519, -3.75, 1.75, 0.519, 24.25, 2.5, 0.519, -4.25, 24.25, 0.519, 24.25) vertices = PackedVector3Array(-26.25, 0.499, -29.25, -26.5, 0.499, -30.75, -27.5, 0.499, -30.75, -27.5, 0.499, -17.25, -26.75, 0.499, -18.25, -26.25, 0.499, -19, -0.25, 0.499, -17.5, 0.75, 0.499, -18, 0.75, 0.499, -19.75, -0.75, 0.499, -19, -1.25, 0.499, -18.5, -1, 0.499, -17.25, -3.25, 0.499, -24.25, -3.25, 0.499, -20.25, -1.25, 0.499, -20.25, 1.75, 0.499, -20.25, 1.75, 0.499, -24.25, -25.75, 0.499, -18.5, -25.5, 0.499, -18.25, -19.75, 0.499, -17.5, -24.75, 0.499, -17.25, -18, 0.499, -17.5, -0.5, 0.499, -16.75, -20.25, 0.499, -18.5, -0.5, 0.499, -6, 0.75, 0.499, -6, 0.75, 0.499, -12.75, 0, 0.499, -13.25, -17, 0.499, -18.5, -24.25, 0.499, -24.25, -24.25, 0.499, -20.25, 2.5, 0.499, -19.75, 24.25, 0.499, -24.25, 2.5, 0.499, -5.75, 24.25, 0.499, -5.5, -2.25, 0.499, -4.25, -2.25, 0.499, -15.5, -24.25, 0.499, -15.5, -1.5, 0.499, -3.75, -24.25, 0.499, 24.25, -1.5, 0.499, 24.25, 0.75, 0.499, -4.25, -0.5, 0.499, -4.25, 1.75, 0.499, -3.75, 1.75, 0.499, 24.25, 2.5, 0.499, -4.25, 24.25, 0.499, 24.25)
polygons = [PackedInt32Array(4, 3, 5), PackedInt32Array(5, 3, 0), PackedInt32Array(0, 3, 1), PackedInt32Array(1, 3, 2), PackedInt32Array(11, 10, 6), PackedInt32Array(6, 10, 9), PackedInt32Array(6, 9, 7), PackedInt32Array(7, 9, 8), PackedInt32Array(14, 13, 12), PackedInt32Array(8, 14, 15), PackedInt32Array(15, 14, 16), PackedInt32Array(16, 14, 12), PackedInt32Array(18, 4, 17), PackedInt32Array(20, 18, 19), PackedInt32Array(8, 9, 14), PackedInt32Array(19, 21, 20), PackedInt32Array(20, 21, 11), PackedInt32Array(6, 22, 11), PackedInt32Array(17, 23, 18), PackedInt32Array(18, 23, 19), PackedInt32Array(25, 24, 26), PackedInt32Array(26, 24, 27), PackedInt32Array(10, 11, 28), PackedInt32Array(28, 11, 21), PackedInt32Array(6, 27, 22), PackedInt32Array(22, 27, 24), PackedInt32Array(17, 4, 5), PackedInt32Array(30, 29, 13), PackedInt32Array(13, 29, 12), PackedInt32Array(15, 16, 31), PackedInt32Array(31, 16, 32), PackedInt32Array(33, 31, 34), PackedInt32Array(34, 31, 32), PackedInt32Array(37, 36, 35), PackedInt32Array(35, 38, 37), PackedInt32Array(37, 38, 40), PackedInt32Array(37, 40, 39), PackedInt32Array(42, 24, 41), PackedInt32Array(41, 24, 25), PackedInt32Array(41, 43, 42), PackedInt32Array(42, 43, 38), PackedInt32Array(38, 43, 44), PackedInt32Array(38, 44, 40), PackedInt32Array(45, 33, 34), PackedInt32Array(45, 34, 43), PackedInt32Array(43, 34, 44), PackedInt32Array(44, 34, 46)] polygons = [PackedInt32Array(4, 3, 5), PackedInt32Array(5, 3, 0), PackedInt32Array(0, 3, 1), PackedInt32Array(1, 3, 2), PackedInt32Array(11, 10, 6), PackedInt32Array(6, 10, 9), PackedInt32Array(6, 9, 7), PackedInt32Array(7, 9, 8), PackedInt32Array(14, 13, 12), PackedInt32Array(8, 14, 15), PackedInt32Array(15, 14, 16), PackedInt32Array(16, 14, 12), PackedInt32Array(18, 4, 17), PackedInt32Array(20, 18, 19), PackedInt32Array(8, 9, 14), PackedInt32Array(19, 21, 20), PackedInt32Array(20, 21, 11), PackedInt32Array(6, 22, 11), PackedInt32Array(17, 23, 18), PackedInt32Array(18, 23, 19), PackedInt32Array(25, 24, 26), PackedInt32Array(26, 24, 27), PackedInt32Array(10, 11, 28), PackedInt32Array(28, 11, 21), PackedInt32Array(6, 27, 22), PackedInt32Array(22, 27, 24), PackedInt32Array(17, 4, 5), PackedInt32Array(30, 29, 13), PackedInt32Array(13, 29, 12), PackedInt32Array(15, 16, 31), PackedInt32Array(31, 16, 32), PackedInt32Array(33, 31, 34), PackedInt32Array(34, 31, 32), PackedInt32Array(37, 36, 35), PackedInt32Array(35, 38, 37), PackedInt32Array(37, 38, 40), PackedInt32Array(37, 40, 39), PackedInt32Array(42, 24, 41), PackedInt32Array(41, 24, 25), PackedInt32Array(41, 43, 42), PackedInt32Array(42, 43, 38), PackedInt32Array(38, 43, 44), PackedInt32Array(38, 44, 40), PackedInt32Array(45, 33, 34), PackedInt32Array(45, 34, 43), PackedInt32Array(43, 34, 44), PackedInt32Array(44, 34, 46)]
geometry_parsed_geometry_type = 1 geometry_parsed_geometry_type = 1
geometry_collision_mask = 4278190081 geometry_collision_mask = 4278190081
agent_height = 2.0 agent_height = 2.0
agent_radius = 0.75
[sub_resource type="PlaneMesh" id="PlaneMesh_nwuu1"] [sub_resource type="PlaneMesh" id="PlaneMesh_nwuu1"]
size = Vector2(50, 50) size = Vector2(50, 50)
@ -335,6 +336,7 @@ navigation_mesh = SubResource("NavigationMesh_gyhlh")
[node name="WorldFloor" type="StaticBody3D" parent="NavigationRegion3D" groups=["PlasticMaterial"]] [node name="WorldFloor" type="StaticBody3D" parent="NavigationRegion3D" groups=["PlasticMaterial"]]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.001, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.001, 0)
collision_layer = 5
[node name="MeshInstance3D" type="MeshInstance3D" parent="NavigationRegion3D/WorldFloor"] [node name="MeshInstance3D" type="MeshInstance3D" parent="NavigationRegion3D/WorldFloor"]
mesh = SubResource("PlaneMesh_nwuu1") mesh = SubResource("PlaneMesh_nwuu1")

View File

@ -63,8 +63,9 @@ folder_colors={
"res://levels/": "blue", "res://levels/": "blue",
"res://levels/ghost_ship/": "purple", "res://levels/ghost_ship/": "purple",
"res://src/": "green", "res://src/": "green",
"res://src/ui/": "yellow", "res://src/ui/": "orange",
"res://src/world/": "teal" "res://src/world/": "teal",
"res://src/world/grunk_beast/behaviors/": "yellow"
} }
[game] [game]

View File

@ -0,0 +1,14 @@
@tool
class_name BlackboardCopy extends ActionLeaf
## Copy a value from one blackboard key to another.
## Source blackboard key.
@export var source_key: String
## Destination blackboard key.
@export var destination_key: String
func tick(_actor: Node, blackboard: Blackboard) -> int:
blackboard.set_value(destination_key, blackboard.get_value(source_key))
return SUCCESS

View File

@ -0,0 +1 @@
uid://wfd5nbethyw6

View File

@ -0,0 +1,14 @@
@tool
class_name BlackboardEraseSafe extends ActionLeaf
## Erases the specified key from the blackboard.
##
## Unlike Beehave's builtin BlackboardEraseAction, this version will succeed silently if
## the given key is not defined.
## Expression representing a blackboard key.
@export var key := ""
func tick(_actor: Node, blackboard: Blackboard) -> int:
blackboard.erase_value(key)
return SUCCESS

View File

@ -0,0 +1 @@
uid://demv7xh27ouvr

View File

@ -0,0 +1,20 @@
@tool
class_name GrabTarget extends ActionLeaf
## Grab the given target!!
##
## Returns FAILURE if there is no valid key at the given target.
## Blackboard key of the grab target.
@export var blackboard_key := "target"
func tick(_actor: Node, blackboard: Blackboard) -> int:
@warning_ignore("unsafe_cast")
var target := blackboard.get_value(blackboard_key) as Node3D
if not is_instance_valid(target):
return FAILURE
# TODO
print_debug("GET FUCKIGN GRABBED IDIOT!! ", target)
return SUCCESS

View File

@ -0,0 +1 @@
uid://b0xue7ao0gjqo

View File

@ -0,0 +1,19 @@
@tool
class_name PickRandomFromGroup extends ActionLeaf
## Picks a random node in the given group and stores it in the blackboard.
##
## Returns FAILURE if there are no nodes in the given group.
## Blackboard key under which the node will be stored.
@export var blackboard_key := "target"
## Name of the group to pick from.
@export var group: String
func tick(actor: Node, blackboard: Blackboard) -> int:
var nodes: Array[Node] = actor.get_tree().get_nodes_in_group(group)
if not nodes:
return FAILURE
blackboard.set_value(blackboard_key, nodes.pick_random())
return SUCCESS

View File

@ -0,0 +1 @@
uid://cg55nu4y0a5ud

View File

@ -0,0 +1,20 @@
@tool
class_name PursueTarget extends ActionLeaf
## Wait until the navigation target is reached (or cannot navigate further)
##
## This action will update the target's position each tick, allowing for tracking moving targets.
## Returns FAILURE if there isn't a Node3D at the given blackboard key.
@export var blackboard_key := "target"
@onready var nav_agent: NavigationAgent3D = %NavAgent
func tick(_actor: Node, blackboard: Blackboard) -> int:
@warning_ignore("unsafe_cast")
var target := blackboard.get_value(blackboard_key) as Node3D
if not is_instance_valid(target):
return FAILURE
nav_agent.target_position = target.global_position
return SUCCESS if nav_agent.is_navigation_finished() else RUNNING

View File

@ -0,0 +1 @@
uid://bkdwuqv4tudka

View File

@ -0,0 +1,26 @@
@tool
class_name SetNavTarget extends ActionLeaf
## Set the nav agent target position to the global position at the given blackboard key.
##
## The target value may be a Vector3 position in space,
## or a Node3D, in which case the global position of the node will be used as the target position.
##
## Returns failure if there is no valid target under the given blackboard key.
## Blackboard key of the Node3D to set as a target
@export var blackboard_key := "target"
@onready var nav_agent: NavigationAgent3D = %NavAgent
func tick(_actor: Node, blackboard: Blackboard) -> int:
var value: Variant = blackboard.get_value(blackboard_key)
if value is Vector3:
@warning_ignore("unsafe_cast")
nav_agent.target_position = value as Vector3
elif value is Node3D and is_instance_valid(value):
@warning_ignore("unsafe_cast")
nav_agent.target_position = (value as Node3D).global_position
else:
return FAILURE
return SUCCESS

View File

@ -0,0 +1 @@
uid://u1ntpwjwjqhj

View File

@ -0,0 +1,21 @@
@tool
class_name SetTargetFromArea extends ActionLeaf
## Store the first node in the given area on the blackboard.
##
## Returns FAILURE if there is no such node overlapping the given area.
## NOTE: You probably should only use this with areas masked to only have one possible body.
## Key to store the target under.
@export var blackboard_key := "target"
## Area to get target from.
@export var area: Area3D
func tick(_actor: Node, blackboard: Blackboard) -> int:
var bodies := area.get_overlapping_bodies()
if not bodies:
return FAILURE
blackboard.set_value(blackboard_key, bodies[0])
return SUCCESS

View File

@ -0,0 +1 @@
uid://b34l3v4sr8rmq

View File

@ -0,0 +1,18 @@
@tool
class_name StartTimer extends ActionLeaf
## Start the given timer.
## If `seconds > 0`, start the timer with that duration, otherwise use the default.
## Timer to start.
@export var timer: Timer
## Number of seconds to set the timer for. If <0, use the default.
@export var seconds := -1.0
func tick(_actor: Node, _blackboard: Blackboard) -> int:
if seconds > 0:
timer.start(seconds)
else:
timer.start()
return SUCCESS

View File

@ -0,0 +1 @@
uid://ykfqlqp7e8og

View File

@ -0,0 +1,9 @@
@tool
class_name TravelToDestination extends ActionLeaf
## Wait until the navigation target is reached (or cannot navigate further)
@onready var nav_agent: NavigationAgent3D = %NavAgent
func tick(_actor: Node, _blackboard: Blackboard) -> int:
return SUCCESS if nav_agent.is_navigation_finished() else RUNNING

View File

@ -0,0 +1 @@
uid://om57w2acvgb7

View File

@ -0,0 +1,11 @@
@tool
class_name AreaHasBodies extends ConditionLeaf
## Does the given Area3D have any overlapping bodies?
## Area3D to check for overlapping bodies.
@export var area: Area3D
func tick(_actor: Node, _blackboard: Blackboard) -> int:
area.get_overlapping_bodies()
return SUCCESS if area.has_overlapping_bodies() else FAILURE

View File

@ -0,0 +1 @@
uid://ulw86qyy1knh

View File

@ -0,0 +1,16 @@
@tool
class_name BodyInArea extends ConditionLeaf
## Is the body at the given blackboard key in the given area?
##
## Returns FAILURE if there is no Node3D at the given blackboard key.
## Blackboard key of the body to check.
@export var blackboard_key := "target"
## Area3D to check for the target body.
@export var area: Area3D
func tick(_actor: Node, blackboard: Blackboard) -> int:
@warning_ignore("unsafe_cast")
var body := blackboard.get_value(blackboard_key) as Node3D
return SUCCESS if is_instance_valid(body) and body in area.get_overlapping_bodies() else FAILURE

View File

@ -0,0 +1 @@
uid://7k5hor1omsxc

View File

@ -0,0 +1,9 @@
@tool
class_name TargetReached extends ConditionLeaf
## Has the nav agent reached its current target?
@onready var nav_agent: NavigationAgent3D = %NavAgent
func tick(_actor: Node, _blackboard: Blackboard) -> int:
return SUCCESS if nav_agent.is_target_reached() else FAILURE

View File

@ -0,0 +1 @@
uid://bnd8y8fkvg082

View File

@ -0,0 +1,10 @@
@tool
class_name TimerRunning extends ConditionLeaf
## Is the given timer currently running?
## Timer to check.
@export var timer: Timer
func tick(_actor: Node, _blackboard: Blackboard) -> int:
return FAILURE if timer.is_stopped() else SUCCESS

View File

@ -0,0 +1 @@
uid://h0cp58nswpml

View File

@ -0,0 +1,31 @@
@tool
class_name RandomDelay extends DelayDecorator
## DelayDecorator which randomizes the wait time to a normal random sample each time it's run.
## Mean wait time, in seconds.
@export var mean_time := 1.0:
set(value):
mean_time = value
randomize_wait_time()
## Standard deviation of wait time, in seconds.
@export var st_dev_time := 0.1:
set(value):
st_dev_time = value
randomize_wait_time()
func _ready() -> void:
randomize_wait_time()
func randomize_wait_time() -> void:
wait_time = maxf(0.0, randfn(mean_time, st_dev_time))
func tick(actor: Node, blackboard: Blackboard) -> int:
var total_time: float = blackboard.get_value(cache_key, 0.0, str(actor.get_instance_id()))
if total_time == 0.0:
randomize_wait_time()
return super.tick(actor, blackboard)

View File

@ -0,0 +1 @@
uid://beyk2xtbjrsg4

View File

@ -2,25 +2,12 @@ class_name GrunkBeast extends CharacterBody3D
## Grunk beast controller ## Grunk beast controller
#region Constants #region Constants
signal state_changed(new_state: State) const STALKING_SOUND_LIMIT := 20.0
enum State {
UNKNOWN,
LURKING,
STALKING,
PURSUIT,
}
const LURK_POINT_GROUP := "LurkPoint"
const STALKING_SOUND_LIMIT := 14.0
const IDLE_EPSILON := 0.001
#endregion #endregion
#region Exported Properties #region Exported Properties
@export var lurk_spontaneity := 0.02
@export var base_speed := 60.0 @export var base_speed := 60.0
@export var pursuit_speed := 180.0 @export var pursuit_speed := 180.0
@export var initial_state := State.LURKING
#endregion #endregion
#region Member Variables #region Member Variables
@ -29,29 +16,52 @@ var gravity: Vector3 = (
* ProjectSettings.get_setting("physics/3d/default_gravity_vector") * ProjectSettings.get_setting("physics/3d/default_gravity_vector")
) )
var state: State:
set(value):
if state != value:
_on_state_change(value)
state = value
var pathfinding := true var pathfinding := true
@onready var nav_agent: NavigationAgent3D = %NavAgent @onready var nav_agent: NavigationAgent3D = %NavAgent
@onready var stalking_timeout: Timer = %StalkingTimeout @onready var nav_probe: NavigationAgent3D = %NavProbe
@onready var idle_timer: Timer = %IdleTimer @onready var stalking_timer: Timer = %StalkingTimer
@onready var blackboard: Blackboard = %Blackboard
#endregion #endregion
#region Character Controller #region Character Controller
func is_pursuing() -> bool:
return blackboard.has_value("pursuit_target")
func is_stalking() -> bool:
return false # TODO
func get_speed() -> float: func get_speed() -> float:
match state: if is_pursuing():
State.PURSUIT: return pursuit_speed
return pursuit_speed
return base_speed return base_speed
func path_shorter_than(target: Vector3, limit: float) -> bool:
var limit_sq := limit * limit
var length_sq := 0.0
var last_pos := global_position
nav_probe.target_position = target
for waypoint: Vector3 in nav_probe.get_current_navigation_path().slice(
nav_probe.get_current_navigation_path_index()
):
# Using distance squared to save a sqrt instruction
length_sq += last_pos.distance_squared_to(waypoint)
if length_sq > limit_sq:
return false
last_pos = waypoint
return true
func _physics_process(delta: float) -> void: func _physics_process(delta: float) -> void:
var motion := Vector3.ZERO var motion := Vector3.ZERO
@ -66,114 +76,13 @@ func _physics_process(delta: float) -> void:
if not is_on_floor(): if not is_on_floor():
velocity += gravity * delta velocity += gravity * delta
if velocity.length_squared() > IDLE_EPSILON:
idle_timer.start()
move_and_slide() move_and_slide()
#endregion
#region State Machine
func _ready() -> void:
state = initial_state
func _current_path_shorter_than(limit: float) -> bool:
var limit_sq := limit * limit
var length_sq := 0.0
var last_pos := global_position
for waypoint: Vector3 in nav_agent.get_current_navigation_path().slice(
nav_agent.get_current_navigation_path_index()
):
# Using distance squared to save a sqrt instruction
length_sq += last_pos.distance_squared_to(waypoint)
if length_sq > limit_sq:
return false
last_pos = waypoint
return true
func _set_new_lurk_point() -> void:
@warning_ignore("unsafe_cast")
var next_lurk_point := get_tree().get_nodes_in_group(LURK_POINT_GROUP).pick_random() as Node3D
if next_lurk_point:
nav_agent.target_position = next_lurk_point.global_position
else:
push_warning(self, " tried to lurk, but couldn't find a lurk point!")
## Lurking routine
func lurk() -> void:
if nav_agent.is_target_reached() or randf() < lurk_spontaneity:
_set_new_lurk_point()
## Stalking routine
func stalk() -> void:
pass
## Pursuit routine
func pursue() -> void:
if Player.instance:
nav_agent.target_position = Player.instance.global_position
func _on_state_change(new_state: State) -> void:
match new_state:
State.LURKING:
_set_new_lurk_point()
State.STALKING:
stalking_timeout.start()
state_changed.emit(new_state)
func update_behavior() -> void:
match state:
State.UNKNOWN:
state = State.LURKING
update_behavior()
State.LURKING:
lurk()
State.STALKING:
stalk()
State.PURSUIT:
pursue()
func on_sound_detected(source: Vector3) -> void: func on_sound_detected(source: Vector3) -> void:
var prev_target := nav_agent.target_position
nav_agent.target_position = source
# Check that the source isn't too far away, e.g. a sound from another room # Check that the source isn't too far away, e.g. a sound from another room
if _current_path_shorter_than(STALKING_SOUND_LIMIT): if path_shorter_than(source, STALKING_SOUND_LIMIT):
# First detection switches to stalking, second switches to pursuit blackboard.set_value("stalking_target", source)
if state == State.STALKING: stalking_timer.start()
state = State.PURSUIT
else:
state = State.STALKING
else:
nav_agent.target_position = prev_target
func on_player_entered_grabbing_range(_body: Node3D) -> void:
print_debug("GET GRABBED, IDIOT!")
func on_player_left_pursuit_range(_body: Node3D) -> void:
state = State.STALKING
func on_stalking_timeout_timeout() -> void:
state = State.LURKING
func on_stalking_idle_timer_timeout() -> void:
pass # Replace with function body.
#endregion #endregion

View File

@ -1,10 +1,30 @@
[gd_scene load_steps=16 format=3 uid="uid://ehf5sg3ahvbf"] [gd_scene load_steps=37 format=3 uid="uid://ehf5sg3ahvbf"]
[ext_resource type="Script" uid="uid://gwwmqwixqqr5" path="res://src/world/grunk_beast/grunk_beast.gd" id="2_qqnhb"] [ext_resource type="Script" uid="uid://gwwmqwixqqr5" path="res://src/world/grunk_beast/grunk_beast.gd" id="2_qqnhb"]
[ext_resource type="Shader" uid="uid://ckxc0ngd37rtk" path="res://src/shaders/gunk.gdshader" id="4_0gxpq"] [ext_resource type="Shader" uid="uid://ckxc0ngd37rtk" path="res://src/shaders/gunk.gdshader" id="4_0gxpq"]
[ext_resource type="Script" uid="uid://cfsiyhhrcua6o" path="res://src/world/game_sound/game_sound_listener.gd" id="5_3gbao"] [ext_resource type="Script" uid="uid://cfsiyhhrcua6o" path="res://src/world/game_sound/game_sound_listener.gd" id="5_3gbao"]
[ext_resource type="Texture2D" uid="uid://cm1jrvx7ftx4c" path="res://assets/black.png" id="5_xuag8"] [ext_resource type="Texture2D" uid="uid://cm1jrvx7ftx4c" path="res://assets/black.png" id="5_xuag8"]
[ext_resource type="Script" uid="uid://bb0t2ovl7wifo" path="res://addons/beehave/nodes/beehave_tree.gd" id="6_d4ex2"]
[ext_resource type="FastNoiseLite" uid="uid://cnlvdtx68giv6" path="res://assets/materials/gunk_noise.tres" id="6_mbqcc"] [ext_resource type="FastNoiseLite" uid="uid://cnlvdtx68giv6" path="res://assets/materials/gunk_noise.tres" id="6_mbqcc"]
[ext_resource type="Script" uid="uid://dme5f24l0edsf" path="res://addons/beehave/blackboard.gd" id="7_cn3ok"]
[ext_resource type="Script" uid="uid://cw22yurt5l74k" path="res://addons/beehave/nodes/composites/selector_reactive.gd" id="7_vvw1q"]
[ext_resource type="Script" uid="uid://cg016dbe7gs1x" path="res://addons/beehave/nodes/composites/sequence.gd" id="8_0gxpq"]
[ext_resource type="Script" uid="uid://7k5hor1omsxc" path="res://src/world/grunk_beast/behaviors/conditions/body_in_area.gd" id="9_xuag8"]
[ext_resource type="Script" uid="uid://u1ntpwjwjqhj" path="res://src/world/grunk_beast/behaviors/actions/set_nav_target.gd" id="10_kjykp"]
[ext_resource type="Script" uid="uid://2qri6rrfv8ui" path="res://addons/beehave/nodes/decorators/cooldown.gd" id="10_ntlom"]
[ext_resource type="Script" uid="uid://beyk2xtbjrsg4" path="res://src/world/grunk_beast/behaviors/decorators/random_delay.gd" id="11_mbqcc"]
[ext_resource type="Script" uid="uid://b0xue7ao0gjqo" path="res://src/world/grunk_beast/behaviors/actions/grab_target.gd" id="11_nq7ke"]
[ext_resource type="Script" uid="uid://8hn4kne15ac5" path="res://addons/beehave/nodes/composites/selector.gd" id="12_dkcdj"]
[ext_resource type="Script" uid="uid://cg55nu4y0a5ud" path="res://src/world/grunk_beast/behaviors/actions/pick_random_from_group.gd" id="12_ml8dd"]
[ext_resource type="Script" uid="uid://dcojdhvj8qcw0" path="res://addons/beehave/nodes/composites/sequence_reactive.gd" id="12_xde72"]
[ext_resource type="Script" uid="uid://b34l3v4sr8rmq" path="res://src/world/grunk_beast/behaviors/actions/set_target_from_area.gd" id="13_x8l6r"]
[ext_resource type="Script" uid="uid://om57w2acvgb7" path="res://src/world/grunk_beast/behaviors/actions/travel_to_destination.gd" id="14_4y64f"]
[ext_resource type="Script" uid="uid://bkdwuqv4tudka" path="res://src/world/grunk_beast/behaviors/actions/pursue_target.gd" id="14_x8l6r"]
[ext_resource type="Script" uid="uid://demv7xh27ouvr" path="res://src/world/grunk_beast/behaviors/actions/blackboard_erase_safe.gd" id="15_4b27i"]
[ext_resource type="Script" uid="uid://dwfdg523bk776" path="res://addons/beehave/nodes/decorators/failer.gd" id="15_oons1"]
[ext_resource type="Script" uid="uid://wfd5nbethyw6" path="res://src/world/grunk_beast/behaviors/actions/blackboard_copy.gd" id="15_umoec"]
[ext_resource type="Script" uid="uid://ykfqlqp7e8og" path="res://src/world/grunk_beast/behaviors/actions/start_timer.gd" id="16_asd50"]
[ext_resource type="Script" uid="uid://h0cp58nswpml" path="res://src/world/grunk_beast/behaviors/conditions/timer_running.gd" id="16_oons1"]
[sub_resource type="NoiseTexture3D" id="NoiseTexture3D_faau1"] [sub_resource type="NoiseTexture3D" id="NoiseTexture3D_faau1"]
width = 256 width = 256
@ -62,10 +82,10 @@ radial_segments = 7
rings = 1 rings = 1
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_faau1"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_faau1"]
radius = 0.45 radius = 0.4
[sub_resource type="SphereShape3D" id="SphereShape3D_wffas"] [sub_resource type="SphereShape3D" id="SphereShape3D_wffas"]
radius = 7.0 radius = 16.0
[sub_resource type="SphereShape3D" id="SphereShape3D_3gbao"] [sub_resource type="SphereShape3D" id="SphereShape3D_3gbao"]
radius = 1.4 radius = 1.4
@ -73,7 +93,11 @@ radius = 1.4
[sub_resource type="SphereShape3D" id="SphereShape3D_d4ex2"] [sub_resource type="SphereShape3D" id="SphereShape3D_d4ex2"]
radius = 20.0 radius = 20.0
[sub_resource type="SphereShape3D" id="SphereShape3D_oons1"]
radius = 3.0
[node name="GrunkBeast" type="CharacterBody3D"] [node name="GrunkBeast" type="CharacterBody3D"]
collision_layer = 36
script = ExtResource("2_qqnhb") script = ExtResource("2_qqnhb")
[node name="MeshInstance3D" type="MeshInstance3D" parent="."] [node name="MeshInstance3D" type="MeshInstance3D" parent="."]
@ -90,19 +114,13 @@ shape = SubResource("CapsuleShape3D_faau1")
[node name="NavAgent" type="NavigationAgent3D" parent="."] [node name="NavAgent" type="NavigationAgent3D" parent="."]
unique_name_in_owner = true unique_name_in_owner = true
path_desired_distance = 0.75
avoidance_enabled = true
debug_enabled = true
[node name="IdleTimer" type="Timer" parent="."] [node name="NavProbe" type="NavigationAgent3D" parent="."]
unique_name_in_owner = true unique_name_in_owner = true
wait_time = 1.2 path_desired_distance = 0.75
one_shot = true
[node name="StalkingTimeout" type="Timer" parent="."]
unique_name_in_owner = true
wait_time = 25.0
one_shot = true
[node name="BehaviorUpdateTimer" type="Timer" parent="."]
autostart = true
[node name="GameSoundListener" type="StaticBody3D" parent="."] [node name="GameSoundListener" type="StaticBody3D" parent="."]
collision_layer = 16 collision_layer = 16
@ -120,15 +138,167 @@ collision_mask = 8
[node name="CollisionShape3D" type="CollisionShape3D" parent="GrabbingRange"] [node name="CollisionShape3D" type="CollisionShape3D" parent="GrabbingRange"]
shape = SubResource("SphereShape3D_3gbao") shape = SubResource("SphereShape3D_3gbao")
[node name="PursuitLimit" type="Area3D" parent="."] [node name="PursuitRange" type="Area3D" parent="."]
collision_layer = 0 collision_layer = 0
collision_mask = 8 collision_mask = 8
[node name="CollisionShape3D" type="CollisionShape3D" parent="PursuitLimit"] [node name="CollisionShape3D" type="CollisionShape3D" parent="PursuitRange"]
shape = SubResource("SphereShape3D_d4ex2") shape = SubResource("SphereShape3D_d4ex2")
[connection signal="timeout" from="StalkingTimeout" to="." method="on_stalking_timeout_timeout"] [node name="AggroRange" type="Area3D" parent="."]
[connection signal="timeout" from="BehaviorUpdateTimer" to="." method="update_behavior"] collision_layer = 0
collision_mask = 8
[node name="CollisionShape3D" type="CollisionShape3D" parent="AggroRange"]
shape = SubResource("SphereShape3D_oons1")
[node name="SniffRange" type="Area3D" parent="."]
collision_layer = 0
collision_mask = 8
[node name="CollisionShape3D" type="CollisionShape3D" parent="SniffRange"]
shape = SubResource("SphereShape3D_wffas")
[node name="StalkingTimer" type="Timer" parent="."]
unique_name_in_owner = true
wait_time = 25.0
one_shot = true
[node name="GrunkBeastBehavior" type="Node" parent="." node_paths=PackedStringArray("blackboard", "actor")]
script = ExtResource("6_d4ex2")
blackboard = NodePath("../Blackboard")
actor = NodePath("..")
metadata/_custom_type_script = "uid://bb0t2ovl7wifo"
[node name="StateSelector" type="Node" parent="GrunkBeastBehavior"]
script = ExtResource("7_vvw1q")
metadata/_custom_type_script = "uid://cw22yurt5l74k"
[node name="GrabSequence" type="Node" parent="GrunkBeastBehavior/StateSelector"]
script = ExtResource("8_0gxpq")
metadata/_custom_type_script = "uid://cg016dbe7gs1x"
[node name="IsTargetInGrabbingRange" type="Node" parent="GrunkBeastBehavior/StateSelector/GrabSequence" node_paths=PackedStringArray("area")]
script = ExtResource("9_xuag8")
blackboard_key = "pursuit_target"
area = NodePath("../../../../GrabbingRange")
metadata/_custom_type_script = "uid://7k5hor1omsxc"
[node name="GrabCooldown" type="Node" parent="GrunkBeastBehavior/StateSelector/GrabSequence"]
script = ExtResource("10_ntlom")
wait_time = 2.0
metadata/_custom_type_script = "uid://2qri6rrfv8ui"
[node name="GrabTarget" type="Node" parent="GrunkBeastBehavior/StateSelector/GrabSequence/GrabCooldown"]
script = ExtResource("11_nq7ke")
blackboard_key = "pursuit_target"
metadata/_custom_type_script = "uid://b0xue7ao0gjqo"
[node name="PursuitSequence" type="Node" parent="GrunkBeastBehavior/StateSelector"]
script = ExtResource("12_xde72")
metadata/_custom_type_script = "uid://dcojdhvj8qcw0"
[node name="TargetSelector" type="Node" parent="GrunkBeastBehavior/StateSelector/PursuitSequence"]
script = ExtResource("12_dkcdj")
metadata/_custom_type_script = "uid://8hn4kne15ac5"
[node name="TargetInPursuitRange" type="Node" parent="GrunkBeastBehavior/StateSelector/PursuitSequence/TargetSelector" node_paths=PackedStringArray("area")]
script = ExtResource("9_xuag8")
blackboard_key = "pursuit_target"
area = NodePath("../../../../../PursuitRange")
metadata/_custom_type_script = "uid://7k5hor1omsxc"
[node name="GetTargetFromAggroRange" type="Node" parent="GrunkBeastBehavior/StateSelector/PursuitSequence/TargetSelector" node_paths=PackedStringArray("area")]
script = ExtResource("13_x8l6r")
blackboard_key = "pursuit_target"
area = NodePath("../../../../../AggroRange")
metadata/_custom_type_script = "uid://b34l3v4sr8rmq"
[node name="UpdateStalkingTarget" type="Node" parent="GrunkBeastBehavior/StateSelector/PursuitSequence"]
script = ExtResource("15_umoec")
source_key = "pursuit_target"
destination_key = "stalking_target"
metadata/_custom_type_script = "uid://wfd5nbethyw6"
[node name="RestartStalkingTimer" type="Node" parent="GrunkBeastBehavior/StateSelector/PursuitSequence" node_paths=PackedStringArray("timer")]
script = ExtResource("16_asd50")
timer = NodePath("../../../../StalkingTimer")
metadata/_custom_type_script = "uid://ykfqlqp7e8og"
[node name="PursueTarget" type="Node" parent="GrunkBeastBehavior/StateSelector/PursuitSequence"]
script = ExtResource("14_x8l6r")
blackboard_key = "pursuit_target"
metadata/_custom_type_script = "uid://bkdwuqv4tudka"
[node name="AlwaysFailDecorator" type="Node" parent="GrunkBeastBehavior/StateSelector"]
script = ExtResource("15_oons1")
metadata/_custom_type_script = "uid://dwfdg523bk776"
[node name="ErasePursuitTarget" type="Node" parent="GrunkBeastBehavior/StateSelector/AlwaysFailDecorator"]
script = ExtResource("15_4b27i")
key = "pursuit_target"
metadata/_custom_type_script = "uid://demv7xh27ouvr"
[node name="StalkingSequence" type="Node" parent="GrunkBeastBehavior/StateSelector"]
script = ExtResource("12_xde72")
metadata/_custom_type_script = "uid://dcojdhvj8qcw0"
[node name="StalkingTimerRunning" type="Node" parent="GrunkBeastBehavior/StateSelector/StalkingSequence" node_paths=PackedStringArray("timer")]
script = ExtResource("16_oons1")
timer = NodePath("../../../../StalkingTimer")
metadata/_custom_type_script = "uid://h0cp58nswpml"
[node name="SetNavTarget" type="Node" parent="GrunkBeastBehavior/StateSelector/StalkingSequence"]
script = ExtResource("10_kjykp")
blackboard_key = "stalking_target"
metadata/_custom_type_script = "uid://u1ntpwjwjqhj"
[node name="TravelToDestination" type="Node" parent="GrunkBeastBehavior/StateSelector/StalkingSequence"]
script = ExtResource("14_4y64f")
metadata/_custom_type_script = "uid://om57w2acvgb7"
[node name="SelectorReactiveComposite" type="Node" parent="GrunkBeastBehavior/StateSelector/StalkingSequence"]
script = ExtResource("7_vvw1q")
metadata/_custom_type_script = "uid://cw22yurt5l74k"
[node name="AlwaysFailDecorator2" type="Node" parent="GrunkBeastBehavior/StateSelector"]
script = ExtResource("15_oons1")
metadata/_custom_type_script = "uid://dwfdg523bk776"
[node name="EraseStalkingTarget" type="Node" parent="GrunkBeastBehavior/StateSelector/AlwaysFailDecorator2"]
script = ExtResource("15_4b27i")
key = "stalking_target"
metadata/_custom_type_script = "uid://demv7xh27ouvr"
[node name="LurkSequence" type="Node" parent="GrunkBeastBehavior/StateSelector"]
script = ExtResource("8_0gxpq")
metadata/_custom_type_script = "uid://cg016dbe7gs1x"
[node name="RandomDelay" type="Node" parent="GrunkBeastBehavior/StateSelector/LurkSequence"]
script = ExtResource("11_mbqcc")
mean_time = 5.0
st_dev_time = 1.0
wait_time = 4.6164
metadata/_custom_type_script = "uid://beyk2xtbjrsg4"
[node name="PickRandomLurkTarget" type="Node" parent="GrunkBeastBehavior/StateSelector/LurkSequence/RandomDelay"]
script = ExtResource("12_ml8dd")
blackboard_key = "lurk_target"
group = "LurkPoint"
metadata/_custom_type_script = "uid://cg55nu4y0a5ud"
[node name="SetNavTarget" type="Node" parent="GrunkBeastBehavior/StateSelector/LurkSequence"]
script = ExtResource("10_kjykp")
blackboard_key = "lurk_target"
metadata/_custom_type_script = "uid://u1ntpwjwjqhj"
[node name="TravelToDestination" type="Node" parent="GrunkBeastBehavior/StateSelector/LurkSequence"]
script = ExtResource("14_4y64f")
metadata/_custom_type_script = "uid://om57w2acvgb7"
[node name="Blackboard" type="Node" parent="."]
unique_name_in_owner = true
script = ExtResource("7_cn3ok")
metadata/_custom_type_script = "uid://dme5f24l0edsf"
[connection signal="sound_detected" from="GameSoundListener" to="." method="on_sound_detected"] [connection signal="sound_detected" from="GameSoundListener" to="." method="on_sound_detected"]
[connection signal="body_entered" from="GrabbingRange" to="." method="on_player_entered_grabbing_range"]
[connection signal="body_exited" from="PursuitLimit" to="." method="on_player_left_pursuit_range"]