Godot Version
4.4.1, Jolt
Question
Hello Godoteers! I am refactoring my FPS enemy zombies from CharacterBody3Ds to RigidBody3Ds (to mainly solve performance issues with 30+ zombies calling move_and_slide and it tanking performance) and I would really like an example of an enemy that is based off RigidBody3D because I have no idea what I’m doing with these physics
I tried searching for RigidBody3D tutorials but all of them seem to be either for player controllers (not sure if this is applicable for my enemy “zombie” use case) or are more physics related tests like balls moving around or something not “make a 3D FPS enemy using RigidBody3D”.
Here is what I cooked up so far but the zombie doesn’t move at all right now and I think the state machine isn’t initializing but need to debug more.
zombie_RigidBody3D.gd
class_name ZombieRigidBody
extends RigidBody3D
## Simple RigidBody3D zombie with force-based movement and state machine.
## Designed for multiplayer-ready implementation with minimal complexity.
# Signals
signal zombie_killed
signal zombie_melee_attack(damage: int)
# State Machine
enum ZombieState { IDLE, PURSUE, ATTACK, DEAD }
var current_state = ZombieState.IDLE
@export_group("Movement")
## Movement force applied toward target in Newtons. [br]Higher values make zombies accelerate faster.
@export_range(50.0, 500.0, 10.0) var movement_force: float = 200.0
## Maximum speed in meters per second. [br]Prevents zombies from accelerating indefinitely.
@export_range(1.0, 10.0, 0.1) var max_speed: float = 3.0
## How quickly the zombie rotates to face target in radians per second.
@export_range(1.0, 10.0, 0.1) var rotation_speed: float = 5.0
@export_group("Combat")
## Maximum health before zombie dies.
@export_range(1, 100) var max_health: int = 10
## Damage dealt per attack.
@export_range(1, 50) var melee_damage: int = 5
## Distance in meters at which zombie can attack.
@export_range(1.0, 5.0, 0.1) var attack_range: float = 2.0
## Time in seconds between attacks.
@export_range(0.5, 5.0, 0.1) var attack_cooldown: float = 1.0
## Distance in meters to start pursuing target.
@export_range(5.0, 50.0, 1.0) var detection_range: float = 20.0
@export_group("Physics")
## Mass of the zombie in kilograms. [br]Heavier zombies are harder to knock back.
@export_range(10.0, 200.0, 5.0) var zombie_mass: float = 50.0
## Linear damping when moving normally. [br]Lower values = less air resistance.
@export_range(0.0, 5.0, 0.1) var movement_damp: float = 1.0
## Linear damping when attacking. [br]Higher values make zombie stop faster during attacks.
@export_range(5.0, 20.0, 0.5) var attack_damp: float = 10.0
# Internal variables
var health: int
var vehicle_target: Node3D = null
var attack_ready: bool = true
var knockback_active: bool = false
# Node references
@onready var attack_timer: Timer = $AttackTimer
@onready var mesh_instance: MeshInstance3D = $MeshInstance3D
@onready var collision_shape: CollisionShape3D = $CollisionShape3D
func _ready() -> void:
# Initialize health
health = max_health
# Set up physics properties
mass = zombie_mass
linear_damp = movement_damp
# Set physics properties
freeze_mode = RigidBody3D.FREEZE_MODE_STATIC
freeze = false
# Add to zombie group
add_to_group("Zombies")
# Connect attack timer if it exists
if attack_timer:
attack_timer.timeout.connect(_on_attack_timer_timeout)
attack_timer.wait_time = attack_cooldown
# Find vehicle target
_find_vehicle_target()
func _physics_process(delta: float) -> void:
# Skip if dead
if current_state == ZombieState.DEAD:
return
# Process current state
match current_state:
ZombieState.IDLE:
_process_idle_state()
ZombieState.PURSUE:
_process_pursue_state(delta)
ZombieState.ATTACK:
_process_attack_state()
func _process_idle_state() -> void:
# Check if target is in detection range
if vehicle_target and global_position.distance_to(vehicle_target.global_position) <= detection_range:
_change_state(ZombieState.PURSUE)
func _process_pursue_state(delta: float) -> void:
if not vehicle_target:
_change_state(ZombieState.IDLE)
return
var distance_to_target: float = global_position.distance_to(vehicle_target.global_position)
# Check if in attack range
if distance_to_target <= attack_range:
_change_state(ZombieState.ATTACK)
return
# Check if target left detection range
if distance_to_target > detection_range * 1.5: # Add hysteresis
_change_state(ZombieState.IDLE)
return
# Apply movement
if not knockback_active:
_apply_movement_force(delta)
func _process_attack_state() -> void:
if not vehicle_target:
_change_state(ZombieState.IDLE)
return
var distance_to_target: float = global_position.distance_to(vehicle_target.global_position)
# Check if target moved out of range
if distance_to_target > attack_range * 1.2: # Add hysteresis
_change_state(ZombieState.PURSUE)
return
# Perform attack if ready
if attack_ready:
_perform_attack()
func _apply_movement_force(delta: float) -> void:
# Calculate direction to target
var direction: Vector3 = (vehicle_target.global_position - global_position).normalized()
direction.y = 0 # Keep movement horizontal
# Apply force if below max speed
if linear_velocity.length() < max_speed:
apply_central_force(direction * movement_force)
# Rotate toward target
var target_transform: Transform3D = transform.looking_at(
global_position + direction,
Vector3.UP
)
transform = transform.interpolate_with(target_transform, rotation_speed * delta)
func _perform_attack() -> void:
# Emit attack signal
zombie_melee_attack.emit(melee_damage)
# Start cooldown
attack_ready = false
if attack_timer:
attack_timer.start()
func _on_attack_timer_timeout() -> void:
attack_ready = true
func _change_state(new_state) -> void:
# Exit current state
match current_state:
ZombieState.ATTACK:
linear_damp = movement_damp # Reset damping
# Enter new state
current_state = new_state
match new_state:
ZombieState.ATTACK:
linear_damp = attack_damp # Increase damping to stop
func _find_vehicle_target() -> void:
# Look for vehicle in the scene
var vehicles: Array = get_tree().get_nodes_in_group("Vehicle")
if vehicles.size() > 0:
vehicle_target = vehicles[0]
## Called when zombie is hit by a car
func car_hit(damage: int, knockback_direction: Vector3, knockback_force: float) -> void:
# Prevent multiple knockbacks
if knockback_active:
return
knockback_active = true
# Apply damage
take_damage(damage)
# Apply knockback impulse
var impulse: Vector3 = knockback_direction.normalized() * knockback_force
impulse.y = abs(knockback_force * 0.3) # Add upward component
apply_central_impulse(impulse)
# Reset knockback after delay
await get_tree().create_timer(0.5).timeout
knockback_active = false
## Apply damage to zombie
func take_damage(damage: int) -> void:
health -= damage
if health <= 0:
die()
## Handle zombie death
func die() -> void:
_change_state(ZombieState.DEAD)
# Emit death signal
zombie_killed.emit()
# Disable physics
freeze = true
collision_shape.disabled = true
# Simple death visualization (tint red and fall)
if mesh_instance and mesh_instance.material_override:
mesh_instance.material_override.albedo_color = Color.RED
# Clean up after delay
await get_tree().create_timer(3.0).timeout
queue_free()
## Set the vehicle target (useful for spawning systems)
func set_vehicle_target(target: Node3D) -> void:
vehicle_target = target
Any help is greatly appreciated! Cheers!