The game I’m working on is heavily menu-based, and I’m trying to find a way to get the scroll container to scroll to the bottom of its contents via script whenever a new menu pops up. Looking at the other answers on here talks about using functions that don’t appear to exist anymore. Anything associated with .value and max_value don’t work, although max value shows up as an integer when printed. What exactly am I missing?
I’m afraid this doesn’t work either. I tried multiple ways of using ScrollContainer, including set_deferred and across multiple scripts. I either get error messages or nothing happens.
Strangely, when I attach the function to a button it works perfectly, but I can’t seem to get it to automatically go. I’ve even tried to use signals to relay the command to a custom script attached to the container, but nothing seems to do the trick.
Update: I just added a print to the signal function and it appears to be calling correctly. It’s almost as if something is overwriting the scroll value after the function is called. Considering I have no other functions attached to the scroll value, any ideas on possible culprits?
As I said, I tried set_deferred already. My function is calling properly, but something else appears to be overwriting the value after the function sets it.
I’ve come across a workaround by using a timer set for .01 seconds into runtime.
Here’s the function I used in my script to solve this problem. I’m taking a LineEdit’s submitted signal, making a new Label (of a custom extended class, CommandEntryLabel), assigning the submitted text to it, and appending it into a VBoxContainer within a ScrollContainer.
My best guess about what’s going on is that you tell it to scroll to what the maximum value is, but then a split second later it updates what that max value ACTUALLY is, which makes it appear like it didn’t really scroll to the bottom.
I hope this helps!
func _on_command_entry_text_submitted(command_text: String) -> void:
var new_entry: CommandEntryLabel = CommandEntryLabel.new()
new_entry.text = command_text
%HistoryEntries.add_child(new_entry)
%CommandEntry.text = ''
# Waiting a split second after adding the text allows the program to catch up and properly
# scroll all the way to the bottom. It's frustrating that I have to even do this hack,
# but... here we are.
get_tree().create_timer(.01).timeout.connect(
func () -> void:
%CommandHistory.scroll_vertical = %CommandHistory.get_v_scroll_bar().max_value
)
You could recursively defer call the same function here:
func scroll_to_bottom_in_n_frames(n: int):
if n <= 0:
@warning_ignore("narrowing_conversion")
scroll_vertical = get_v_scroll_bar().max_value
else:
scroll_to_bottom_in_n_frames.call_deferred(n - 1)
I ran into a similar problem described in this thread, except I had to wait for 3 frames for the sizing to propagate to my scroll container, due to how I’m using a few nested containers between the list I’m appending to and the scrolling container.
So by playing around with a few of the suggestions here (thank you all, this thread is a life-saver lol) I ended up with this for making a DMs-style chatbox:
func new_message(message) :
var message_instance = individualChats.instantiate()
message_instance.set_data(message)
$ScrollContainer/VBoxContainer.add_child(message_instance)
func update_scroll() :
var max_scroll = $ScrollContainer.get_v_scroll_bar().max_value
$ScrollContainer.set_deferred("scroll_vertical", max_scroll)
func _on_action_to_send_message() :
var msg = "Hello World"
new_message(msg)
update_scroll.call_deferred()
Not sure if this only works because it’s instantiating another scene for the individual messages, but it might be worth testing without that and seeing if something like this:
func update_scroll() :
var max_scroll = $ScrollContainer.get_v_scroll_bar().max_value
$ScrollContainer.set_deferred("scroll_vertical", max_scroll)
func menu_popup() :
# insert code here
update_scroll.call_deferred()
Is this a bug? If yes, has anyone reported it yet? This feels like a really weird and hacky solution for something as basic as “scrolling to bottom in a node specifically designed for scrolling”
For future reference… here’s one of the solutions from above but in C#:
Years later, spent all night trying to solve this problem with some help from this thread. This is a custom ScrollContainer which keeps the scrollbar pinned to the bottom as it’s content is changed, but only when the scrollbar was touching the bottom before the content was changed. Those who come later may find it useful. Made in Godot 4.6.2.
extends ScrollContainer
## Scroll container that stays at bottom if new element is added and scroll was at bottom
class_name PinDownScrollContainer
## [code]true[/code] if scrollbar is at bottom
var _pinning : bool = false
func _init():
get_v_scroll_bar().changed.connect(_on_scroll_bar_changed)
get_v_scroll_bar().value_changed.connect(_on_scroll_bar_value_changed)
## When scroll bar size changes (element count changed), keep scroll at bottom is pinning [code]true[/code]
func _on_scroll_bar_changed():
if _pinning:
_scroll_to_bottom.call_deferred()
## When scroll bar is scrolled, set [code]pinned[/code] to [code]true[/code]
func _on_scroll_bar_value_changed(x):
_pinning = (scroll_vertical >= get_v_scroll_bar().max_value-get_v_scroll_bar().page-1.0)
## Scroll to the bottom
func _scroll_to_bottom():
scroll_vertical = get_v_scroll_bar().max_value