Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ builds/

# Blender
*.blend1
/examples/behaviors3d/addons
132 changes: 85 additions & 47 deletions examples/behaviors3d/block_scene.gd
Original file line number Diff line number Diff line change
@@ -1,69 +1,107 @@
extends Level

const AABB_MARGIN := Vector3(1.0, 1.0, 1.0)
const AABB_MARGIN := 0.5
var aabbs : Array[AABB] = []

@export var object_pool : Array[PackedScene]

@onready var ground = $Ground

@onready var character_location: Vector3
@onready var target_location: Vector3

var _free_character_locations: Array[Vector3]
var _free_character_location_idx: int
var _free_target_locations: Array[Vector3]
var _free_target_location_idx: int

func generate_level() -> void:
_adjust_ground()
_spawn_objects()
_adjust_ground()
_spawn_objects()


## Returns a free position for the character
func get_spawn_location() -> Vector3:
return Vector3(
randf_range(-ground.size.x/2, ground.size.x/2),
5,
randf_range(-ground.size.z/2, ground.size.z/2)
)
var location := _free_character_locations[_free_character_location_idx]
_free_character_location_idx = (_free_character_location_idx + 1) % _free_character_locations.size()
return location


## Returns a free position for the target
func get_target_location() -> Vector3:
return Vector3(
randf_range(-ground.size.x/2, ground.size.x/2),
1,
randf_range(-ground.size.z/2, ground.size.z/2)
)
var location := _free_target_locations[_free_target_location_idx]
_free_target_location_idx = (_free_target_location_idx + 1) % _free_target_locations.size()
return location

func get_random_location(y_offset := 1.0, xz_padding := 0.0) -> Vector3:
return Vector3(
randf_range(-ground.size.x/2 + xz_padding, ground.size.x/2 - xz_padding),
y_offset,
randf_range(-ground.size.z/2 + xz_padding, ground.size.z/2 - xz_padding)
)

func _intersects_aabb(aabb:AABB) -> bool:
for other_aabb in aabbs:
if aabb.intersects(other_aabb):
return true
return false
for other_aabb in aabbs:
if aabb.intersects(other_aabb):
return true
return false


func _adjust_ground() -> void:
ground.size = Vector3(randf_range(10, 40), 1, randf_range(10, 40))
ground.size = Vector3(randf_range(10, 40), 1, randf_range(10, 40))

get_node("Walls/Left").position = Vector3(-ground.size.x/2, 0, 0)
get_node("Walls/Left").size.x = ground.size.z
get_node("Walls/Right").position = Vector3(ground.size.x/2, 0, 0)
get_node("Walls/Right").size.x = ground.size.z
get_node("Walls/Forward").position = Vector3(0, 0, -ground.size.z/2)
get_node("Walls/Forward").size.x = ground.size.x
get_node("Walls/Backward").position = Vector3(0, 0, ground.size.z/2)
get_node("Walls/Backward").size.x = ground.size.x
get_node("Walls/Left").position = Vector3(-ground.size.x/2, 0, 0)
get_node("Walls/Left").size.x = ground.size.z
get_node("Walls/Right").position = Vector3(ground.size.x/2, 0, 0)
get_node("Walls/Right").size.x = ground.size.z
get_node("Walls/Forward").position = Vector3(0, 0, -ground.size.z/2)
get_node("Walls/Forward").size.x = ground.size.x
get_node("Walls/Backward").position = Vector3(0, 0, ground.size.z/2)
get_node("Walls/Backward").size.x = ground.size.x

func _spawn_objects() -> void:
var total_area = ground.size.x * ground.size.z
var max_objects = int(total_area)
for i in range(max_objects):
var object = object_pool.pick_random().instantiate()
add_child(object)
# object.rotate_y(deg_to_rad(randf_range(0, 360)))
object.position = Vector3(
randf_range(-ground.size.x/2 + object.size.x/2, ground.size.x/2 - object.size.x/2),
5,
randf_range(-ground.size.z/2 + object.size.z/2, ground.size.z/2 - object.size.z/2)
)
# TODO: this is not perfect, but it's good enough for now
var object_aabb = AABB(Vector3.ZERO, object.size + AABB_MARGIN)
var rotated_aabb = object.transform * object_aabb
rotated_aabb.position = object.position
if _intersects_aabb(rotated_aabb):
object.queue_free()
continue


aabbs.append(rotated_aabb)
var total_area = ground.size.x * ground.size.z
var max_objects = int(total_area)

#region Sets a few free positions for character and target to spawn
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to ensure there's always some free space between character/target and the boxes. While it limits the amount of random positions, this is one way to fix the player or target sometimes getting spawned inside of a box, while not requiring the entire level to regenerate on reset. Sometimes the character got stuck at the edges (at least with my other modifications, not sure about the original), so a padding parameter was added to the random position method.

image

var free_locations_count: int = 10
for i in free_locations_count:
character_location = get_random_location(ground.size.y/2.0 + _character_aabb.size.y / 2.0, 1)
target_location = get_random_location(ground.size.y/2.0 + _target_aabb.size.y / 2.0, 1)

var character_aabb_global: AABB = _character_aabb
character_aabb_global.position += to_global(character_location)
character_aabb_global = character_aabb_global.grow(AABB_MARGIN)

var target_aabb_global: AABB = _character_aabb
target_aabb_global.position += to_global(target_location)
target_aabb_global = target_aabb_global.grow(AABB_MARGIN)

aabbs.append(character_aabb_global)
aabbs.append(target_aabb_global)

_free_character_locations.append(character_location)
_free_target_locations.append(target_location)
#endregion

for i in range(max_objects):
var object = object_pool.pick_random().instantiate()
add_child(object)
# object.rotate_y(deg_to_rad(randf_range(0, 360)))
object.position = Vector3(
randf_range(-ground.size.x/2 + object.mesh.size.x/2, ground.size.x/2 - object.mesh.size.x/2),
ground.size.y/2 + object.mesh.size.y / 2.0,
#5,
randf_range(-ground.size.z/2 + object.mesh.size.z/2, ground.size.z/2 - object.mesh.size.z/2)
)
# TODO: this is not perfect, but it's good enough for now
var object_global_aabb: AABB = object.aabb
object_global_aabb.position += object.global_position
object_global_aabb = object_global_aabb.grow(AABB_MARGIN)

if _intersects_aabb(object_global_aabb):
object.queue_free()
continue


aabbs.append(object_global_aabb)
135 changes: 78 additions & 57 deletions examples/behaviors3d/character.gd
Original file line number Diff line number Diff line change
@@ -1,94 +1,115 @@
extends CharacterBody3D
class_name AICharacter

## A reference to the main mesh, used for AABB overlap checks
@export var aabb_mesh: Mesh
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to the character and target, used to leave some free spawn positions and position the character/target.
For now it's just a copy paste of the main mesh in the editor, this creates a reference to the same resource, but also allows using a different AABB mesh if needed.

image


@export var target: Node3D
const SPEED = 5.0
const JUMP_VELOCITY = 4.5

@onready var ai_controller = $AIController3D
@onready var aabb := aabb_mesh.get_aabb()
@onready var level_runner: LevelRunner = get_parent()

var forward_backward_action: float = 0.0
var straf_left_right_action: float = 0.0
var turn_left_right_action: float = 0.0
var jump_action: bool = false


func _ready() -> void:
ai_controller.init(self)
CameraManager.register_player(self)
ai_controller.init(self)
CameraManager.register_player(self)


func _physics_process(delta: float) -> void:
# Add the gravity.
var drag = 1.0
if not is_on_floor():
velocity += get_gravity() * delta
drag = 0.8

# Handle jump.


if get_jump() and is_on_floor():
velocity.y = JUMP_VELOCITY

# Get the input direction and handle the movement/deceleration.
# As good practice, you should replace UI actions with custom gameplay actions.
var direction := get_input_dir()
if direction:
velocity.x = direction.x * SPEED * drag
velocity.z = direction.z * SPEED * drag
else:
velocity.x = move_toward(velocity.x, 0, SPEED)
velocity.z = move_toward(velocity.z, 0, SPEED)

# Player turning logic
var turn = get_turn_dir()
if turn:
var target_rotation = rotation.y + turn
rotation.y = move_toward(rotation.y, target_rotation, 5.0 * delta)

move_and_slide()

# Add the gravity.
var drag = 1.0
if not is_on_floor():
velocity += get_gravity() * delta
drag = 0.8

# Handle jump.
if get_jump() and is_on_floor():
velocity.y = JUMP_VELOCITY

# Get the input direction and handle the movement/deceleration.
# As good practice, you should replace UI actions with custom gameplay actions.
var direction := get_input_dir()
if direction:
velocity.x = direction.x * SPEED * drag
velocity.z = direction.z * SPEED * drag
else:
velocity.x = move_toward(velocity.x, 0, SPEED)
velocity.z = move_toward(velocity.z, 0, SPEED)

# Player turning logic
var turn = get_turn_dir()
if turn:
var target_rotation = rotation.y + turn
rotation.y = move_toward(rotation.y, target_rotation, 5.0 * delta)

move_and_slide()
process_debug_reset()
process_debug_character_fell()


## [Debug method] Used to test different spawn positions
func process_debug_reset():
if Input.is_action_just_pressed("reset"):
reset()


## [Debug method] Prints out if the character falls
func process_debug_character_fell():
if position.y < -10:
print("[Debug] character fell, scene: ", level_runner.level_scene.name)


func get_jump():
if ai_controller.heuristic == "model":
return jump_action
else:
return Input.is_action_just_pressed("ui_accept")
if ai_controller.heuristic == "model":
return jump_action
else:
return Input.is_action_just_pressed("ui_accept")


func get_input_dir() -> Vector3:
var input_dir = Vector2()
if ai_controller.heuristic == "model":
input_dir = Vector2(straf_left_right_action, forward_backward_action)
else:
input_dir = Input.get_vector("right", "left", "backward", "forward")
var input_dir = Vector2()
if ai_controller.heuristic == "model":
input_dir = Vector2(straf_left_right_action, forward_backward_action)
else:
input_dir = Input.get_vector("right", "left", "backward", "forward")



var dir = Vector3(input_dir.x, 0, input_dir.y).normalized()
dir.z = clamp(dir.z, -0.5, 1.0)
return transform.basis * dir
var dir = Vector3(input_dir.x, 0, input_dir.y).normalized()
dir.z = clamp(dir.z, -0.5, 1.0)
return transform.basis * dir

func get_turn_dir() -> float:
if ai_controller.heuristic == "model":
return turn_left_right_action
if ai_controller.heuristic == "model":
return turn_left_right_action

return Input.get_action_strength("turn_left") - Input.get_action_strength("turn_right")
return Input.get_action_strength("turn_left") - Input.get_action_strength("turn_right")



func _on_collector_area_entered(_area: Area3D) -> void:
ai_controller.reward += 10.0
ai_controller.reset_best_goal_distance()
ai_controller.reward += 10.0
ai_controller.reset_best_goal_distance()

func reset():
position = Vector3(0, 1, 0)
velocity = Vector3()
rotation = Vector3()
get_parent().reset_target()
ai_controller.reset_best_goal_distance()
position = level_runner.level_scene.get_spawn_location()
velocity = Vector3()
rotation = Vector3()
level_runner.reset_target()
ai_controller.reset_best_goal_distance()


func toggle_camera(on: bool):
if on:
$Camera3D.make_current()
else:
$Camera3D.clear_current()
if on:
$Camera3D.make_current()
else:
$Camera3D.clear_current()
31 changes: 29 additions & 2 deletions examples/behaviors3d/level.gd
Original file line number Diff line number Diff line change
@@ -1,8 +1,35 @@
extends Node3D
class_name Level


#region Currently set by LevelRunner
var character: AICharacter
var target: AITarget
#endregion


var _character_aabb: AABB
var _target_aabb: AABB


func _ready():
_set_aabbs()


## Sets the character and target aabb's
func _set_aabbs():
_character_aabb = character.aabb
_target_aabb = target.aabb


## Default implementation for spawn location
func get_spawn_location() -> Vector3:
var spawn_location: Vector3
spawn_location.y = 2
return spawn_location

func generate_level():
assert(false)
assert(false)

func get_target_location():
assert(false)
assert(false)
Loading