I have finally got it working.
it might look a little strange but… thats kinda how I had to get it to work for an AI
to explain it simply the structure looks like this
(CharacterBody3D)<
|-(MovementController)-Its pretty much handles movement that modifies the |parent (works for both player and AI inputs)
|-(Controller:AI in this case, but can be player)
I also have a resource listed as AIInput (basically acts like a bus)
I know this is probably very overkill and would be solved with fundementally alot simpler solutions, but I was having fun messing around. It works and thiers that. putting the code below and be mindful its a work in progress
Code 1 (AIinput)
# Simulates movement input
var target_direction: Vector2 = Vector2.ZERO
var target_rotation: float = 0.0
var target_position: Vector3 = Vector3.ZERO
var is_walking: bool = true
var is_sprinting: bool = false
var is_jumping: bool = false
func get_movement_input() -> Vector2:
return target_direction
func get_rotation_input() -> float:
return target_rotation
func get_target() -> Vector3:
print(target_position)
return target_position
func get_walk_input() -> bool:
return is_walking
func get_sprint_input() -> bool:
return is_sprinting
func get_jump_input() -> bool:
return is_jumping
Code 2 (AIController) < simple state machine (very much prototyping)
class_name AIController extends Node
enum AIStates {Idle, Wander}
@export var movement_controller:MovementController
@export var idle_min_duration: float = 0.0
@export var idle_max_duration: float = 5.0
@export var wander_radius: float = 30.0
@export var destination_tolerance: float = 0.5
@onready var ai_input: AIInput = AIInput.new() #this needs to be here so each AIInput is unique and all the controllers don't spam!
var current_state: AIStates = AIStates.Idle
var state_timer: float = 2.0
func _ready() -> void:
print("AI ALIVE. Going Idle")
func _process(delta: float) -> void:
handle_behavior(delta)
func switch_state(new_state: AIStates) -> void:
current_state = new_state
state_timer = 0.0
#print("Switched to state:", AIStates.keys()[new_state])
if new_state == AIStates.Idle:
state_timer = randf_range(idle_min_duration, idle_max_duration)
elif new_state == AIStates.Wander:
var random_direction: Vector3 = Vector3(
randf_range(-wander_radius,wander_radius), #X
0, #Y
randf_range(-wander_radius,wander_radius) #Z
)
set_target(random_direction)
state_timer = randf_range(idle_min_duration, idle_max_duration)
func handle_behavior(delta: float) -> void:
state_timer -= delta
match current_state:
AIStates.Idle:
stop_moving()
if state_timer <= 0.0:
switch_state(AIStates.Wander)
AIStates.Wander:
move_forward()
if state_timer <= 0.0:
switch_state(AIStates.Idle)
func stop_moving() -> void:
ai_input.target_direction = Vector2(0,0)
func move_forward() -> void:
ai_input.target_direction = Vector2(0,1)
func set_target(target_position: Vector3) -> void:
ai_input.target_position = target_position
#print(ai_input.target_position)
func get_ai_input() -> AIInput:
return ai_input
Code 3(Movement controller)<had to make the rotation “unique”, couldnt figure out how to make it work like a player, so failed in that aspect
class_name MovementController extends Node
var base_speed:float
var walk_speed:float
var sprint_speed:float
var jump_height:float
var current_speed:float =0.0
enum movement_states {Neutral,Walking,Sprinting}
var current_movement_state:movement_states = movement_states.Neutral
func _ready() -> void:
var parent = get_parent()
if parent and parent is CharacterBody3D:
# Directly access the variables
base_speed = parent.base_speed
walk_speed = parent.walk_speed
sprint_speed = parent.sprint_speed
jump_height = parent.jump_height
func handle_movement_state()->void:
match current_movement_state:
movement_states.Neutral:current_speed=base_speed
movement_states.Walking:current_speed=walk_speed
movement_states.Sprinting:current_speed=sprint_speed
func set_movement_state(is_walking:bool,is_sprinting:bool)->void:
if is_walking:current_movement_state=movement_states.Walking
elif is_sprinting: current_movement_state=movement_states.Sprinting
else:current_movement_state=movement_states.Neutral
func handle_acceleration(entity:CharacterBody3D,target_direction:Vector2)->void:
var direction:Vector3 = (entity.transform.basis*Vector3(target_direction.x,0,target_direction.y))
direction.normalized()
if direction:
entity.velocity.x=direction.x*current_speed
entity.velocity.z=direction.z*current_speed
else:
entity.velocity.x=move_toward(entity.velocity.x,0,current_speed)
entity.velocity.z=move_toward(entity.velocity.z,0,current_speed)
func handle_rotation(entity:CharacterBody3D,target_direction:Vector3)->void:
var current_forward: Vector3 = -entity.transform.basis.z
target_direction = target_direction.normalized()
var angle:float = current_forward.angle_to(target_direction) #get the rotation angle (radians) between current forward and target
var cross:Vector3= current_forward.cross(target_direction) #determine the cross product to check rotation direction (clockwise/counterclockwise)
if cross.y <0:
angle = -angle #rotate clockwise if cross product points downward
entity.rotate_y(angle)
#print("Rotating entity to face:", target_direction) #test prints
func handle_jump(entity:CharacterBody3D,is_jumping:bool)->void:
if is_jumping:
entity.velocity.y=jump_height
#TEST ZONE
func handle_ai_rotation(entity:CharacterBody3D,target_position:Vector3, delta:float)-> void:
var direction_to_target = (target_position - entity.global_transform.origin).normalized()
if direction_to_target.is_equal_approx(Vector3.UP) or direction_to_target.is_equal_approx(-Vector3.UP):
direction_to_target += Vector3(0.01, 0, 0.01)
direction_to_target = direction_to_target.normalized()
var current_yaw = entity.rotation.y
var target_yaw = atan2(direction_to_target.x, direction_to_target.z)
if abs(target_yaw - current_yaw) < 0.01:
return
var rotation_speed = 5.0
entity.rotation.y = lerp_angle(current_yaw, target_yaw, delta * rotation_speed)
func handle_target_rotation(entity: CharacterBody3D, target_position: Vector3) -> Vector3:
var direction_to_target = (target_position - entity.global_transform.origin).normalized()
return direction_to_target
Code 4 (CharacterBody3D)<where the magic happens
extends CharacterBody3D
#artifical intelligence
@onready var AIinput:AIInput = AIInput.new()
@export var ai_controller:AIController=null
#movement
@onready var movement_input_controller:MovementInputController = MovementInputController.new()
@export var movement_controller:MovementController= MovementController.new()
#stats
@export var base_speed:float = 3.0
@export var walk_speed:float = 3.0
@export var sprint_speed:float = 10.0
@export var jump_height:float = 8.0
func _ready() -> void:
print(AIinput)
if ai_controller:
AIinput = ai_controller.get_ai_input() #order might be wrong so might need changing
#print(self, " -> ", movement_input_controller)
#print(self, " -> ", movement_controller)
func _physics_process(delta: float) -> void:
handle_gravity(delta)
movement_controller.handle_movement_state()
movement_controller.handle_acceleration(self, AIinput.get_movement_input())
movement_controller.handle_ai_rotation(self,AIinput.target_position,delta)
movement_controller.handle_jump(self,AIinput.get_jump_input())
move_and_slide()
#loving the ground
func handle_gravity(delta:float)->void:
if not is_on_floor():
velocity += get_gravity()*delta
Overall its pretty simple and took alot of trouble shooting to get it to this point.
I dont know why I struggled with handling the AI rotation so much. took me almost 2 full weekends (full time college student so only like 4 hours after I finished schoolwork). I am curious about everyones thoughts on it since I have it working!
No errors, debugs or weird stuff (did have a weird problem where it was spinning in circles at one point but fixed that pretty quickly).