Hello! I have been trying to develop a simple enemy for my game. The goal for this enemy is to serve as a gulliotine/smashing hazard that tries to crush the player the second they pass underneath them.
I have attempted this by having a CharacterBody3D move toward the player rapidly as soon as the Area3D connected to it feels the player enter. Once it detects the ground, it pauses, before rising again, stopping as soon as it hits the ceiling. Unfortunetly, this method leads to multiple issues that relate to its Falling and Rising functions.
I wanted to know if there is a simple but effective method that can achieve what I want while staying not too complicated, as I am a beginner coder.
The way you described it seems completely legit and I don’t see why it shouldn’t work.
Can you share how you implemented it and what specifically doesn’t work with your solution?
I would maybe change the CharacterBody3D node type to AnimatableBody3D as it seems to be more suited for this purpose.
For some reason, randomly, when the guillotine makes contact with the ground, the guillotine will immediately bounce up and stop, failing to rise as it should. I have tested it many times and each time this error pops up is inconsistent–sometimes it works, sometimes it breaks. It might stem from how I coded it, but I cannot for the life of me pinpoint what is wrong with it.
My code looks like this. (These are two separate gdscript files that work for my state machine, the issue is probably not the State Machine itself)
extends Node
var fsm: StateMachine
@onready var CHAR := $"../.."
@onready var ABLE_RISE = false
func enter():
pass
func _process(delta: float) -> void:
#This code has the Guillotine move up until it hits the ceiling
if CHAR.is_on_floor():
ABLE_RISE = true
if ABLE_RISE == true:
await get_tree().create_timer(.8).timeout
CHAR.velocity.y = move_toward(0, 100, 20)
CHAR.move_and_slide()
if CHAR.is_on_ceiling():
ABLE_RISE = false
#This exits into the next State
func exit(next_state):
fsm.change_to(next_state)
#When the player enters the Trigger Area, the exit() function is called
func _on_trigger_area_area_entered(area: Area3D) -> void:
if area.is_in_group("PLAYER") and CHAR.velocity.y == 0:
exit("Gulli_Attack")
########NEXT STATE V V V
extends Node
var fsm: StateMachine
@onready var CHAR := $"../.."
#Upon the State being entered, it starts a timer inside the Guillotine.
func enter():
await get_tree().create_timer(.2).timeout
$"../../Timer2".start()
pass
func _process(delta: float) -> void:
pass
func exit():
# Go back to the last state
fsm.back()
#The Guillotine rapidly falls downward, and when it hits the ground, it stops, and exits this state to the idle one.
func ATTACK():
if CHAR.velocity.y >= 0:
CHAR.velocity.y = 0
$"../../HITBOX".monitorable = true
CHAR.velocity.y = move_toward(CHAR.position.y, -100, 400)
CHAR.move_and_slide()
if CHAR.is_on_floor() or CHAR.velocity.y == 0:
$"../../Timer2".stop()
$"../../HITBOX".monitorable = false
await get_tree().create_timer(.5).timeout
exit()
pass
func _unhandled_key_input(event):
pass
func _on_timer_timeout() -> void:
exit()
pass # Replace with function body.
#The ATTACK() func is called every time the looping timer ends.
func _on_timer_2_timeout() -> void:
ATTACK()
pass # Replace with function body.
Personally, I’d probably do this something like:
# Untested!
const PRE_ATTACK_TIME: float = 0.2
const FLOOR_REST_TIME: float = 0.5
const CEILING_REST_TIME: float = 0.8
enum State { waiting, pre_attack, attacking, resting, rising, ceiling_reset }
var state: State = State.waiting
var rest_time: float = 0.0
var target: bool = false
func _process(delta: float) -> void:
match(state):
State.waiting:
if target:
State = state.pre_attack
rest_time = PRE_ATTACK_TIME
State.pre_attack:
rest_time -= delta
if rest_time < 0.0:
state = State.attack
$"../../HITBOX".monitorable = true
CHAR.velocity.y = move_toward(CHAR.position.y, -100, 400)
State.attacking:
CHAR.move_and_slide()
if CHAR.is_on_floor() or is_zero_approx(CHAR.velocity.y):
$"../../HITBOX".monitorable = false
rest_time = REST_TIME
state = State.resting
State.resting:
rest_time -= delta
if rest_time <= 0.0:
state = State.rising
CHAR.velocity.y = move_toward(0, 100, 20)
target = false
State.rising:
CHAR.move_and_slide()
if CHAR.is_on_ceiling():
rest_time = CEILING_REST_TIME
state = State.ceiling_reset
State.ceiling_reset:
rest_time -= delta
if rest_time < 0.0:
state = State.waiting
func _target_acquired(area: Area3D) -> void:
if area.is_in_group("PLAYER") && state == State.waiting:
target = true
That keeps all the logic in one place, more or less. The target bool is so we can detect the player entering the target area even if we’re not quite ready to hit them; this code will trigger an (eventual) attack if we’re waiting on the ceiling, waiting on the floor, or rising.
Edit: Code changes; I missed your pre-attack delay.
I was unable to implement this enum system into my code, as I think I am simply too inept right now. However, I took inspiration and took the logic into one area, which allowed for me to FINALLY get it right!! Thank you tons!
The code now looks like this:
extends CharacterBody3D
var ABLE_RISE = false
var ATTACK = false
func _process(delta: float) -> void:
if not position.z == 0:
position.z = 0
if is_on_ceiling():
ABLE_RISE = false
if is_on_floor():
ABLE_RISE = true
ATTACK = false
move_and_slide()
func _on_trigger_area_area_entered(area: Area3D) -> void:
if area.is_in_group("PLAYER") and ABLE_RISE == false:
ATTACK = true
$Timer2.start()
pass # Replace with function body.
func _on_timer_2_timeout() -> void:
if ABLE_RISE == true:
await get_tree().create_timer(.5).timeout
velocity.y = move_toward(0, 100, 30)
if ATTACK == true:
await get_tree().create_timer(.2).timeout
self.velocity.y = move_toward(0, -100, 100)
pass # Replace with function body.