godot-proposals
godot-proposals copied to clipboard
When running multiple instances, open windows next to each other instead of on top
Describe the project you are working on
A networked soccer game
Describe the problem or limitation you are having in your project
Debugging networked programs can be tough, especially because you often need multiple programs running at the same time. While the Debug/Run Multiple Instances provides a useful way to start many sessions at once, you must still manually resize and move the windows each time you start a new session. It would be really helpful if your multiple windows would open next to each other rather than exactly on top of each other.
Describe the feature / enhancement and how it helps to overcome the problem or limitation
Add a 'tiled' checkbox option to the Debug/Run Multiple Instances section. When checked, the extra windows will be laid out next to each other in a grid instead of on top of each other.
Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams
When you run, windows will be smaller and laid out in a grid instead of on top of each other.
If this enhancement will not be used often, can it be worked around with a few lines of script?
No
Is there a reason why this should be core and not an add-on in the asset library?
Requires changing how programs are launched.
It can be worked around by manually changing window position based on launch arguments.
Can it be configured in project settings? Or do you need to run script commands in the _ready() of your first scene?
Both is possible actually.
display/window/size/initial_position allows to define initial position of the window. You can use feature overrides to define multiple positions and then run your instances with different feature tags. However seems like you can't use custom tags in the settings dialog, so you can either add these settings manually in project.godot or use existing tags.
Then provide these tags for the instances:
(though using default tags in this way is not recommended; better to use custom tags)
It's easier from code, if you have autoload singleton just set the position in _init(), so it's moved at the very beginning.
I think this is something Godot can and should do on it's own, for the following reasons:
- Placing windows manually every time you run the editor is really annoying
- Wanting your window automatically placed so that you can test comfortably is far from an uncommon use case
- Godot acknowledges this, and since 2.0 has allowed users to define window placement from the editor gui via the
Run/Window Placementeditor settings section. - Godot 4.0 introduced the option to run multiple instances
- Multiple instances still honor the settings in
Run/Window Placementbut this new use case is not handled gracefully. All windows follow exactly the same settings which means they end up in exactly the same position, with exactly the same size, fully covering each other, forcing the user to move them one by one. - This is even worse than the default behavior in most stacking window managers like DWM(Windows) and KDE which would use a cascading layout
- Given that multiplayer debugging often involves syncing issues, an option to tile the instances makes sense.
To flesh out the proposal a bit more I think the new settings could be the following:
Mockup
Description
| Prop Name | Description |
|---|---|
| Multiple Instance Placement Style | Defines placement approach to use when running multiple instances. None (default): Do nothing. Preserve the current behavior Cascading: Use a cascading layout. Tiling: Use a grid layout |
| Tile Grid Size | How to divide the screen space. Defaults to 2x1 (side-to-side split screen) |
| Preserve Aspect Ratio | Off (default) : Each instance is resized to fully cover its assigned tile On: Each instance is resized so it fits inside it's assigned tile, while respecting the aspect ratio defined in "display/window/size/viewport_width" and "display/window/size/viewport_width" |
Some common questions:
Q: Why add cascading? No one requested that A: I added it for completion since most window managers offer it, and we can easily get the title bar size with DisplayServer::window_get_title_size. Feel free to ignore it if you think it's unnecessary bloat.
Q: Why "Tile Grid Size" instead of just side by side? A: Because some users might want two instances side-by-side, others might want them top and bottom, others might have 4 instances instead. By letting the user just specify the grid size you cover all those cases with the same implementation
Q: What happens if the user sets the amount of instances to say, four, and the grid has fewer divisions, e.g: 2x1? A: The tile index wraps around and, in the example, you'd get instances 3 and 4 on top of 1 and 2 respectively. It's the responsibility of the user to set a grid size that makes sense for the amount of running instances. "Auto" could be an option but I don't know if changing the amount of running instances all the time is a common use case.
Q: What's the point of "Preserve Aspect Ratio"? A: Some games have UI and cameras designed for a specific aspect ratio (usually the 16:9 standard), so we might want to support that use case. Again feel free to ignore it if you think it's unnecesary
Implementation details
In editor_run.cpp around https://github.com/godotengine/godot/blob/88ed6af1e6844908293aa1599421b40870be513c/editor/editor_run.cpp#L125 we need to:
- Get the instance count:
int instance_count = RunInstancesDialog::get_singleton()->get_instance_count(); - if instance_count > 1 we switch based on the value of EDITOR_GET("run/window_placement/multiple_instance_placement_style")
- For the "None" case we do what we currently do
- For the "Cascading" case we can do
Size2i size DisplayServer::window_get_title_sizeand then iterate over the instance_count addingSize2i(size.y, size.y) * ito the position of each window - For the "Tiling" case:
- First we validate the grid size. Ensure x and y are no smaller than 1
- We get the tile count
int tile_count = grid_size.x * grid_size.y - We iterate over the instance count.
assigned_tile = i % instance_count - We use
assigned_tile,grid_size, andscreen_rectto calculate the size and position of each instance window - If "Preserve Aspect Ratio" is set we compare the tile aspect ratio and the project aspect ratio to decide if we need to shrink the window vertically or horizontally
- We save that info in args[i]. Args is defined here https://github.com/godotengine/godot/blob/88ed6af1e6844908293aa1599421b40870be513c/editor/editor_run.cpp#L50 and should be an array of List<String> now
- Here https://github.com/godotengine/godot/blob/88ed6af1e6844908293aa1599421b40870be513c/editor/editor_run.cpp#L234 we replace args with args[i]
Workaround
@blackears I'm using this to have side by side windows when I test the project I'm working on
Then on some autoload:
@export var SPLIT_SCREEN_STYLE := VERTICAL
@export var USE_RATIO := true
func _ready():
var screen_rect = DisplayServer.screen_get_usable_rect()
var screen_ratio = screen_rect.size.aspect()
if SPLIT_SCREEN_STYLE == HORIZONTAL:
get_window().size.x = screen_rect.size.x / 2
get_window().size.y = get_window().size.x / screen_ratio if USE_RATIO else screen_rect.size.y
get_window().position.y = screen_rect.position.y
else:
get_window().size.y = screen_rect.size.y / 2
get_window().size.x = get_window().size.y * screen_ratio if USE_RATIO else screen_rect.size.x
get_window().position.x = screen_rect.position.x
if "--server" in OS.get_cmdline_args():
get_window().position = screen_rect.position
elif "--client" in OS.get_cmdline_args():
if SPLIT_SCREEN_STYLE == HORIZONTAL:
get_window().position.x = screen_rect.size.x / 2
else:
get_window().position.y = screen_rect.size.y / 2
I think this is something Godot can and should do on it's own, for the following reasons:
[...]
Great suggestion, and thanks so much for this script, it's so useful!
This workaround doesn't work in 4.4 because the first run instance is always locked to the center of the screen
Anyone know how to disable that feature?
EDIT: found it, disable this in the "Game" editor tab
Hello, I would like to expand @noidexe's workaround:
First, I've created this helper script in a separate file named debug_tools.gd:
The detect_instance_index function is based on this script: https://gist.github.com/CrankyBunny/71316e7af809d7d4cf5ec6e2369a30b9
class_name DebugTools
var instance_index: int = -1
var instance_socket: TCPServer
func detect_instance_index() -> void:
instance_socket = TCPServer.new()
for index: int in 20:
if instance_socket.listen(5000 + index) == OK:
instance_index = index
break
func update_instance_window_rect(window: Window, max_instances_count: int, title_bar_height: int = 30) -> void:
var screen_rect: Rect2 = Rect2(DisplayServer.screen_get_usable_rect())
var cols: int = ceili(sqrt(max_instances_count))
var rows: int = ceili(float(max_instances_count) / cols)
var width: float = screen_rect.size.x / cols
var height: float = screen_rect.size.y / rows
var origin: Vector2 = screen_rect.position + Vector2(
(int(float(instance_index) / cols)) * width,
(instance_index % cols) * height
)
window.size = Vector2(width, height - title_bar_height)
window.position = origin + Vector2.DOWN * title_bar_height
Then, in some autoload script, I using the DebugTools like this:
var debug_tools: DebugTools
func _init() -> void:
if OS.is_debug_build():
debug_tools = DebugTools.new()
debug_tools.detect_instance_index()
func _ready() -> void:
if OS.is_debug_build():
debug_tools.update_instance_window_rect(get_window(), 4)
In the test above, I’m using 4 debug instances. However, I’m unable to detect the number of selected debug instances in the code, so I have to manually specify it as a parameter in the debug_tools.update_instance_window_rect function.
The debug_tools.update_instance_window_rect function adjusts the size and position of the current instance window, arranging it in a grid.
In my prototyping code, I’m using the debug_tools.instance_index variable to automatically generate the test user's login name.
@KanaszM that's pretty cool.
At W4 we ended up finding an alternative solution to automatically set up the "Run Multiple Instances" popup with the right data.
Based on a tool JuanFdS showed me I noticed it was probably possible to create an EditorPlugin, get a ref to the Run Multiple Instances popup and manipulate it from a plugin as if it were the user.
Basically something like this:
@tool
enum Columns {
COLUMN_OVERRIDE_ARGS,
COLUMN_LAUNCH_ARGUMENTS,
COLUMN_OVERRIDE_FEATURES,
COLUMN_FEATURE_TAGS,
}
static func find_run_instances_dialog_nodes() -> Dictionary:
var nodes := {
"instance_count_spinbox": null,
"run_multiple_instances_checkbox": null,
"instance_data_tree": null
}
var editor_gui := EditorInterface.get_base_control()
var child_nodes := editor_gui.get_children()
var run_instances_dialog: Node
for c in child_nodes:
if c.name.contains("RunInstancesDialog"):
run_instances_dialog = c
break
if not run_instances_dialog:
return nodes
var main_vbox: VBoxContainer = run_instances_dialog.get_children().filter(func(c: Node): return c is VBoxContainer).front()
nodes.instance_data_tree = main_vbox.get_children().filter(func(c: Node): return c is Tree).front()
var grid: GridContainer = main_vbox.get_children().filter(func(c: Node): return c is GridContainer).front()
for c in grid.get_children():
if c is CheckBox:
nodes.run_multiple_instances_checkbox = c
elif c is SpinBox:
nodes.instance_count_spinbox = c
return nodes
static func update_instance_settings(instance_count: int, instance_args: Array[String], feature_tags : String = "") -> void:
var nodes := find_run_instances_dialog_nodes()
if not nodes.instance_count_spinbox or not nodes.run_multiple_instances_checkbox or not nodes.instance_data_tree:
push_error("Could not find RunInstancesDialog nodes")
return
# Always turn it on, it's an easier way to always pass the args to instances
nodes.run_multiple_instances_checkbox.button_pressed = true # instance_count > 1
nodes.instance_count_spinbox.value = instance_count
var tree_root = nodes.instance_data_tree.get_root()
var tree_items: Array = tree_root.get_children()
for i in tree_items.size():
var ti: TreeItem = tree_items[i]
ti.set_checked(Columns.COLUMN_OVERRIDE_ARGS, true)
ti.set_text(Columns.COLUMN_LAUNCH_ARGUMENTS, instance_args[i])
ti.set_text(Columns.COLUMN_FEATURE_TAGS, feature_tags)
With that solved lumenwrites found a pretty clever way to handle the window positioning and sizing. You can create a dashboard where you set the number of instances and any extra options like in my initial mockup, and the EditorPlugin can precalculate everything just once and pass it as launch arguments. Godot already handles --position {x},{y}, --resolution {width}x{height} and even --screen {n}. You can also add things like --instance-id {n} or --user-email {email} and use OS.get_cmdline_args() to parse them in your running instances.
The plugin we are using is tied to internal stuff but that's the gist of it. You can even use an autoload to auto-connect the instances and start a match as soon as you press F5.
A lot of quick and easy code workarounds are given to achieve something way more adaptative than a never perfect multi-instance config solution.
My way to manage a basic client/server debug with 2 instances and an arg on each of them (context: wide display, server on top of 1/3 of the display, client on bottom of 1/3 of the display, godot on 2/3 right of the display) :
# server instance args :
--is-debug-server
# client instance args :
--is-debug-client
# main.gd
func _ready() -> void:
if OS.is_debug_build():
var window_size = DisplayServer.screen_get_size() / Vector2i(3, 2)
if OS.get_cmdline_user_args().has("--is-debug-server"):
get_window().position = Vector2i(0, 0)
get_window().size = window_size
start_server()
if OS.get_cmdline_user_args().has("--is-debug-client"):
get_window().position = Vector2i(0, window_size.y)
get_window().size = window_size
start_client()
And of course each use case can be handle in any needed way.
Finally, already said but for a no-code solution of my context (without automatic launch of the server/client part), we can configure each instance args like that (I have a 3840x1600 display) :
# server instance args :
--position 0,0 --resolution 1280x800
# client instance args :
--position 0,800 --resolution 1280x800
I don't think making plans to implement something far more complex (tested for production use and with multiple use cases management) is needed, or even useful. It won't be possible to manage all the use cases people can have.
And if someone really needs something like that, this is a good feature for an addon, but with basic solutions like the code and no-code ones I gave, any use case can be managed without any pain and any core modification.
--position 0,0 --resolution 1280,800
For anyone finding this before the launch args docs, it's "--resolution 1280x800" with an "x" not a comma