–1 vote

TLDR; Are there subsecond situations where things go "wrong" with area_exited/entered signals for 2D collision? i.e. running functions for the condition that would be true if the area was entered, but it's since been exited?

My game has a dialog system where you can press an assigned "check" button at any time during normal play to check the environment directly in front of the player's facing direction. A dialog box appears when you activate this ability, discussing whatever it determines you're "checking." Think Mother series, if you've played it. It should work like this:
https://imgur.com/gallery/EWOiWJx

The player has a small, capsule-shaped "checkbox" collision that rotates with their facing direction, and all checkable assets in the world have a rectangular "seenbox" collision roughly the size of their world collision shape (the collision that keeps them from traveling thru walls, etc.). See here, where the player is the fox thing facing right, the NPC is the man facing up, the green line is going around half of the checkbox, and the red line is going around half of the seenbox:
https://imgur.com/gallery/4yG0R9B

Once you press the "check" button, you get a dialog box at the bottom of the screen that either reads "No problem here" if there's no interference between the "checkbox" and any "seenbox" or reads whatever I have loaded into a JSON file for that specific asset. If that asset is an NPC, they are given a name and a secondary textbox appears to show this name. In the instance of the NPC in the image above, I have him named Tim and he says "Suh, dude?" An autoload script called DialogManager loads in the JSON file at bootup and assigns global variables called "checkName" (the name of the checked NPC, if the checked asset is an NPC) and "checkText" (the dialog to be read when an asset is checked) continuously. The game then determines which dialog ID to use based on these variables. Here's DialogManager's script:

extends Control

# Constants
export(String, FILE, "*.json") var dialog_file


# Variables
var dialog = []
var current_dialog_index = 0

onready var nextText = "Empty"
onready var checkText = "No problem here."
onready var checkType = "Item"
onready var checkName = "Error"


# Functions
func _ready():
    play()


func play():
    dialog = load_dialog()


func load_dialog():
    var file = File.new()
    if file.file_exists(dialog_file):
        file.open(dialog_file, file.READ)
        return parse_json(file.get_as_text())


func run_dialog():
    checkText = dialog[current_dialog_index]['Text']
    checkType = dialog[current_dialog_index]['Type']
    if checkType == "NPC":
        checkName = dialog[current_dialog_index]['Name']

There's also a textbox script and a world UI script that manage the rest like displaying things correctly. Just a warning that those are some very long scripts though as there are a lot of conditionals.

Textbox:

# Extensions
extends Control

# Variables
var state = IDLE
var pause_char = "="
var break_char = "|"
var split_text = []
var next_split_text = ""
var find_delimiter = 0
var remote_next_text = ""
var last_text = ""
var next_percent_visible = 0.0
var next_split = []
var pause_split = []

export (bool) var text = true
export var text_lines = 1
export var size_x = 30
export var size_y = 30
export var read_speed = 0.025

onready var textOnScreen = false
onready var pauseDelay = false
onready var bubble = $Bubble
onready var nextLine = $NextLine
onready var label = $Label
onready var tween = $Tween
onready var startTimer = $StartTimer
onready var pauseTimer = $PauseTimer


# Enumerations
enum {
    IDLE,
    READING,
    FINISHED,
}


# Functions
func _ready():
    if text == true:
        label.show()
        size_y = 15 * text_lines + 15
        label.rect_size.x = size_x - 20
        label.max_lines_visible = text_lines
    else:
        label.hide()

    bubble.rect_size.x = size_x
    bubble.rect_size.y = size_y
    nextLine.rect_size.x = size_x
    nextLine.rect_size.y = size_y

    if size_x < bubble.rect_min_size.x:
        size_x = bubble.rect_min_size.x
    if size_y < bubble.rect_min_size.y:
        size_y = bubble.rect_min_size.y

    nextLine.hide()


func _process(delta):
    match state:
        IDLE:                                                      
            idle_state(delta)
        READING:
            reading_state(delta)
        FINISHED:
            finished_state(delta)

    if visible:
        bubble.rect_size.x = size_x
        bubble.rect_size.y = size_y
        nextLine.rect_size.x = size_x
        nextLine.rect_size.y = size_y

        if size_x < bubble.rect_min_size.x:
            size_x = bubble.rect_min_size.x
        if size_y < bubble.rect_min_size.y:
            size_y = bubble.rect_min_size.y


func idle_state(delta):
    nextLine.hide()
    label.percent_visible = 0.0
    if textOnScreen == true:
        if pauseDelay == false:
            if Input.is_action_just_pressed("ui_accept") or Input.is_action_just_pressed("check"):
                startTimer.start()


func reading_state(delta):
    if textOnScreen == true:
        if pauseDelay == false:
            if Input.is_action_just_pressed("ui_accept") or Input.is_action_just_pressed("check"):
                label.percent_visible = 1.0
                tween.seek(100)
                if pauseDelay == false:
                    if Input.is_action_just_pressed("ui_accept") or Input.is_action_just_pressed("check"):
                        _on_Tween_all_completed()


func finished_state(delta):
    label.percent_visible = 1.0
    pauseDelay = false
    if textOnScreen == true:
        if pauseDelay == false:
            if Input.is_action_just_pressed("ui_accept") or Input.is_action_just_pressed("check"):
                state = IDLE


func set_current_frame(value):
    nextLine.texture.current_frame = value


func _on_StartTimer_timeout():
    next_percent_visible = 0.0
    display_text(DialogManager.nextText)


func display_text(next_text):
    nextLine.hide()
    remote_next_text = next_text
    if break_char in next_text or pause_char in next_text:
        delimit_text(next_text)
    else:
        label.percent_visible = next_percent_visible
        label.text = next_text
        tween.interpolate_property(label, "percent_visible",
            next_percent_visible, 1.0, len(next_text) * read_speed,
            Tween.TRANS_LINEAR)
        tween.start()


func delimit_text(next_text):
    if break_char in next_text and pause_char in next_text:
        find_delimiter = next_text.find(break_char) - next_text.find(pause_char)
        if find_delimiter <= 1:
            split_text = next_text.split(break_char, true, 1)
        elif find_delimiter > 1:
            split_text = next_text.split(pause_char, true, 1)
    elif break_char in next_text and not pause_char in next_text:
        split_text = next_text.split(break_char, true, 1)
        find_delimiter = 1
    elif not break_char in next_text and pause_char in next_text:
        split_text = next_text.split(pause_char, true, 1)
        find_delimiter = 2

    next_text = split_text[0]
    next_split_text = split_text[1]

    display_text(next_text)


func _on_Tween_started(object, key):
    state = READING


func _on_Tween_all_completed():
    if find_delimiter != 0:
        if find_delimiter <= 1:
            next_percent_visible = 0.0
            set_current_frame(0)
            nextLine.show()
            if label.percent_visible == 1.0 and tween.is_active() == false:
                if pauseDelay == false:
                    if Input.is_action_just_pressed("ui_accept") or Input.is_action_just_pressed("check"):
                        return_to_display()
        else:
            pauseTimer.start()
            pauseDelay = true

    elif find_delimiter == 0:
        state = FINISHED
        set_current_frame(0)
        nextLine.show()
        split_text = []
        next_split_text = ""
        find_delimiter = 0


func _on_PauseTimer_timeout():
    pauseDelay = false
    last_text = remote_next_text
    next_split_text = str(remote_next_text,next_split_text)

    if break_char in next_split_text or pause_char in next_split_text:
        if break_char in next_split_text and pause_char in next_split_text:
            find_delimiter = next_split_text.find(break_char) - next_split_text.find(pause_char)
            if find_delimiter <= 1:
                pause_split = next_split_text.split(break_char, true, 1)
            elif find_delimiter > 1:
                pause_split = next_split_text.split(pause_char, true, 1)
        elif break_char in next_split_text and not pause_char in next_split_text:
            pause_split = next_split_text.split(break_char, true, 1)
        elif not break_char in next_split_text and pause_char in next_split_text:
            pause_split = next_split_text.split(pause_char, true, 1)

        next_percent_visible = 0.05 + float(len(last_text)) / float(len(pause_split[0]))
    else:
        next_percent_visible = 0.05 + float(len(last_text)) / float(len(next_split_text))

    return_to_display()


func return_to_display():
    if not break_char in next_split_text and not pause_char in next_split_text:
        find_delimiter = 0

    display_text(next_split_text)

Part 1 of 2...see comments for part 2:

Godot version 3.5.stable
in Engine by (37 points)

Part 2/2

WorldUI possibly relevant snippets:

# Extensions
extends CanvasLayer

# Variables
var state = UNPAUSED

onready var textBoxes = $TextBoxes
onready var dialogBox = $TextBoxes/DialogBox
onready var nameBox = $TextBoxes/NameBox
onready var dialogBoxLabel = $TextBoxes/DialogBox/Label
onready var nameBoxLabel = $TextBoxes/NameBox/Label
onready var tweenDialogBoxIn = $Tweens/TweenDialogBoxIn
onready var tweenDialogBoxOut = $Tweens/TweenDialogBoxOut
onready var tweenNameBoxIn = $Tweens/TweenNameBoxIn
onready var tweenNameBoxOut = $Tweens/TweenNameBoxOut

# Enumerations
enum {
    UNPAUSED,
    PAUSEBAR,
    INVENTORY,
    PARTY,
    SETTINGS,
    DIALOG,
    CUTSCENE
}


# Functions
func _ready():
    textBoxes.show()
    dialogBox.hide()
    nameBox.hide()


func _process(delta):
    match state:
        UNPAUSED:                                                      
            unpaused_state(delta)
        PAUSEBAR:
            pausebar_state(delta)
        INVENTORY:
            inventory_state(delta)
        PARTY:
            party_state(delta)
        SETTINGS:
            settings_state(delta)
        DIALOG:
            dialog_state(delta)
        CUTSCENE:
            cutscene_state(delta)


func unpaused_state(delta):
    get_tree().paused = false
    dialogBox.textOnScreen = false

    DialogManager.nextText = "Empty"

    dialogBox.state = 0

    if Input.is_action_just_pressed("check"):
        DialogManager.nextText = DialogManager.checkText
        dialogBox.startTimer.start()

        tweenDialogBoxIn.interpolate_property(dialogBox, "rect_position",
            Vector2(0, 180), Vector2(0, 135),
            Tween.TRANS_SINE)
        tweenDialogBoxIn.start()

        tweenTopPanel.interpolate_property(topPanel, "rect_position",
            Vector2(0, -24), Vector2(0, 0),
            Tween.TRANS_SINE)
        tweenTopPanel.start()

        if DialogManager.checkType == "NPC":
            tweenNameBoxIn.interpolate_property(nameBox, "rect_position",
                Vector2(-100, 112), Vector2(0, 112),
                Tween.TRANS_SINE)
            tweenNameBoxIn.start()

        state = DIALOG


func dialog_state(delta):
    get_tree().paused = true

    dialogBox.textOnScreen = true

    dialogBox.show()
    topPanel.show()

    if DialogManager.checkType == "NPC":
        nameBox.show()
        nameBoxLabel.text = DialogManager.checkName
        var get_font = nameBoxLabel.get_font("res://UI/Fonts/apple_kid.ttf")
        var get_string_size = get_font.get_string_size(nameBoxLabel.text)
        nameBoxLabel.rect_size.x = get_string_size.x + 21
        nameBox.size_x = nameBoxLabel.rect_size.x
        nameBox.state = 2
        nameBox.nextLine.hide()
    elif DialogManager.checkType == "Item":
        nameBox.hide()

    if dialogBox.state == 2:
        if Input.is_action_just_pressed("check") or Input.is_action_just_pressed("ui_accept"):
            tweenDialogBoxOut.interpolate_property(dialogBox, "rect_position",
                Vector2(0, 135), Vector2(0, 180),
                Tween.TRANS_SINE)
            tweenDialogBoxOut.start()

            tweenTopPanel.interpolate_property(topPanel, "rect_position",
                Vector2(0, 0), Vector2(0, -24),
                Tween.TRANS_SINE)
            tweenTopPanel.start()

            if DialogManager.checkType == "NPC":
                tweenNameBoxOut.interpolate_property(nameBox, "rect_position",
                    Vector2(0, 112), Vector2(-100, 112),
                    Tween.TRANS_SINE)
                tweenNameBoxOut.start()

            state = UNPAUSED


func _on_TweenDialogBoxOut_tween_all_completed():
    dialogBox.hide()


func _on_TweenNameBoxOut_tween_all_completed():
    nameBox.hide()

I just randomly came across a repeatable (but not totally easy to repeat) glitch in my dialog system where if I press the "check" button just as (we're talking milliseconds after) the player slides against and then off of the NPC's collision and the checkbox exits the seenbox, the game will still consider the current dialog text to be whatever the asset would normally say, but removes the nameBox if it was an NPC. This can be seen in that same image:
https://imgur.com/gallery/4yG0R9B

Here's the video of it happening. Notice that the second time I speak to the NPC the name is there, but it wasn't there the first time. Ignore that the name is Timmmmmmmmmm lol, I was testing something else at the time of recording and was just lucky to catch this.
https://imgur.com/gallery/Vk1VTZ2

Thoughts? Anything unclear? I know that was a lot but I see too many posts that aren't thorough enough lol.

Please log in or register to answer this question.

Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read Frequently asked questions and How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to [email protected] with your username.