Revamp the cinema GUI code

This patch introduces a new class for managing the cinema overlay (two
black bars) and cutscene mode. This makes it more extensible for the
future, e.g. allowing to display text on the bars.
Since <include> elements can only be placed inside an <object>, this
patch needs to restructure session.xml a bit and also adds some
explanation comments while at it.
This commit is contained in:
Vantha 2026-01-04 15:22:19 +01:00 committed by Vantha
parent d882ab74a1
commit 64de934dd3
7 changed files with 198 additions and 127 deletions

View file

@ -0,0 +1,92 @@
/**
* This class manages the cinematic overlay, which is responsible for showing black bars at the top and bottom while
* cinema paths are playing. Cinema paths are predefined camera animations, which block player input of any kind while
* playing; cutscenes, essentially. Whether one is playing is communicated through the simulation state.
*/
class CinemaOverlay
{
constructor()
{
this.overlay = Engine.GetGUIObjectByName("cinemaOverlay");
this.barTop = Engine.GetGUIObjectByName("cinemaOverlayBarTop");
this.barBottom = Engine.GetGUIObjectByName("cinemaOverlayBarBottom");
// Objects to hide while showing the overlay.
this.primarySessionOverlays = Engine.GetGUIObjectByName("primaryOverlays");
this.bandbox = Engine.GetGUIObjectByName("bandbox");
this.hotkeys = Engine.GetGUIObjectByName("hotkeys");
this.overlay.onSimulationUpdate = this.onSimulationUpdate.bind(this);
this.overlay.onWindowResized = () =>
{
this.recalculateBarSizes();
};
this.overlay.hidden = true;
this.isCutsceneModeEnabled = Engine.Renderer_GetCutsceneModeEnabled();
this.recalculateBarSizes();
}
/**
* Enable or disable cutscene mode and remember it in order to save unnecessary calls to the engine.
* This, however, assumes that the mode isn't modified anywhere else.
*/
setCutsceneModeEnabled(enabled)
{
if (this.isCutsceneModeEnabled == enabled)
return;
Engine.Renderer_SetCutsceneModeEnabled(!!enabled);
this.isCutsceneModeEnabled = enabled;
}
isInCutsceneMode()
{
return this.isCutsceneModeEnabled;
}
onSimulationUpdate()
{
const cinemaPathPlaying = GetSimState().cinemaPathPlaying;
if (this.overlay.hidden && cinemaPathPlaying)
this.show();
else if (!this.overlay.hidden && !cinemaPathPlaying)
this.hide();
}
show()
{
if (!this.overlay.hidden)
return;
this.primarySessionOverlays.hidden = true;
this.bandbox.hidden = true;
this.hotkeys.hidden = true;
this.overlay.hidden = false;
this.setCutsceneModeEnabled(true);
}
hide()
{
if (this.overlay.hidden)
return;
this.primarySessionOverlays.hidden = !g_ShowGUI;
this.hotkeys.hidden = false;
this.overlay.hidden = true;
this.setCutsceneModeEnabled(false);
}
recalculateBarSizes()
{
const minHeight = 115;
const width = this.overlay.getComputedSize().right;
const height = this.overlay.getComputedSize().bottom;
// The aspect ratio of 2.39:1 is typical in films and has a cinematic feel to it.
const barHeight = Math.max(minHeight, (height - width / 2.39) / 2);
this.barTop.size.bottom = barHeight;
this.barBottom.size.top = -barHeight;
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<object name="cinemaOverlay">
<object name="cinemaOverlayBarTop" type="image" sprite="color:black" size="0 0 100% 100"/>
<object name="cinemaOverlayBarBottom" type="image" sprite="color:black" size="0 100%-100 100% 100%"/>
</object>

View file

@ -527,7 +527,7 @@ function getPreferredEntities(ents)
function handleInputBeforeGui(ev, hoveredObject)
{
if (GetSimState().cinemaPlaying)
if (g_CinemaOverlay.isInCutsceneMode())
return false;
// Capture cursor position so we can use it for displaying cursors,
@ -843,7 +843,7 @@ function handleInputBeforeGui(ev, hoveredObject)
}
function handleInputAfterGui(ev)
{
if (GetSimState().cinemaPlaying)
if (g_CinemaOverlay.isInCutsceneMode())
return false;
if (ev.hotkey === undefined)
ev.hotkey = null;

View file

@ -444,7 +444,6 @@ function handleNetStatusMessage(message)
if (message.status == "disconnected")
{
g_Disconnected = true;
updateCinemaPath();
closeOpenDialogs();
}

View file

@ -12,6 +12,7 @@ var g_Ambient;
var g_AutoFormation;
var g_Chat;
var g_Cheats;
var g_CinemaOverlay;
var g_DeveloperOverlay;
var g_DiplomacyColors;
var g_DiplomacyDialog;
@ -296,6 +297,7 @@ function init(initData, hotloadData)
g_Ambient = new Ambient();
g_AutoFormation = new AutoFormation();
g_Chat = new Chat(g_PlayerViewControl, g_Cheats);
g_CinemaOverlay = new CinemaOverlay();
g_DeveloperOverlay = new DeveloperOverlay(g_PlayerViewControl, g_Selection);
g_DiplomacyDialog = new DiplomacyDialog(g_PlayerViewControl, g_DiplomacyColors);
g_GameSpeedControl = new GameSpeedControl(g_PlayerViewControl);
@ -631,6 +633,10 @@ function onTick()
handleNetMessages();
updateCursorAndTooltip();
updateTimers();
if (g_CinemaOverlay.isInCutsceneMode())
return;
if (g_Selection.dirty)
{
@ -650,12 +656,7 @@ function onTick()
else if (g_ShowAllStatusBars && now % g_StatusBarUpdate <= tickLength)
recalculateStatusBarDisplay();
updateTimers();
Engine.GuiInterfaceCall("ClearRenamedEntities");
const isPlayingCinemaPath = GetSimState().cinemaPlaying && !g_Disconnected;
if (isPlayingCinemaPath)
updateCinemaOverlay();
}
function onSimulationUpdate()
@ -686,7 +687,6 @@ function onSimulationUpdate()
handler();
// TODO: Move to handlers
updateCinemaPath();
handleNotifications();
updateGUIObjects();
}
@ -694,40 +694,8 @@ function onSimulationUpdate()
function toggleGUI()
{
g_ShowGUI = !g_ShowGUI;
updateCinemaPath();
}
// TODO: The whole cinema UI should be handled by its own class.
var g_CutsceneModeEnabled = false;
function updateCinemaPath()
{
const isPlayingCinemaPath = GetSimState().cinemaPlaying && !g_Disconnected;
Engine.GetGUIObjectByName("session").hidden = !g_ShowGUI || isPlayingCinemaPath;
Engine.GetGUIObjectByName("cinemaOverlay").hidden = !isPlayingCinemaPath;
if (isPlayingCinemaPath && !g_CutsceneModeEnabled)
{
Engine.Renderer_SetCutsceneModeEnabled(true);
g_CutsceneModeEnabled = true;
}
else if (!isPlayingCinemaPath && g_CutsceneModeEnabled)
{
Engine.Renderer_SetCutsceneModeEnabled(false);
g_CutsceneModeEnabled = false;
}
}
function updateCinemaOverlay()
{
const cinemaOverlay = Engine.GetGUIObjectByName("cinemaOverlay");
const width = cinemaOverlay.getComputedSize().right;
const height = cinemaOverlay.getComputedSize().bottom;
let barHeight = (height - width / 2.39) / 2;
if (barHeight < 0)
barHeight = 0;
Engine.GetGUIObjectByName("cinemaBarTop").size.bottom = barHeight;
Engine.GetGUIObjectByName("cinemaBarBottom").size.top = -barHeight;
Engine.GetGUIObjectByName("primaryOverlays").hidden = !g_ShowGUI;
Engine.GetGUIObjectByName("supplementaryOverlays").hidden = !g_ShowGUI;
}
// TODO: Use event subscription onSimulationUpdate, onEntitySelectionChange, onPlayerViewChange, ... instead

View file

@ -8,6 +8,7 @@
<script directory="gui/session/"/>
<script directory="gui/session/campaigns/"/>
<script directory="gui/session/chat/"/>
<script directory="gui/session/cinema/"/>
<script directory="gui/session/developer_overlay/"/>
<script directory="gui/session/diplomacy/"/>
<script directory="gui/session/diplomacy/playercontrols/"/>
@ -34,93 +35,99 @@
onSimulationUpdate();
</action>
<!-- Hotkeys won't work properly unless outside menu -->
<include directory="gui/session/hotkeys/"/>
<include file="gui/session/NetworkStatusOverlay.xml"/>
<include file="gui/session/NetworkDelayOverlay.xml"/>
<include file="gui/session/PauseOverlay.xml"/>
<include file="gui/session/TimeNotificationOverlay.xml"/>
<!-- Chat messages -->
<object name="chatPanel" size="3 131 100% 100%-240" z="0" absolute="true">
<object name="chatLines">
<repeat count="20">
<object name="chatLine[n]" type="button" style="chatPanelOverlay" tooltip_style="sessionToolTipBottomBold" ghost="true" hidden="true"/>
</repeat>
<object name="primaryOverlays">
<!-- Chat messages -->
<object name="chatPanel" size="3 131 100% 100%-240" z="0" absolute="true">
<object name="chatLines">
<repeat count="20">
<object name="chatLine[n]" type="button" style="chatPanelOverlay" tooltip_style="sessionToolTipBottomBold" ghost="true" hidden="true"/>
</repeat>
</object>
</object>
<include directory="gui/session/chat/"/>
<include directory="gui/session/developer_overlay/"/>
<include directory="gui/session/dialogs/"/>
<include directory="gui/session/diplomacy/"/>
<include directory="gui/session/match_settings/"/>
<include file="gui/session/GameSpeedControl.xml"/>
<include file="gui/session/PanelEntities.xml"/>
<include file="gui/session/ResearchProgress.xml"/>
<include file="gui/session/TimeNotificationOverlay.xml"/>
<include file="gui/session/TopPanel.xml"/>
<include file="gui/session/trade/TradeDialog.xml"/>
<include file="gui/session/tutorial_panel.xml"/>
<include file="gui/session/Menu.xml"/>
<!-- Contains miscellanious objects s.a.: the technology research -->
<!-- progress, group selection icons, and the hero selection icon -->
<include directory="gui/session/session_objects/"/>
<!-- Information tooltip -->
<!-- Follows the mouse around if 'independent' is set to 'true'. -->
<object name="informationTooltip" type="tooltip" independent="true" style="informationTooltip"/>
<!-- Structure placement info tooltip -->
<object name="placementTooltip" type="tooltip" independent="true" style="informationTooltip"/>
<!-- START of BOTTOM PANEL -->
<!-- Limit to the minimal supported width of 1024 pixels. -->
<object size="50%-512 0 50%+512 100%">
<object size="50%-512 100%-204 50%-312 100%">
<include file="gui/session/minimap/MiniMap.xml"/>
</object>
<!-- Supplemental Details Panel (left). -->
<object name="supplementalSelectionDetails"
size="50%-316 100%-166 50%-110 100%"
sprite="supplementalDetailsPanel"
type="image"
z="20"
>
<include directory="gui/session/selection_panels_left/"/>
</object>
<!-- Selection Details Panel (middle). -->
<object name="selectionDetails"
size="50%-114 100%-204 50%+114 100%"
sprite="selectionDetailsPanel"
type="image"
>
<include directory="gui/session/selection_panels_middle/"/>
</object>
<!-- Commands Panel (right). -->
<object name="unitCommands"
size="50%+110 100%-166 50%+512 100%"
sprite="unitCommandsPanel"
type="image"
z="20"
>
<include directory="gui/session/selection_panels_right/"/>
</object>
</object><!-- END OF BOTTOM PANEL -->
</object><!-- END OF PRIMARY OVERLAYS -->
<!-- Hotkeys of button objects can only be triggered while the object is neither set hidden nor disabled. -->
<!-- That is why they are placed outside of menus and overlays. -->
<!-- And this object can therefore be used to turn on and off all these hotkeys here at once. -->
<object name="hotkeys">
<include directory="gui/session/hotkeys/"/>
</object>
<include directory="gui/session/chat/"/>
<include directory="gui/session/developer_overlay/"/>
<include directory="gui/session/dialogs/"/>
<include directory="gui/session/diplomacy/"/>
<include directory="gui/session/match_settings/"/>
<include file="gui/session/GameSpeedControl.xml"/>
<include file="gui/session/PanelEntities.xml"/>
<include file="gui/session/ResearchProgress.xml"/>
<include file="gui/session/TopPanel.xml"/>
<include file="gui/session/trade/TradeDialog.xml"/>
<include file="gui/session/tutorial_panel.xml"/>
<include file="gui/session/Menu.xml"/>
<!-- Selection bandbox. -->
<!-- Placed outside the primary overlays, so it can be shown even when all overlays are hidden (e.g. with the hotkey)-->
<object name="bandbox" type="image" sprite="bandbox" ghost="true" hidden="true" z="200"/>
<!-- Contains miscellanious objects s.a.: the technology research -->
<!-- progress, group selection icons, and the hero selection icon -->
<include directory="gui/session/session_objects/"/>
<!-- Overlaps with the primary overlays, they're never shown both at once. -->
<include file="gui/session/cinema/CinemaOverlay.xml"/>
<!-- Information tooltip -->
<!-- Follows the mouse around if 'independent' is set to 'true'. -->
<object name="informationTooltip" type="tooltip" independent="true" style="informationTooltip"/>
<!-- Structure placement info tooltip -->
<object name="placementTooltip" type="tooltip" independent="true" style="informationTooltip"/>
<!-- START of BOTTOM PANEL -->
<!-- Limit to the minimal supported width of 1024 pixels. -->
<object size="50%-512 0 50%+512 100%">
<object size="50%-512 100%-204 50%-312 100%">
<include file="gui/session/minimap/MiniMap.xml"/>
</object>
<!-- Supplemental Details Panel (left). -->
<object name="supplementalSelectionDetails"
size="50%-316 100%-166 50%-110 100%"
sprite="supplementalDetailsPanel"
type="image"
z="20"
>
<include directory="gui/session/selection_panels_left/"/>
</object>
<!-- Selection Details Panel (middle). -->
<object name="selectionDetails"
size="50%-114 100%-204 50%+114 100%"
sprite="selectionDetailsPanel"
type="image"
>
<include directory="gui/session/selection_panels_middle/"/>
</object>
<!-- Commands Panel (right). -->
<object name="unitCommands"
size="50%+110 100%-166 50%+512 100%"
sprite="unitCommandsPanel"
type="image"
z="20"
>
<include directory="gui/session/selection_panels_right/"/>
</object>
</object><!-- END OF BOTTOM PANEL -->
<!-- Objects that make sense to sometimes still show while the primary overlays are hidden. -->
<object name="supplimentaryOverlays">
<include file="gui/session/NetworkStatusOverlay.xml"/>
<include file="gui/session/NetworkDelayOverlay.xml"/>
<include file="gui/session/PauseOverlay.xml"/>
</object>
</object> <!-- END OF SESSION OBJECT -->
<!-- Selection bandbox -->
<object name="bandbox" type="image" sprite="bandbox" ghost="true" hidden="true" z="200"/>
<!-- Cinema overlay -->
<object name="cinemaOverlay" hidden="true" ghost="true">
<object name="cinemaBarTop" type="image" sprite="color:0 0 0 255" size="0 0 100% 100" ghost="true"/>
<object name="cinemaBarBottom" type="image" sprite="color:0 0 0 255" size="0 100%-100 100% 100%" ghost="true"/>
</object>
</objects>

View file

@ -155,7 +155,7 @@ GuiInterface.prototype.GetSimulationState = function()
const cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
if (cmpCinemaManager)
ret.cinemaPlaying = cmpCinemaManager.IsPlayingQueue();
ret.cinemaPathPlaying = cmpCinemaManager.IsPlayingQueue();
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
ret.victoryConditions = cmpEndGameManager.GetVictoryConditions();