Strip BBCode from an array and detect characters

Godot Version

v4.1.2.stable.official [399c9dc39]

Question

This is a 2 pronged question, but I’m currently programming a typewriter style dialogue system for an RPG project I’m working on, and I want to have it so the dialogue box reads off the text.

I noticed a particular quirk in the code i’ve written. The dialogue itself seems to play on for longer than it should, from what I can only assume is because the text includes bbcode markdown tags when it checks the length of the string, but I’m not sure how to go about detecting this and working around this issue.

At the same time, I want to also detect when the next character in said string is a space so it can skip over playing a sound for it.

Below I’ve included the current code I have for the dialogue system. I’m still just learning GDScript, so a lot of the code is a bit messy and unoptimized. I would greatly appreciate any help with this though!

extends Control
 
@export var dialogPath = "res://assets/data/dialogue/test/test.json"
@export var textboxPath = "res://assets/images/dialogue/"
@export var portraitPath = "res://assets/images/dialogue/portraits/"
@export var textSoundPath = "res://assets/sounds/text/"
@export var textSpeed : float = 0.04
 
var dialog
var sound
 
var phraseNum = 0
var finished = false
 
func _ready():
	dialog = getDialog()
	assert(dialog, "Dialog not found")
	create_tween().tween_property($DarkenBG, "modulate", Color(0, 0, 0, 0.25), 1).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT)
	create_tween().tween_property($DialogueBox, "position:y", $DialogueBox.position.y - 100, 0.8).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
	create_tween().tween_property($TopBar, "position:y", $TopBar.position.y + 50, 1).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT)
	create_tween().tween_property($BottomBar, "position:y", $BottomBar.position.y - 50, 1).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT)
	create_tween().tween_property($Portrait_Left, "position:x", $Portrait_Left.position.x + 300, 1).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
	create_tween().tween_property($Portrait_Right, "position:x", $Portrait_Right.position.x - 300, 1).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
	$Timer.wait_time = 1.2
	nextPhrase()
 
func _process(_delta):
	#$Indicator.visible = finished
	if Input.is_action_just_pressed("game_interact"):
		if finished:
			nextPhrase()
		else:
			$Text.visible_characters = len($Text.text)
 
func getDialog() -> Array:
	var output = JSON.parse_string(FileAccess.get_file_as_string(dialogPath))
	
	if typeof(output) == TYPE_ARRAY:
		return output
	else:
		return []
 
func nextPhrase() -> void:
	if phraseNum >= len(dialog):
		$Timer.wait_time = 0.8
		create_tween().tween_property($DarkenBG, "modulate", Color(0, 0, 0, 0), 1).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT)
		create_tween().tween_property($DialogueBox, "position:y", $DialogueBox.position.y + 100, 0.6).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
		create_tween().tween_property($Text, "position:y", $Text.position.y + 100, 0.6).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
		create_tween().tween_property($Name, "position:y", $Text.position.y + 100, 0.6).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
		create_tween().tween_property($TopBar, "position:y", $TopBar.position.y - 50, 0.8).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT)
		create_tween().tween_property($BottomBar, "position:y", $BottomBar.position.y + 50, 0.8).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT)
		create_tween().tween_property($Portrait_Left, "position:x", $Portrait_Left.position.x - 300, 0.8).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
		create_tween().tween_property($Portrait_Right, "position:x", $Portrait_Right.position.x + 300, 0.8).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
		$Timer.start()
		await $Timer.timeout
		queue_free()
		return
	
	finished = false
	
	#Dialogue API - Mode & Name
	var dialogMode = dialog[phraseNum]["Mode"]
	$DialogueBox/AnimationPlayer.play(dialogMode)
	
	#Dialogue API - Text, Textboxes and Sound
	$Text.bbcode_text = dialog[phraseNum]["Text"]
	$Text.set_position(Vector2(-152,-50))
	if dialogMode != "M":
		var box = textboxPath + "textbox_" + dialog[phraseNum]["Name_" + dialogMode] + ".png"
		if FileAccess.file_exists(box): 
			$DialogueBox.texture = load(box)
		else: $DialogueBox.texture = load(textboxPath + "textbox_generic.png")
		sound = textSoundPath + "text_" + dialog[phraseNum]["Name_" + dialogMode] + ".ogg"
		if FileAccess.file_exists(sound): 
			$AudioStreamPlayer.stream = load(sound)
		else: $AudioStreamPlayer.stream = load(textSoundPath + "text_generic.ogg")
	if dialogMode == "L":
		$Text.set_position(Vector2(-72,-50))
		$Name.set_position(Vector2(166, -82))
		create_tween().tween_property($Portrait_Left, "modulate", Color(1, 1, 1, 1), 1).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT)
		create_tween().tween_property($Portrait_Right, "modulate", Color(0.5, 0.5, 0.5, 1), 1).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT)
	elif dialogMode == "R":
		$Text.set_position(Vector2(-232,-50))
		$Name.set_position(Vector2(-226, -82))
		create_tween().tween_property($Portrait_Left, "modulate", Color(0.5, 0.5, 0.5, 1), 1).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT)
		create_tween().tween_property($Portrait_Right, "modulate", Color(1, 1, 1, 1), 1).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT)
	elif dialogMode == "M":
		create_tween().tween_property($Portrait_Left, "modulate", Color(0.5, 0.5, 0.5, 1), 1).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT)
		create_tween().tween_property($Portrait_Right, "modulate", Color(0.5, 0.5, 0.5, 1), 1).set_trans(Tween.TRANS_EXPO).set_ease(Tween.EASE_OUT)
	$Text.visible_characters = 0
		
	#Dialogue API - Portraits
	var img_l = portraitPath + dialog[phraseNum]["Name_L"] + "_" + dialog[phraseNum]["Emote_L"] + "_L.png"
	if FileAccess.file_exists(img_l): 
		$Portrait_Left.texture = load(img_l)
	else: $Portrait_Left.texture = null
	
	var img_r = portraitPath + dialog[phraseNum]["Name_R"] + "_" + dialog[phraseNum]["Emote_R"] + "_R.png"
	if FileAccess.file_exists(img_r): 
		$Portrait_Right.texture = load(img_r)
	else: $Portrait_Right.texture = null
	
	#Dialogue API - Initial Timer
	$Timer.start()
	await $Timer.timeout
	$Timer.wait_time = textSpeed
	if dialogMode == "L" or dialogMode == "R":
		$Name.bbcode_text = dialog[phraseNum]["Name_" + dialogMode]
	else: $Name.bbcode_text = " "
	#Dialogue API - Text Displaying
	while $Text.visible_characters < len($Text.text):
		$Text.visible_characters += 1
		$AudioStreamPlayer.play(1)
		$Timer.start()
		await $Timer.timeout
	
	finished = true
	phraseNum += 1
	return

can you show an example or a piece of your json text?

To get the parsed text without BBCode you’ll need to use RichTextLabel.get_parsed_text()

To add delay/ silence to spaces and other characters I create an Animation at runtime and play that animation. Example:

extends Node


## The speed at which the text animation will play
@export var playback_speed = 1.5
## The speead at which each character will appear
@export var char_speed = 0.08
## The long stop delay
@export var long_stop_delay = 0.4
## The long stop characters
@export var long_stop_chars = [".", "?", "!"]
## The short stop delay
@export var short_stop_delay = 0.2
## The short stop characters
@export var short_stop_chars = [",", ";", ":"]


@onready var rich_text_label: RichTextLabel = $RichTextLabel
@onready var animation_player: AnimationPlayer = $AnimationPlayer
@onready var audio_stream_player: AudioStreamPlayer = $AudioStreamPlayer


func _ready() -> void:
	rich_text_label.bbcode_enabled = true
	rich_text_label.visible_characters_behavior = TextServer.VC_CHARS_AFTER_SHAPING

	display("[b][wave]Hello![/wave][/b] How are you? I'm fine, I guess. I can play sounds!!! [rainbow]YAY![/rainbow]")
	await animation_player.animation_finished
	await get_tree().create_timer(2).timeout
	display("Another sentence I can say, I'm not sure what else to say. What do you think?")

	
func display(text:String) -> void:
	# Stop the current animation
	animation_player.stop()

	# Reset the visible characters to 0 and set the text to be displayed
	rich_text_label.visible_characters = 0
	rich_text_label.text = text

	var animation = _generate_animation(rich_text_label.get_parsed_text())

	# If the animation player does not have a global animation library, create one
	if not animation_player.has_animation_library(""):
		animation_player.add_animation_library("", AnimationLibrary.new())
	var library = animation_player.get_animation_library("")

	# remove the old animation if it exists
	if library.has_animation("play_text"):
		library.remove_animation("play_text")

	# Add the new animation
	library.add_animation("play_text", animation)
	# Set the speed of the animation
	animation_player.speed_scale = playback_speed
	animation_player.play("play_text")


func play_sound(idx:int, char:String):
	audio_stream_player.play()


func _generate_animation(text:String) -> Animation:
	# Create a new animation with 2 tracks, one for the visible_characters and one for the play_sound()
	var animation = Animation.new()
	var track_index = animation.add_track(Animation.TYPE_VALUE)
	var sound_index = animation.add_track(Animation.TYPE_METHOD)

	animation.track_set_path(track_index, "%s:visible_characters" % get_path_to(rich_text_label))
	animation.track_set_path(sound_index, get_path_to(self))

	# For each character check if it should skip the sound and add a delay
	# if the current character is inside one of the short/long characters array and if the next is a space
	var time = 0.0
	for i in text.length():
		var current_char = text[i]
		var next_char = null
		if i < text.length() - 1:
			next_char = text[i+1]

		var skip_sound = false
		if current_char == " ":
			skip_sound = true
		var delay = char_speed
		if next_char and next_char == " ":
			if current_char in short_stop_chars:
				delay = short_stop_delay
				skip_sound = true
			elif current_char in long_stop_chars:
				delay = long_stop_delay
				skip_sound = true

		animation.track_insert_key(track_index, time, i+1)
		if not skip_sound:
			animation.track_insert_key(sound_index, time, {"method": "play_sound", "args": [i, current_char]})

		time += delay

	# Set the final time to the animation
	animation.length = time

	return animation

Result

(Enable sound)

6 Likes

This worked great, thank you! I managed to get some parts from this code to fit in with the current system i’m using without the need of an animation player, and it works like a charm.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.