// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
using System.Threading;
using FlaxEditor.States;
using FlaxEditor.Windows;
using FlaxEngine;
namespace FlaxEditor.Modules
{
///
/// Manages play in-editor feature (game simulation).
///
///
public sealed class SimulationModule : EditorModule
{
private bool _isPlayModeRequested;
private bool _isPlayModeStopRequested;
private bool _stepFrame;
private bool _updateOrFixedUpdateWasCalled;
private long _breakpointHangFlag;
private EditorWindow _enterPlayFocusedWindow;
private Guid[] _scenesToReload;
internal SimulationModule(Editor editor)
: base(editor)
{
FlaxEngine.Scripting.FixedUpdate += OnFixedUpdate;
}
///
/// Gets a value indicating whether Editor is during breakpoint hit hang (eg. Visual Script breakpoint).
///
public bool IsDuringBreakpointHang { get; set; }
///
/// Occurs when breakpoint hang begins (on eg. Visual Script breakpoint hit).
///
public event Action BreakpointHangBegin;
///
/// Occurs when breakpoint hang ends (eg. Visual Script debugging tools continue game execution).
///
public event Action BreakpointHangEnd;
internal object BreakpointHangTag;
internal void OnBreakpointHangBegin()
{
_breakpointHangFlag = 1;
IsDuringBreakpointHang = true;
BreakpointHangBegin?.Invoke();
}
internal void StopBreakpointHang()
{
Interlocked.Decrement(ref _breakpointHangFlag);
}
internal bool ContinueBreakpointHang()
{
return Interlocked.Read(ref _breakpointHangFlag) == 1;
}
internal void OnBreakpointHangEnd()
{
IsDuringBreakpointHang = false;
BreakpointHangTag = null;
BreakpointHangEnd?.Invoke();
}
///
/// Delegates between playing game and playing scenes in editor based on the user's editor preference.
///
public void DelegatePlayOrStopPlayInEditor()
{
switch (Editor.Options.Options.Interface.PlayButtonAction)
{
case Options.InterfaceOptions.PlayAction.PlayGame:
Editor.Simulation.RequestPlayGameOrStopPlay();
return;
case Options.InterfaceOptions.PlayAction.PlayScenes:
Editor.Simulation.RequestPlayScenesOrStopPlay();
return;
}
}
///
/// Returns true if play mode has been requested.
///
public bool IsPlayModeRequested => _isPlayModeRequested;
///
/// Requests start playing in editor.
///
public void RequestStartPlayScenes()
{
if (Editor.StateMachine.IsEditMode)
{
Editor.Log("[PlayMode] Start");
if (Editor.Options.Options.General.AutoReloadScriptsOnMainWindowFocus)
ScriptsBuilder.CheckForCompile();
_isPlayModeRequested = true;
Editor.UI.UpdateToolstrip();
}
}
///
/// Requests playing game start or stop in editor from the project's configured FirstScene.
///
public void RequestPlayGameOrStopPlay()
{
if (Editor.StateMachine.IsPlayMode)
{
RequestStopPlay();
}
else
{
RequestStartPlayGame();
}
}
///
/// Requests start playing in editor from the project's configured FirstScene.
///
public void RequestStartPlayGame()
{
if (!Editor.StateMachine.IsEditMode)
{
return;
}
var firstScene = Content.Settings.GameSettings.Load().FirstScene;
if (firstScene == Guid.Empty)
{
if (Level.IsAnySceneLoaded)
Editor.Simulation.RequestStartPlayScenes();
return;
}
if (!FlaxEngine.Content.GetAssetInfo(firstScene.ID, out var info))
{
Editor.LogWarning("Invalid First Scene in Game Settings.");
}
// Load scenes after entering the play mode
_scenesToReload = new Guid[Level.ScenesCount];
for (int i = 0; i < _scenesToReload.Length; i++)
_scenesToReload[i] = Level.GetScene(i).ID;
Level.UnloadAllScenes();
Level.LoadScene(firstScene);
Editor.PlayModeEnd += OnPlayGameEnd;
RequestPlayScenesOrStopPlay();
}
private void OnPlayGameEnd()
{
Editor.PlayModeEnd -= OnPlayGameEnd;
Level.UnloadAllScenes();
foreach (var sceneId in _scenesToReload)
Level.LoadScene(sceneId);
}
///
/// Requests stop playing in editor.
///
public void RequestStopPlay()
{
if (Editor.StateMachine.IsPlayMode)
{
Editor.Log("[PlayMode] Stop");
if (IsDuringBreakpointHang)
StopBreakpointHang();
_isPlayModeStopRequested = true;
Editor.UI.UpdateToolstrip();
}
}
///
/// Requests the playing scenes start or stop in editor.
///
public void RequestPlayScenesOrStopPlay()
{
if (Editor.StateMachine.IsPlayMode)
RequestStopPlay();
else
RequestStartPlayScenes();
}
///
/// Requests the playing mode resume or pause if already running.
///
public void RequestResumeOrPause()
{
if (Editor.StateMachine.PlayingState.IsPaused)
Editor.Simulation.RequestResumePlay();
else
Editor.Simulation.RequestPausePlay();
}
///
/// Requests pause in playing.
///
public void RequestPausePlay()
{
if (Editor.StateMachine.IsPlayMode && !Editor.StateMachine.PlayingState.IsPaused)
{
Editor.Log("[PlayMode] Pause");
Editor.StateMachine.PlayingState.IsPaused = true;
Editor.UI.UpdateToolstrip();
}
}
///
/// Request resume in playing.
///
public void RequestResumePlay()
{
if (Editor.StateMachine.IsPlayMode && Editor.StateMachine.PlayingState.IsPaused)
{
Editor.Log("[PlayMode] Resume");
Editor.StateMachine.PlayingState.IsPaused = false;
Editor.UI.UpdateToolstrip();
}
}
///
/// Requests playing single frame in advance.
///
public void RequestPlayOneFrame()
{
if (Editor.StateMachine.IsPlayMode && Editor.StateMachine.PlayingState.IsPaused)
{
Editor.Log("[PlayMode] Step one frame");
// Set flag
_stepFrame = true;
_updateOrFixedUpdateWasCalled = false;
Editor.StateMachine.PlayingState.IsPaused = false;
// Update
Editor.UI.UpdateToolstrip();
}
}
///
public override void OnPlayBegin()
{
Editor.Windows.FlashMainWindow();
// Pick focused window to restore it
var gameWin = Editor.Windows.GameWin;
var editWin = Editor.Windows.EditWin;
if (editWin != null && editWin.IsSelected)
_enterPlayFocusedWindow = editWin;
else if (gameWin != null && gameWin.IsSelected)
_enterPlayFocusedWindow = gameWin;
// Show Game widow if hidden
if (gameWin != null)
{
if (gameWin.FocusOnPlay)
gameWin.FocusGameViewport();
gameWin.SetWindowMode(Editor.Options.Options.Interface.DefaultGameWindowMode);
}
Editor.Log("[PlayMode] Enter");
}
///
public override void OnPlayEnd()
{
// Restore focused window before play mode
if (_enterPlayFocusedWindow != null)
{
_enterPlayFocusedWindow.FocusOrShow();
_enterPlayFocusedWindow = null;
}
Editor.UI.UncheckPauseButton();
Editor.Log("[PlayMode] Exit");
}
///
public override void OnUpdate()
{
// Check if can enter playing in editor mode
if (Editor.StateMachine.CurrentState.CanEnterPlayMode)
{
// Check if play mode has been requested
if (_isPlayModeRequested)
{
// Check if editor has been compiled and scripting reloaded (there is no pending reload action)
if ((ScriptsBuilder.IsReady || !Editor.Options.Options.General.AutoReloadScriptsOnMainWindowFocus) && !Level.IsAnyActionPending && Level.IsAnySceneLoaded)
{
// Clear flag
_isPlayModeRequested = false;
// Enter play mode
var shouldPlayModeStartWithStep = Editor.UI.IsPauseButtonChecked;
Editor.StateMachine.GoToState();
if (shouldPlayModeStartWithStep)
{
RequestPausePlay();
}
}
}
// Check if play mode exit has been requested
else if (_isPlayModeStopRequested)
{
// Clear flag
_isPlayModeStopRequested = false;
// Exit play mode
Editor.StateMachine.GoToState();
}
// Check if step one frame
else if (_stepFrame)
{
if (_updateOrFixedUpdateWasCalled)
{
// Clear flag and pause
_stepFrame = false;
Editor.StateMachine.PlayingState.IsPaused = true;
Editor.UI.UpdateToolstrip();
}
else
{
// Fixed update may not be called but don't allow to call update for more than 2 times during single step
_updateOrFixedUpdateWasCalled = true;
}
}
}
else
{
// Clear flags
_isPlayModeRequested = false;
_isPlayModeStopRequested = false;
_stepFrame = false;
_updateOrFixedUpdateWasCalled = false;
}
}
private void OnFixedUpdate()
{
// Rise the flag so play mode step end will be called after physics update (user see objects movement)
_updateOrFixedUpdateWasCalled = true;
}
}
}