Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dual view to the Game editor for editing and playing at the same time #11091

Open
RobProductions opened this issue Nov 4, 2024 · 14 comments · May be fixed by godotengine/godot#104079
Open

Comments

@RobProductions
Copy link

Describe the project you are working on

Improvements for the Godot editor.

Describe the problem or limitation you are having in your project

Now that godotengine/godot#97257 has been merged and embedding to follow soon, I wanted to follow up on an idea by @ccioanca which would I believe would vastly improve the workflow of this new feature. The problem comes from having to press a button to switch between 2D/3D edit mode and game input on the same viewport.

In the example given, this makes it impossible to live edit through the new runtime tools while someone else plays the game, say for a presentation. Also, let's say that some animation glitch occurs when the player interacts with a lever and you wanted to take a closer look at it, but it's out of view of the player's regular camera. It would be really difficult to view the glitch up close in this case because you may have to toggle input/camera control to start the interaction, at which point you may miss the important moment you're trying to debug. Obviously you could code around this for specific cases to test, but it creates a larger burden on the developer for something that could already be available to them if more tooling options were available.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

A good way to remedy these issues would be to have 2 views of the game, one with game input and one with the new runtime selection active. With this, you could set up a debugging view of the game before key moments happen and watch them occur from both the editor view and the player's view as you play through your project. This an approach other game engines use and it's extremely convenient for getting more visibility on complicated game worlds. Example:

image

Additionally, it would be neat to get options for different combinations of displaying the game, like edit view embedded but play view in its own window, or both views embedded, or both in their own window. I believe Unreal has somewhat similar options letting you choose how the game is run and how you debug it. Would definitely be tough to implement but I think the more customizable it is the better :)

Finally, a tangentially related idea would be that if we ever implement #7233 or something similar with fully customizable panels, you could sort of get this feature for free. It would be neat for the user to be able to create as many views into the game as they want and create their own layouts for editing/testing, but this is much further away of course, so it's best left for another time, just something to keep in mind.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

From a UX standpoint, I think the Game tab could look something like this:

GodotSplitScreenGameViewMockup

For now, to preserve the original behavior, we could have something similar to the View dropdown in the regular editor:

image

Which lets you toggle between 1 embedded view or 2 embedded views, and then whichever button will be used to "pop out" the game in the upcoming embedding PR can be applied with a separate button for each view. This way, both can be embedded or standalone at user discretion.

From a programming standpoint, the key challenge is passing input into one view and not into the other view. I don't have a lot of ideas as to how this will be possible, but mainly we would need to cull out typical input events when the game view is not focused. Conversely, editor hotkeys, etc. should not be used when the edit view is not focused. This would involve setting the "input is active" bool that was added for runtime debugging (called disable_input) when the user focuses on a specific view (either by mouse hover or mouse click into the view) rather than when they click the game input button.

However, this approach won't work for someone playing and editing at the same time. For now, I think the above idea is a reasonable start/initial PR, but to solve the problem of simultaneous view input, we would probably have to track the viewport source of each input event and cull out editor-specific keys/mouse events based on that. For rendering the individual views, we simply have to force the game to use another Viewport and add an additional piece of info to the viewport telling us whether it is in edit mode or not. I'm glossing over a bunch of details but I believe these basic concepts can kick off more detailed ideas. I'm happy to discuss implementation caveats/approaches if there's any suggestions!

If this enhancement will not be used often, can it be worked around with a few lines of script?

It can not be worked around easily.

Is there a reason why this should be core and not an add-on in the asset library?

All users would benefit from having additional options for debugging/running their game :)

@allenwp
Copy link

allenwp commented Nov 9, 2024

Until #7233 is implemented, here are my thoughts on how this issue should be addressed with game embedding. I believe it should be implemented to match the existing Script behaviour

Edit: I've moved this comment to a more appropriate location based on later conversation.

@RobProductions
Copy link
Author

When clicking on this "Make the game editor floating" button, it would behave the same as the floating script editor, which would address the issues brought up by this proposal:

To clarify, do you mean that this would be used to toggle a single view of the game between floating and embedded, or to create a duplicate view that is not embedded which is used for playing? If it's the first case, the point of this specific proposal is to be able to work with two views at the same time so I just want to reiterate that.

This is a good idea for toggling between embedded and not embedded, however @Hilderin 's PR godotengine/godot#99010 seems to have already tackled how the embedded view switches (and is more relevant to embedding functionality) so if this about button styling I'd recommend bringing the mockup over there to see if it can gain traction :)

@allenwp
Copy link

allenwp commented Nov 10, 2024

To clarify, do you mean that this would be used to toggle a single view of the game between floating and embedded, or to create a duplicate view that is not embedded which is used for playing? If it's the first case, the point of this specific proposal is to be able to work with two views at the same time so I just want to reiterate that.

Ah, I misunderstood this proposal! You're right, I should post in the other PR; I wasn't aware of this PR yet, and that's correct that my comment only applies to that PR and not this proposal. Thanks!

(When I read "2D/3D edit mode", I interpreted that to mean "2D/3D workspace", but this was intended to describe the Game workspace when input is disabled and 2D/3D selection is active.)

@RobProductions
Copy link
Author

I took a look at what it would take to get this working, and I was able to get the editor UI set up to handle 2 game views:

Image

However, both GameView instances fight over embedding a single game window. My best guess at solving this is:

  1. Have the debugger session spawn in a new Window Node when the "dual mode" is active
  2. Update the embedding calls to allow for embedding specific windows within a process
  3. Tell each GameView to access a specific window from the game so that they don't try to embed the same window

But, reading this code in display_server_windows.cpp:

HWND DisplayServerWindows::_find_window_from_process_id(OS::ProcessID p_pid, HWND p_current_hwnd) {
	DWORD pid = p_pid;
	WindowEnumData ed = { pid, p_current_hwnd, NULL };

	// First, check our own child, maybe it's already embedded.
	if (!EnumChildWindows(p_current_hwnd, _enum_proc_find_window_from_process_id_callback, (LPARAM)&ed) && (GetLastError() == ERROR_SUCCESS)) {
		if (ed.hWnd) {
			return ed.hWnd;
		}
	}

	// Then check all the opened windows on the computer.
	if (!EnumWindows(_enum_proc_find_window_from_process_id_callback, (LPARAM)&ed) && (GetLastError() == ERROR_SUCCESS)) {
		return ed.hWnd;
	}

	return NULL;
}

It seems that the window chosen to be embedded is just the first window in the "list of all windows" that matches the process ID. If I'm understanding that right, I'm not sure how this could be changed so that it searches for a specific window; I'm not sure if it's guaranteed that they will show up in a consistent order, and even if they did we would want to make sure that it's only embedding the new window created by the debugger, not some other user window.

Rather than go around in circles, maybe it's best to get some expert direction on this... @Hilderin if you have a chance, could you let me know if I'm on the right track here? Or is there some better solution to showing 2 embedded views without adding more functionality to the display server? I'm fairly certain I can handle the editor adjustments needed to get everything functional, but when it comes to potentially big display server changes I'm a bit lost... Thanks if you or anyone else is able to provide ideas :)

@Hilderin
Copy link

Hilderin commented Jan 27, 2025

Very interesting!! If I understand correctly, you want to have multiple instances of the running game embedded in the editor? To embed correctly each game window, you need to get the process id of each game instance from editor_run, have 2 instances of embedded_process control, and each control receive a different process id. In that case, the method _find_window_from_process_id should work as is because it should find the window for the right process.

@RobProductions
Copy link
Author

Very interesting!! If I understand correctly, you want to have multiple instances of the running game embedded in the editor? To embed correctly each game window, you need to get the process id of each game instance from editor_run, have 2 instances of embedded_process control, and each control receive a different process id. In that case, the method _find_window_from_process_id should work as is because it should find the window for the right process.

Actually I'm trying to get multiple windows from the same instance of the running game to become embedded, not multiple instances of the game! During my experiments I did try enabling multiple instance runs from the Debug menu to see if that would make things easier, and you're right that there is a clear path that way, but both instances of the game would get out of sync if you started to make changes in one. That wouldn't really work well for this proposal since it's about having 2 views of the same running game with one view getting runtime select and the other view getting game input.

So from my research (though I might be wrong) I think both windows of the game instance would share the same process ID, and in that case _find_window_from_process_id would only ever return the first window it finds. Is there a good way to differentiate between windows from the same process? Can we rely on the EnumWindows returning a consistent order of created windows? If so, I was thinking maybe we could just have a "window index" value that the user just sets in the editor UI so that they have control over what gets embedded. Then we could tell find_window to return the second or third or nth window that matches the PID...?

Feel free to let me know if this isn't a reasonable idea lol and thank you for the info!

@Hilderin
Copy link

Hilderin commented Jan 27, 2025

Sorry, I think I now understand correctly. Okay, you have two windows of the same process that you want to embed.

The first thing you need is to ensure that both windows have the editor as their parent. I’m not sure how you create the second window, but it must have the editor's window ID in the hWndParent parameter when calling CreateWindowExW.

Second, you need a way to differentiate between windows of the same process that share the same parent. One simple approach is to use the window title. If you know the expected window title, you could add a title parameter to the embed_process method and pass it to _find_window_from_process_id. Additionally, update the WindowEnumData struct to include the title and modify _enum_proc_find_window_from_process_id_callback to check for it.

Here’s an example of how to retrieve a window's title:

wchar_t title[256];
int length = GetWindowTextW(hWnd, title, sizeof(title) / sizeof(title[0]));
if (length > 0) {
    print_line("Embedded window found: " + String::utf16((const char16_t *)title));
} else {
    print_line("Embedded window found: no title");
}

This approach could also be implemented for Linux later. The only issue with this suggestion is that the main game window's title could be modified via a game script, so you can't always rely on the default game window title. As a workaround, you could add an option in the embed_process method to ignore the title if necessary.

@RobProductions
Copy link
Author

Second, you need a way to differentiate between windows of the same process that share the same parent. One simple approach is to use the window title. If you know the expected window title, you could add a title parameter to the embed_process method and pass it to _find_window_from_process_id. Additionally, update the WindowEnumData struct to include the title and modify _enum_proc_find_window_from_process_id_callback to check for it.

Ah, perfect! I should've figured that you could retrieve the window name via the Windows API, so I will definitely try this out and see how it goes :) thank you!

This approach could also be implemented for Linux later.

I'm less familiar with the Linux API and don't have a Linux machine to test, I'll look into it and see what can be done but then is it necessary to have both Linux & Windows implemented in the same PR because it would modify the embed_process params? (I assume the code calling it expects all Display Server definitions to be the same)

The only issue with this suggestion is that the main game window's title could be modified via a game script, so you can't always rely on the default game window title. As a workaround, you could add an option in the embed_process method to ignore the title if necessary.

Got it. Let's say there's a "window title" param that it checks for and if the title string is empty, it will do what it does now and just grab the first window it finds that matches the PID. To solve the issue of users changing window names while giving them the most control over debugging, I was thinking each GameView could have a dropdown for "Embedded Window" that could either be "First Available" or "Custom Name" allowing them to choose which game window gets embedded (with "First Available" being the default so it matches the current behavior). This seems like the best way for them to choose the window they want, because they might not know the order in which their multiple game windows will appear (i.e. no idea what "index" each window is in the list), but they will probably know the name of each window. Here's a mockup to sort of demonstrate the idea:

Image

Finally, would it make sense to first implement the "specific window embedding" Display Server changes as its own PR before tackling the editor stuff? Might be easier to pinpoint issues that way. Thanks again for the help and I appreciate the ideas!

@Hilderin
Copy link

Hilderin commented Jan 29, 2025

I'm less familiar with the Linux API and don't have a Linux machine to test, I'll look into it and see what can be done but then is it necessary to have both Linux & Windows implemented in the same PR because it would modify the embed_process params?

I suggest you make it work on Window to begin with and the adjustment could be made for Linux afterward in another PR. But yes, you will need to adjust the embed_process method signature everywhere (right now it's only implemented for Window and Linux).

Got it. Let's say there's a "window title" param that it checks for and if the title string is empty, it will do what it does now and just grab the first window it finds that matches the PID.

That will probably cause some issue because the order of the window found in _find_window_from_process_id does not respect any rules. It will probably be weird for the user to see different window each time the game is running.

One alternative way could be to send the window id from the game procress to the editor. It could be done via the EngineDebugger which runs in the game process. It could send a message to the editor when an "embeddable" window is created. The editor receives those messages in ScriptEditorDebugger::_parse_message and a signal can be emitted for the GameViewPlugin to receive. That's how the Floating Game title is updated when the game title changes.

Check window::title for an exemple to send the message to the editor:

#ifdef DEBUG_ENABLED
	if (EngineDebugger::get_singleton() && window_id == DisplayServer::MAIN_WINDOW_ID && !Engine::get_singleton()->is_project_manager_hint()) {
		Array arr;
		arr.push_back(tr_title);
		EngineDebugger::get_singleton()->send_message("window:title", arr);
	}
#endif

Once you have the window id, it could be passed to the embedded_process as a optionnal parameter.

That way the drop down with the window list could even come from the real list of existing windows from the game.

That's just another suggestion, I'm still not exactly sure how the second window will be created and exactly what you want to achieve :)

Finally, would it make sense to first implement the "specific window embedding" Display Server changes as its own PR before tackling the editor stuff?

I really suggest you do all in the same PR (except maybe for Linux if you can't do this part). That way, you will be sure the adjustments in DisplayServer will be the right ones and the code is actually useful.

@RobProductions
Copy link
Author

I suggest you make it work on Window to begin with and the adjustment could be made for Linux afterward in another PR. But yes, you will need to adjust the embed_process method signature everywhere (right now it's only implemented for Window and Linux).

Gotcha, will do.

That will probably cause some issue because the order of the window found in _find_window_from_process_id does not respect any rules. It will probably be weird for the user to see different window each time the game is running.

Isn't it fine for single-window apps though, which is how the embedding currently works? I guess I should clarify my current idea: Users can either pick the "dual screen" Game View or the "single screen" game view. The single screen game view works exactly as it does now, therefore the "first found" option should work fine for most cases by default (because it's identical to the way it functions currently). But let's say you as a user create a second window in your game (with a Window Node), then you might want that second window to become embedded in the Game View instead of the "main" one. Again, this is a current limitation because if you do this right now on 4.4, it's unclear which window becomes embedded* (in theory, but I learned something while coding this that I'll share below). But with the option of selecting the window to be embedded, you can eliminate uncertainty and give the user more control.

Then this applies to the "dual screen" Game View by just giving you the same "first found" or "custom name" option, so for each Game View you can pick which window you want to see in each one. So in the end you'll only want to use Dual Screen/embedded window option when you have created another window yourself, it won't automatically create a new debug window for you. While it asks to the user to perform a few extra steps for the dual screen to become useful, I think this might be more powerful in the long run, especially if you have a dual-window game to begin with. That's just where my mind is at right now, it seems the most straightforward option, but you can let me know if this is confusing or has issues I'm not seeing!

*One last thing I want to note is that technically only the "main window" i.e. the window that is loaded first from the game will be considered for embedding in 4.4 because it is the only one that receives an hWndParent set to the editor. Window Node windows are created as subwindows and did not have a parent, until I added that in on my branch to allow for embedding them as you suggested.

One alternative way could be to send the window id from the game procress to the editor. It could be done via the EngineDebugger which runs in the game process. It could send a message to the editor when an "embeddable" window is created. The editor receives those messages in ScriptEditorDebugger::_parse_message and a signal can be emitted for the GameViewPlugin to receive. That's how the Floating Game title is updated when the game title changes.

Check window::title for an exemple to send the message to the editor:

#ifdef DEBUG_ENABLED
if (EngineDebugger::get_singleton() && window_id == DisplayServer::MAIN_WINDOW_ID && !Engine::get_singleton()->is_project_manager_hint()) {
Array arr;
arr.push_back(tr_title);
EngineDebugger::get_singleton()->send_message("window:title", arr);
}
#endif

Once you have the window id, it could be passed to the embedded_process as a optionnal parameter.
That way the drop down with the window list could even come from the real list of existing windows from the game.

This seems a little more challenging to implement, but I think I get the idea. However, wouldn't this mean you need to the run the game at least once for the list to be populated? And couldn't that list get out of date if the user deletes a custom window? I really like the idea of having the dropdown reflect the window names though, so if these drawbacks are fine, or if there are solutions to them, I can definitely give it a shot! 😄

That's just another suggestion, I'm still not exactly sure how the second window will be created and exactly what you want to achieve :)

For now I was just thinking the user will be responsible for creating any additional windows they want to see embedded. Hopefully that sounds good as a more flexible implementation instead of having the dual screen mode create a new debugging window

I really suggest you do all in the same PR (except maybe for Linux if you can't do this part). That way, you will be sure the adjustments in DisplayServer will be the right ones and the code is actually useful.

Okay! I'll merge the display server adjustments into the editor changes and get the whole idea up for one big PR then :) Thanks to your guidance I was able to get secondary windows embedded, here's an example of the user-created "DebugWindow" viewed normally:

Image

And here's that same DebugWindow (which is a subwindow) now embedded:

Image

Obviously I'll have to sort out some issues (passing data through the GameView, editor changes, potentially changing the embedded_windows cache system to track specific windows instead of whole processes) but I feel like I'm on the right track now, so thanks a ton!

@RobProductions
Copy link
Author

So it took a fair amount of code changes (hopefully that's okay for the PR 😅 ) but I've got everything working together now and here's what I came up with: you can now choose between the "Main Window" and a "Custom Window", and when "Custom Window" is selected you get to input the title of the window that should become embedded in that view. "Main Window" works the same as it does now (where only the first loaded window in your app, what Godot calls the Main Window, becomes embedded) and is hopefully less confusing than "first found". Here's all of that in action:

Capture2025-02-03.17-53-25.mov

I want to stress that I'm showing all this in the proposed "dual screen" mode, which you can toggle. By default, you'll still only be working with one game view, and the "embedded window selector" works even with one Game View :) so I very much hope both of those are welcome features! What remains is figuring out how to activate the debugger systems based on the focused window (which honestly sounds a little challenging now that I've thought about it but at least it should be possible) and polishing up the code and user workflow as best I can.

My question for @Hilderin is: in order to get the custom window embedding to work, I had to send information to the built game from the editor so that the subwindow knows whether it should be parented or not. I learned that the existing embedding works by passing a --wid param when running the game, so I figured I would mirror that by adding a --swid (subwindow ID) param that takes in the window title and a parented window ID handle. After thinking on it, this way seems nice because even users/apps outside of the Godot editor can request that subwindows become parented through the new argument, but I wanted to make sure that is okay to do. Does this sound like the best approach for sending info to the built game or is there some other method I should look into?

Lastly, I just want to make sure the work I'm doing is as useful as possible and integrates smoothly with the codebase so if anyone has questions or ideas that I should look into feel free to let me know, thanks!

@Hilderin
Copy link

Hilderin commented Feb 4, 2025

I figured I would mirror that by adding a --swid (subwindow ID) param that takes in the window title and a parented window ID handle. After thinking on it, this way seems nice because even users/apps outside of the Godot editor can request that subwindows become parented through the new argument, but I wanted to make sure that is okay to do. Does this sound like the best approach for sending info to the built game or is there some other method I should look into?

That sounds a good approach to me.

Lastly, I just want to make sure the work I'm doing is as useful as possible and integrates smoothly with the codebase so if anyone has questions or ideas that I should look into feel free to let me know, thanks!

Just be sure the dual view is not enabled by default and could be opt in easily. I believe most games created in Godot use only a single window. I'm not exactly sure how, but one potentially useful feature for many users would be having "built-in" secondary views/windows. For example, the override camera could appear in a second window without requiring any additional code in the game. I'm not an expert in long-term Godot game development, so maybe others have more ideas.

@RobProductions
Copy link
Author

Just be sure the dual view is not enabled by default and could be opt in easily. I believe most games created in Godot use only a single window.

Yup, I implemented the split screen selector as a radio button in the embed options menu as I had it in the mockup above, so it'll be easily accessible and off by default.

I'm not exactly sure how, but one potentially useful feature for many users would be having "built-in" secondary views/windows. For example, the override camera could appear in a second window without requiring any additional code in the game. I'm not an expert in long-term Godot game development, so maybe others have more ideas.

I was thinking that too originally, but it seemed too challenging for now. As I wrote somewhere above, I had wanted the debugger to be able to send a signal that creates a new window node and automatically had it become embedded, but doing that would still require solving all the stuff I did already. It would be nice but I can live without it since it's only a few steps to make a debugging window yourself. Hopefully that part can be added for a future PR as I think subwindow embedding is more generally useful for people who make multi-window games.

Anyways thanks for keeping me on track! It'll take a bit to clean everything up but I have a clear path towards the PR :)

@RobProductions
Copy link
Author

Thanks to the help I've received from Hilderin I've opened up the PR that will close this issue so feel free to share your support here: godotengine/godot#104079

There are a few compromises that had to be made which can be mitigated in the future now that this core work is complete. I'll list them here for clarity:

  • The secondary game view needs a user created window to work. It would be nice if a debugging window was automatically created for the built game and used in the game view, perhaps with a "Debugging Window" setting in the dropdown that doesn't require a window name. This could be accomplished via debugging signals (the same way that the debugger tells the game to select objects in the runtime select tools) that run when the game first initializes.
  • The PR was getting too big and I chose to leave the secondary GameView debugging for another time. This means only the main GameView has debugging controls currently. Ideally, both GameViews would have debugging parity and can independently toggle camera override/input/etc. To accomplish this, we'd need to either use 2 copies of the Runtime Select debugger (though it is currently a singleton) or we'd have to send the desired window data through the debugging signals so that it knows which window to act on. I'm confident that one of these approaches will work, it will just take some more thought/guidance to know which solution is better for a future PR.
  • A side effect of the above means that when you use the debugging controls like camera override, they will only affect the main window regardless of where it's located. This should be remedied automatically when the secondary GameView controls are implemented.

As you can see, the current PR is just the first step towards a more streamlined debugging process, but I think this is a good point to split the work so that it's not too drastic of a change all at once. Appreciate the ideas and I'll be sure to keep an eye out for future suggestions :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants