How To Create Dynamic Sized Sprite For A Speech Bubble?

Godot Version

4.3 flatpak version

Question

My goal is I’m trying to write a dynamic speech bubble system to where I can throw in text and calculate the size and placement of a speech bubble based on the length of the string. How do I go about creating this dynamically sized speech bubble? I made a 16x16 circle sprite thinking I could just define some regions in a TextureRect that has Repeat enabled to handle the growing/shrinking, but that messes up the “corners” of the rounded speech bubble. My second idea was make multiple sprite sections of the speech bubble and then Frankenstein them together, but there’s clearly a better way than both of them and I just can’t seem to find the correct switches in the inspector to achieve this. If anyone can help point me in the right direction, I’d appreciate it. Thanks.

You dynamically size the sprite using the scale property, which is awful and i hate it.

You dynamically size a speech bubble by not using sprites and instead using

  • CanvasLayer
    • PanelContainer
      • NinePatchRect (which preserverves your bubble corners)
        • Label with wordwrap and minimum size set

That should work with a rounded rectangle. If you want it super rounded… i’ve always have bad luck with speech bubbles if the text is too near the corner. But maybe a HBoxContainer of [leftSide, center, rightside] with the edges keepSize=fit_height? i haven’t tried it though.

Maybe someone smarter than me will answer.

Thanks for suggestion, it led me down a few other rabbit holes and I may have found a good way to accomplish this. Needs some more tweaks since it doesn’t work fully yet, but I see how it can.

  • CanvasLayer
    • PanelContainer
      • TileMapLayer (with a sprite atlas of the speech bubble configured as an autotile terrain)
      • RichTextLabel (I want context menus and more functionality than just a label)

Then in my CanvasLayer GD Script, I have a build_bubble(text: String) function that calls tilemaplayer.set_cells_terrain_connect(array_of_cells, 0, 0, false) which builds the bubble. Then I assign the label’s text to the argument.

The tweaks I need are positioning the text’s position and size properties based on the tilemaplayer’s get_used_rect() and some math to count how long a text string is and divide based on the max speech line and how many lines I want displayed prior to drawing the tile map. But it looks like this is a good dynamic way to build a speech bubble that can be customized at runtime by adding support for a few more arguments to build_bubble

1 Like

That’s a little insane, as so many clever game ideas are, and i super want to see screenshots when this is implemented.

All right, well that answered my initial question and now I’m onto the rest of the dialogue system with scrolling through long entries of text. Here’s my entire code, I know it’s messy but it works for the bubble part. And I added some nice customization to help build during runtime when I get the advance text system figured.

Hopefully this helps

extends CanvasLayer

@export var bubble_max_width = 50
@export var bubble_max_lines = 3

@export var horizontal_margin := 2
@export var font_size := 1.3
@export var font_line_height_size = 10


@onready var speech_line := $SpeechBubble/SpeechLine
@onready var tilemap := $SpeechBubble

var all_text_displayed: bool = false

enum speaker_direction {LEFT, RIGHT, UP, DOWN}
var bubble_default_screen_positions := {speaker_direction.LEFT: 10,
										speaker_direction.RIGHT: 70,
										speaker_direction.UP: 10,
										speaker_direction.DOWN: 100}

func display_next_text():
	pass

func build_bubble(text: String, bubble_side: speaker_direction,
				 	#Optional Params				
					sizzle: int = 4,
					bubble_height: int = bubble_default_screen_positions[speaker_direction.UP]) -> void:
		
	all_text_displayed = false
	tilemap.clear()

	# This looks at either making the speech bubble the size of
	# the supplied text, or the hardcoded max width defined above
	var ideal_bubble_width := 0
	var lines_required := 1
	if text.length() * font_size > bubble_max_width:
		ideal_bubble_width = bubble_max_width
		
		# We want to find how many lines of text it will need to display the bubble
		# This helps us understand how tall to make the speech bubble
		lines_required += ceil(text.length() / bubble_max_width)
		if lines_required > bubble_max_lines:
			lines_required = bubble_max_lines
	else:
		ideal_bubble_width = text.length() * font_size
			
	var arr_cell := []
	# Creates an array of cells to build the speech bubble using some slight pixel randomness with
	# the sizzle parameter to add some liveliness if wanted
	var bubble_sizzle := randi_range(0, sizzle) 
	for x in range(bubble_default_screen_positions[bubble_side] + bubble_sizzle,
					bubble_default_screen_positions[bubble_side] + ideal_bubble_width + bubble_sizzle):
		for y in range(bubble_height, bubble_height + (lines_required * 4)):
			arr_cell.append(Vector2i(x,y))
	
	# Uses the tilemap to build the autotiled speech bubble
	tilemap.set_cells_terrain_connect(arr_cell, 0, 0, false)
	speech_line.text = text
	
	# Find the properties of the drawn autotiled rectangle and assign the label's
	# size and position to ensure it stays within those boundaries
	# There are some small pixel adjustments in the + and - that may be really bad, but it works 
	# for the default font right now
	var used_rect: Rect2i = tilemap.get_used_rect()
	speech_line.position = (tilemap.map_to_local(used_rect.position) + Vector2(horizontal_margin, -2))
	speech_line.size = (tilemap.map_to_local(used_rect.size) - Vector2(horizontal_margin + 10, font_line_height_size))

func _input(event: InputEvent) -> void:
        # Just here to see a new bubble draw and size differently
	if event.is_action_pressed("click"):
		build_bubble("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip",
					 speaker_direction.RIGHT, 4, 20)
		
	
func _ready() -> void:
	build_bubble("Dynamic text?", speaker_direction.LEFT)
	

1 Like