0
\$\begingroup\$

I have a player character with a knockback ability which has a certain number of charges. When the charges are depleted, the player must pass through an Area3D to replenish them. I want the Area3D's CollisionShape3D the player passes through to deactivate and hide, then be reactivated and unhidden when the player collides with a different CollisionShape3D of the same Area3D, which will deactivate and hide in turn.

My code below includes an array of four CollisionShape3Ds. When entering CollisionShape3D 1 and CollisionShape3D 2, the code seems to operate as intended; they deactivate and hide. However, CollisionShape3D 3 and CollisionShape3D 4 do not hide when the player enters, instead, their expected logic applies to CollisionShape3D 1 and CollisionShape3D 2 again, as can be seen in this external video; I'm using OmniLight3Ds to light up the knockback charge indicators.

(In the video, I use the knockback ability twice, which turns off the lights and then passing through the CollisionShape3Ds unhides them: I wanted to include that since it may seem like the lights fail to update in the second half of the video, whereas I just stopped using the knockback and showed passing through the shapes.)

So, CollisionShape3D 1 and CollisionShape3D 2 apply their function logic to themselves to deactivate and hide, while also replenishing and updating the OmniLight3Ds visuals, whereas CollisionShape3D 3 and CollisionShape3D 4 also replenish and update visuals correctly but apply their logic to the first two CollisionShape3Ds. I don’t understand what’s happening here, because they are all included in the same array and are all children of the same Area3D node.

enter image description here

I created a new project in which I only included what’s on screen and the code for the player character, including default character movement to try to replicate the results with the fewest intrusions from other processes. Here is the code:

extends CharacterBody3D

const SPEED = 5.0
const JUMP_VELOCITY = 4.5
var gravity = -9.8

@export var health = 1


var max_knockback_uses: int = 5
var current_knockback_uses: int = 5
var knockback_indicators: Array[OmniLight3D] = []

var collision_shapes: Array[CollisionShape3D] = []
var active_collision_shape: CollisionShape3D = null

func _ready():
    print("Area3D is monitoring:", $"../Area3D".monitoring)
    print("Collision shapes array: ", collision_shapes)
    knockback_indicators = [
        $"Knockback_Indicator 1", $"Knockback_Indicator 2", $"Knockback_Indicator 3", $"Knockback_Indicator 4", $"Knockback_Indicator 5"
    ]
    print("Knockback indicators:", knockback_indicators)
    update_knockback_visuals()
    
    collision_shapes = [
        $"../Area3D/CollisionShape3D 1", 
        $"../Area3D/CollisionShape3D 2", 
        $"../Area3D/CollisionShape3D 3", 
        $"../Area3D/CollisionShape3D 4",
    ]
    

func _physics_process(delta):
    


    if not is_on_floor():
        velocity += get_gravity() * delta

    # Handle jump.
    if Input.is_action_just_pressed("ui_accept") 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 input_dir := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
    var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
    if direction:
        velocity.x = direction.x * SPEED
        velocity.z = direction.z * SPEED
    else:
        velocity.x = move_toward(velocity.x, 0, SPEED)
        velocity.z = move_toward(velocity.z, 0, SPEED)

    move_and_slide()    
        
    
var knockback_power = 100.0
var knockback_radius = 50.0

func knockback():
    
    var enemies = get_tree().get_nodes_in_group("enemies")
    
    # Iterate through each enemy
    for enemy in enemies:
        if enemy is CharacterBody3D:
            # Calculate the distance between the player and the enemy
            var direction = enemy.global_transform.origin - global_transform.origin
            var distance = direction.length()
            
            # Check if the enemy is within the knockback radius
            if distance <= knockback_radius:
                direction = direction.normalized() # Get the direction vector
                # Apply knockback force
                enemy.apply_knockback(direction * knockback_power)
        

func use_knockback():
    if current_knockback_uses > 0:
        knockback()
        current_knockback_uses -= 1
        print("Knockback used. Remaining:", current_knockback_uses)
        print("Calling update_knockback_visuals()...")
        update_knockback_visuals()
    else:
        print("No knockback uses left!")

func _input(event):
        if event.is_action_pressed("Knockback"):
            use_knockback()
            print("knockback pressed")
func replenish_knockback(amount: int = 1):
    print("Replenish knockback called")
    if current_knockback_uses < max_knockback_uses:
        current_knockback_uses = max_knockback_uses
        update_knockback_visuals()
        print("Knockback replenished. Current uses:", current_knockback_uses)


func update_knockback_visuals():
    print("Updating knockback visuals...")
    for i in range(max_knockback_uses):
        if knockback_indicators[i]:
            if i < current_knockback_uses:
                knockback_indicators[i].visible = true
                print("Indicator", i + 1, "visible")
            else:
                knockback_indicators[i].visible = false
                print("Indicator", i + 1, "hidden")
        else:
            print("Knockback indicator", i + 1, "is missing!")


func _on_area_3d_body_entered(body: Node3D) -> void:
    print("body entered")
    if body == self:  
        for shape in collision_shapes:
            if shape and not shape.disabled:  
                replenish_knockback()
                deactivate_collision_shape(shape)
                reactivate_other_collision_shapes(shape)
                break
        
func deactivate_collision_shape(shape: CollisionShape3D):
    if shape and not shape.disabled:
        shape.set_deferred("disabled", true)
        shape.visible = false
        print("Deactivating shape:", shape.name, "Disabled state after call:", shape.disabled)
        active_collision_shape = shape
        shape.set_deferred("disabled", true)
        await get_tree().process_frame
        print("Post-deferred disabled state:", shape.disabled)
    else: 
        print("Error: Shape is null in deactivate_collision_shape")

func reactivate_other_collision_shapes(excluded_shape: CollisionShape3D):
    # Reactivate all other shapes
    for shape in collision_shapes:
        if shape and shape != excluded_shape:
            shape.set_deferred("disabled", false)
            shape.visible = true
            print("Reactivated shape:", shape.name)

Thanks in advance for any help. This is my first project and this is the last segment of code I need to finish my game. It's also the most complicated, believe or not (it's a very small game). I even asked ChatGPT, but this is as far as I could get with it. I'm now at the point where I just don't understand the interactions happening here to point out conflicts. Again, thanks for any time you give me.

\$\endgroup\$
0

1 Answer 1

0
\$\begingroup\$

Your current code looks fine yet a little convoluted. That's okay, especially for a "first project". However, it makes it hard to pinpoint the exact line causing your issue.

For this reason, I'd like to suggest an alternate solution. Since your rule is "All shapes but one" (that is, after the first player interaction), instead of doing:

  1. Deactivate the current shape.
  2. Activate the previous inactive shape.

You could do equivalently:

  1. Activate all shapes.
  2. Deactivate the current shape.

First, we activate all collision shapes regardless of which is disabled; then, we deactivate one and only one shape. This strategy ensures the above rule yields while decreasing the number of checks and simplifying your code.

This solution can be implemented by using a different signal emitted by the Area3D in place of the old one and its two support functions:

func _on_area_3d_body_shape_entered(_body_rid: RID, body: Node3D, _body_shape_index: int, local_shape_index: int) -> void:
    # Return if not colliding with the player
    if not body == self: return
    # Activate all shapes indiscriminately
    for shape in collision_shapes:
        shape.set_deferred("disabled", false)
        shape.visible = true
    # Find the shape the player collided with...
    var area3d_shape_owner = shape_find_owner(local_shape_index) # Returns an ID used later
    var area3d_shape_node = shape_owner_get_owner(local_shape_owner) # Returns a CollisionShape3D
    # ...and deactivate it
    local_shape_node.set_deferred("disabled", true)
    local_shape_node.visible = false
\$\endgroup\$

You must log in to answer this question.