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: