#region License /* FNA - XNA4 Reimplementation for Desktop Platforms * Copyright 2009-2022 Ethan Lee and the MonoGame Team * * Released under the Microsoft Public License. * See LICENSE for details. */ #endregion #region Using Statements using System; using System.Collections.Generic; using System.Diagnostics; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input.Touch; #endregion namespace Microsoft.Xna.Framework { public class Game : IDisposable { #region Public Properties public GameComponentCollection Components { get; private set; } private ContentManager INTERNAL_content; public ContentManager Content { get { return INTERNAL_content; } set { if (value == null) { throw new ArgumentNullException(); } INTERNAL_content = value; } } public GraphicsDevice GraphicsDevice { get { if (graphicsDeviceService == null) { graphicsDeviceService = (IGraphicsDeviceService) Services.GetService(typeof(IGraphicsDeviceService)); if (graphicsDeviceService == null) { throw new InvalidOperationException( "No Graphics Device Service" ); } } return graphicsDeviceService.GraphicsDevice; } } private TimeSpan INTERNAL_inactiveSleepTime; public TimeSpan InactiveSleepTime { get { return INTERNAL_inactiveSleepTime; } set { if (value < TimeSpan.Zero) { throw new ArgumentOutOfRangeException( "The time must be positive.", default(Exception) ); } if (INTERNAL_inactiveSleepTime != value) { INTERNAL_inactiveSleepTime = value; } } } private bool INTERNAL_isActive; public bool IsActive { get { return INTERNAL_isActive; } internal set { if (INTERNAL_isActive != value) { INTERNAL_isActive = value; if (INTERNAL_isActive) { OnActivated(this, EventArgs.Empty); } else { OnDeactivated(this, EventArgs.Empty); } } } } public bool IsFixedTimeStep { get; set; } private bool INTERNAL_isMouseVisible; public bool IsMouseVisible { get { return INTERNAL_isMouseVisible; } set { if (INTERNAL_isMouseVisible != value) { INTERNAL_isMouseVisible = value; FNAPlatform.OnIsMouseVisibleChanged(value); } } } public LaunchParameters LaunchParameters { get; private set; } private TimeSpan INTERNAL_targetElapsedTime; public TimeSpan TargetElapsedTime { get { return INTERNAL_targetElapsedTime; } set { if (value <= TimeSpan.Zero) { throw new ArgumentOutOfRangeException( "The time must be positive and non-zero.", default(Exception) ); } INTERNAL_targetElapsedTime = value; } } public GameServiceContainer Services { get; private set; } public GameWindow Window { get; private set; } #endregion #region Internal Variables internal bool RunApplication; #endregion #region Private Variables /* You will notice that these lists have some locks on them in the code. * Technically this is not accurate to XNA4, as they just happily crash * whenever there's an Add/Remove happening mid-copy. * * But do you really think I want to get reports about that crap? * -flibit */ private List updateableComponents; private List currentlyUpdatingComponents; private List drawableComponents; private List currentlyDrawingComponents; private IGraphicsDeviceService graphicsDeviceService; private IGraphicsDeviceManager graphicsDeviceManager; private GraphicsAdapter currentAdapter; private bool hasInitialized; private bool suppressDraw; private bool isDisposed; private readonly GameTime gameTime; private Stopwatch gameTimer; private TimeSpan accumulatedElapsedTime; private long previousTicks = 0; private int updateFrameLag; private bool forceElapsedTimeToZero = false; // must be a power of 2 so we can do a bitmask optimization when checking worst case private const int PREVIOUS_SLEEP_TIME_COUNT = 128; private const int SLEEP_TIME_MASK = PREVIOUS_SLEEP_TIME_COUNT - 1; private TimeSpan[] previousSleepTimes = new TimeSpan[PREVIOUS_SLEEP_TIME_COUNT]; private int sleepTimeIndex = 0; private TimeSpan worstCaseSleepPrecision = TimeSpan.FromMilliseconds(1); private static readonly TimeSpan MaxElapsedTime = TimeSpan.FromMilliseconds(500); private bool[] textInputControlDown; private bool textInputSuppress; #endregion #region Events public event EventHandler Activated; public event EventHandler Deactivated; public event EventHandler Disposed; public event EventHandler Exiting; #endregion #region Public Constructor public Game() { AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; LaunchParameters = new LaunchParameters(); Components = new GameComponentCollection(); Services = new GameServiceContainer(); Content = new ContentManager(Services); updateableComponents = new List(); currentlyUpdatingComponents = new List(); drawableComponents = new List(); currentlyDrawingComponents = new List(); IsMouseVisible = false; IsFixedTimeStep = true; TargetElapsedTime = TimeSpan.FromTicks(166667); // 60fps InactiveSleepTime = TimeSpan.FromSeconds(0.02); for (int i = 0; i < previousSleepTimes.Length; i += 1) { previousSleepTimes[i] = TimeSpan.FromMilliseconds(1); } textInputControlDown = new bool[FNAPlatform.TextInputCharacters.Length]; hasInitialized = false; suppressDraw = false; isDisposed = false; gameTime = new GameTime(); Window = FNAPlatform.CreateWindow(); Mouse.WindowHandle = Window.Handle; TouchPanel.WindowHandle = Window.Handle; FrameworkDispatcher.Update(); // Ready to run the loop! RunApplication = true; } #endregion #region Destructor ~Game() { Dispose(false); } #endregion #region IDisposable Implementation public void Dispose() { Dispose(true); GC.SuppressFinalize(this); if (Disposed != null) { Disposed(this, EventArgs.Empty); } } protected virtual void Dispose(bool disposing) { if (!isDisposed) { if (disposing) { // Dispose loaded game components. for (int i = 0; i < Components.Count; i += 1) { IDisposable disposable = Components[i] as IDisposable; if (disposable != null) { disposable.Dispose(); } } if (Content != null) { Content.Dispose(); } if (graphicsDeviceService != null) { // FIXME: Does XNA4 require the GDM to be disposable? -flibit (graphicsDeviceService as IDisposable).Dispose(); } if (Window != null) { FNAPlatform.DisposeWindow(Window); } ContentTypeReaderManager.ClearTypeCreators(); } AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException; isDisposed = true; } } [DebuggerNonUserCode] private void AssertNotDisposed() { if (isDisposed) { string name = GetType().Name; throw new ObjectDisposedException( name, string.Format( "The {0} object was used after being Disposed.", name ) ); } } #endregion #region Public Methods public void Exit() { RunApplication = false; suppressDraw = true; } public void ResetElapsedTime() { /* This only matters the next tick, and ONLY when * IsFixedTimeStep is false! * For fixed timestep, this is totally ignored. * -flibit */ if (!IsFixedTimeStep) { forceElapsedTimeToZero = true; } } public void SuppressDraw() { suppressDraw = true; } public void RunOneFrame() { if (!hasInitialized) { DoInitialize(); gameTimer = Stopwatch.StartNew(); hasInitialized = true; } Tick(); } public void Run() { AssertNotDisposed(); if (!hasInitialized) { DoInitialize(); hasInitialized = true; } BeginRun(); BeforeLoop(); gameTimer = Stopwatch.StartNew(); RunLoop(); EndRun(); AfterLoop(); } public void Tick() { /* NOTE: This code is very sensitive and can break very badly, * even with what looks like a safe change. Be sure to test * any change fully in both the fixed and variable timestep * modes across multiple devices and platforms. */ AdvanceElapsedTime(); if (IsFixedTimeStep) { /* If we are in fixed timestep, we want to wait until the next frame, * but we don't want to oversleep. Requesting repeated 1ms sleeps and * seeing how long we actually slept for lets us estimate the worst case * sleep precision so we don't oversleep the next frame. */ while (accumulatedElapsedTime + worstCaseSleepPrecision < TargetElapsedTime) { System.Threading.Thread.Sleep(1); TimeSpan timeAdvancedSinceSleeping = AdvanceElapsedTime(); UpdateEstimatedSleepPrecision(timeAdvancedSinceSleeping); } /* Now that we have slept into the sleep precision threshold, we need to wait * for just a little bit longer until the target elapsed time has been reached. * SpinWait(1) works by pausing the thread for very short intervals, so it is * an efficient and time-accurate way to wait out the rest of the time. */ while (accumulatedElapsedTime < TargetElapsedTime) { System.Threading.Thread.SpinWait(1); AdvanceElapsedTime(); } } // Now that we are going to perform an update, let's poll events. FNAPlatform.PollEvents( this, ref currentAdapter, textInputControlDown, ref textInputSuppress ); // Do not allow any update to take longer than our maximum. if (accumulatedElapsedTime > MaxElapsedTime) { accumulatedElapsedTime = MaxElapsedTime; } if (IsFixedTimeStep) { gameTime.ElapsedGameTime = TargetElapsedTime; int stepCount = 0; // Perform as many full fixed length time steps as we can. while (accumulatedElapsedTime >= TargetElapsedTime) { gameTime.TotalGameTime += TargetElapsedTime; accumulatedElapsedTime -= TargetElapsedTime; stepCount += 1; AssertNotDisposed(); Update(gameTime); } // Every update after the first accumulates lag updateFrameLag += Math.Max(0, stepCount - 1); /* If we think we are running slowly, wait * until the lag clears before resetting it */ if (gameTime.IsRunningSlowly) { if (updateFrameLag == 0) { gameTime.IsRunningSlowly = false; } } else if (updateFrameLag >= 5) { /* If we lag more than 5 frames, * start thinking we are running slowly. */ gameTime.IsRunningSlowly = true; } /* Every time we just do one update and one draw, * then we are not running slowly, so decrease the lag. */ if (stepCount == 1 && updateFrameLag > 0) { updateFrameLag -= 1; } /* Draw needs to know the total elapsed time * that occured for the fixed length updates. */ gameTime.ElapsedGameTime = TimeSpan.FromTicks(TargetElapsedTime.Ticks * stepCount); } else { // Perform a single variable length update. if (forceElapsedTimeToZero) { /* When ResetElapsedTime is called, * Elapsed is forced to zero and * Total is ignored entirely. * -flibit */ gameTime.ElapsedGameTime = TimeSpan.Zero; forceElapsedTimeToZero = false; } else { gameTime.ElapsedGameTime = accumulatedElapsedTime; gameTime.TotalGameTime += gameTime.ElapsedGameTime; } accumulatedElapsedTime = TimeSpan.Zero; AssertNotDisposed(); Update(gameTime); } // Draw unless the update suppressed it. if (suppressDraw) { suppressDraw = false; } else { /* Draw/EndDraw should not be called if BeginDraw returns false. * http://stackoverflow.com/questions/4054936/manual-control-over-when-to-redraw-the-screen/4057180#4057180 * http://stackoverflow.com/questions/4235439/xna-3-1-to-4-0-requires-constant-redraw-or-will-display-a-purple-screen */ if (BeginDraw()) { Draw(gameTime); EndDraw(); } } } #endregion #region Internal Methods internal void RedrawWindow() { /* Draw/EndDraw should not be called if BeginDraw returns false. * http://stackoverflow.com/questions/4054936/manual-control-over-when-to-redraw-the-screen/4057180#4057180 * http://stackoverflow.com/questions/4235439/xna-3-1-to-4-0-requires-constant-redraw-or-will-display-a-purple-screen * * Additionally, if we haven't even started yet, be quiet until we have! * -flibit */ if (gameTime.TotalGameTime != TimeSpan.Zero && BeginDraw()) { Draw(new GameTime(gameTime.TotalGameTime, TimeSpan.Zero)); EndDraw(); } } #endregion #region Protected Methods protected virtual bool BeginDraw() { if (graphicsDeviceManager != null) { return graphicsDeviceManager.BeginDraw(); } return true; } protected virtual void EndDraw() { if (graphicsDeviceManager != null) { graphicsDeviceManager.EndDraw(); } } protected virtual void BeginRun() { } protected virtual void EndRun() { } protected virtual void LoadContent() { } protected virtual void UnloadContent() { } protected virtual void Initialize() { /* According to the information given on MSDN, all GameComponents * in Components at the time Initialize() is called are initialized: * * http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.game.initialize.aspx * * Note, however, that we are NOT using a foreach. It's actually * possible to add something during initialization, and those must * also be initialized. There may be a safer way to account for it, * considering it may be possible to _remove_ components as well, * but for now, let's worry about initializing everything we get. * -flibit */ for (int i = 0; i < Components.Count; i += 1) { Components[i].Initialize(); } /* This seems like a condition that warrants a major * exception more than a silent failure, but for some * reason it's okay... but only sort of. You can get * away with initializing just before base.Initialize(), * but everything gets super broken on the IManager side * (IService doesn't seem to matter anywhere else). */ graphicsDeviceService = (IGraphicsDeviceService) Services.GetService(typeof(IGraphicsDeviceService)); if (graphicsDeviceService != null) { graphicsDeviceService.DeviceDisposing += (o, e) => UnloadContent(); if (graphicsDeviceService.GraphicsDevice != null) { LoadContent(); } else { graphicsDeviceService.DeviceCreated += (o, e) => LoadContent(); } } } protected virtual void Draw(GameTime gameTime) { lock (drawableComponents) { for (int i = 0; i < drawableComponents.Count; i += 1) { currentlyDrawingComponents.Add(drawableComponents[i]); } } foreach (IDrawable drawable in currentlyDrawingComponents) { if (drawable.Visible) { drawable.Draw(gameTime); } } currentlyDrawingComponents.Clear(); } protected virtual void Update(GameTime gameTime) { lock (updateableComponents) { for (int i = 0; i < updateableComponents.Count; i += 1) { currentlyUpdatingComponents.Add(updateableComponents[i]); } } foreach (IUpdateable updateable in currentlyUpdatingComponents) { if (updateable.Enabled) { updateable.Update(gameTime); } } currentlyUpdatingComponents.Clear(); FrameworkDispatcher.Update(); } protected virtual void OnExiting(object sender, EventArgs args) { if (Exiting != null) { Exiting(this, args); } } protected virtual void OnActivated(object sender, EventArgs args) { AssertNotDisposed(); if (Activated != null) { Activated(this, args); } } protected virtual void OnDeactivated(object sender, EventArgs args) { AssertNotDisposed(); if (Deactivated != null) { Deactivated(this, args); } } protected virtual bool ShowMissingRequirementMessage(Exception exception) { if (exception is NoAudioHardwareException) { FNAPlatform.ShowRuntimeError( Window.Title, "Could not find a suitable audio device. " + " Verify that a sound card is\ninstalled," + " and check the driver properties to make" + " sure it is not disabled." ); return true; } if (exception is NoSuitableGraphicsDeviceException) { FNAPlatform.ShowRuntimeError( Window.Title, "Could not find a suitable graphics device." + " More information:\n\n" + exception.Message ); return true; } return false; } #endregion #region Private Methods private void DoInitialize() { AssertNotDisposed(); /* If this is late, you can still create it yourself. * In fact, you can even go as far as creating the * _manager_ before base.Initialize(), but Begin/EndDraw * will not get called. Just... please, make the service * before calling Run(). */ graphicsDeviceManager = (IGraphicsDeviceManager) Services.GetService(typeof(IGraphicsDeviceManager)); if (graphicsDeviceManager != null) { graphicsDeviceManager.CreateDevice(); } Initialize(); /* We need to do this after virtual Initialize(...) is called. * 1. Categorize components into IUpdateable and IDrawable lists. * 2. Subscribe to Added/Removed events to keep the categorized * lists synced and to Initialize future components as they are * added. */ updateableComponents.Clear(); drawableComponents.Clear(); for (int i = 0; i < Components.Count; i += 1) { CategorizeComponent(Components[i]); } Components.ComponentAdded += OnComponentAdded; Components.ComponentRemoved += OnComponentRemoved; } private void CategorizeComponent(IGameComponent component) { IUpdateable updateable = component as IUpdateable; if (updateable != null) { lock (updateableComponents) { SortUpdateable(updateable); } updateable.UpdateOrderChanged += OnUpdateOrderChanged; } IDrawable drawable = component as IDrawable; if (drawable != null) { lock (drawableComponents) { SortDrawable(drawable); } drawable.DrawOrderChanged += OnDrawOrderChanged; } } private void SortUpdateable(IUpdateable updateable) { for (int i = 0; i < updateableComponents.Count; i += 1) { if (updateable.UpdateOrder < updateableComponents[i].UpdateOrder) { updateableComponents.Insert(i, updateable); return; } } updateableComponents.Add(updateable); } private void SortDrawable(IDrawable drawable) { for (int i = 0; i < drawableComponents.Count; i += 1) { if (drawable.DrawOrder < drawableComponents[i].DrawOrder) { drawableComponents.Insert(i, drawable); return; } } drawableComponents.Add(drawable); } private void BeforeLoop() { currentAdapter = FNAPlatform.RegisterGame(this); IsActive = true; // Perform initial check for a touch device TouchPanel.TouchDeviceExists = FNAPlatform.GetTouchCapabilities().IsConnected; } private void AfterLoop() { FNAPlatform.UnregisterGame(this); } private void RunLoop() { /* Some platforms (i.e. Emscripten) don't support * indefinite while loops, so instead we have to * surrender control to the platform's main loop. * -caleb */ if (FNAPlatform.NeedsPlatformMainLoop()) { /* This breaks control flow and jumps * directly into the platform main loop. * Nothing below this call will be executed. */ FNAPlatform.RunPlatformMainLoop(this); } while (RunApplication) { Tick(); } OnExiting(this, EventArgs.Empty); } private TimeSpan AdvanceElapsedTime() { long currentTicks = gameTimer.Elapsed.Ticks; TimeSpan timeAdvanced = TimeSpan.FromTicks(currentTicks - previousTicks); accumulatedElapsedTime += timeAdvanced; previousTicks = currentTicks; return timeAdvanced; } /* To calculate the sleep precision of the OS, we take the worst case * time spent sleeping over the results of previous requests to sleep 1ms. */ private void UpdateEstimatedSleepPrecision(TimeSpan timeSpentSleeping) { /* It is unlikely that the scheduler will actually be more imprecise than * 4ms and we don't want to get wrecked by a single long sleep so we cap this * value at 4ms for sanity. */ TimeSpan upperTimeBound = TimeSpan.FromMilliseconds(4); if (timeSpentSleeping > upperTimeBound) { timeSpentSleeping = upperTimeBound; } /* We know the previous worst case - it's saved in worstCaseSleepPrecision. * We also know the current index. So the only way the worst case changes * is if we either 1) just got a new worst case, or 2) the worst case was * the oldest entry on the list. */ if (timeSpentSleeping >= worstCaseSleepPrecision) { worstCaseSleepPrecision = timeSpentSleeping; } else if (previousSleepTimes[sleepTimeIndex] == worstCaseSleepPrecision) { TimeSpan maxSleepTime = TimeSpan.MinValue; for (int i = 0; i < previousSleepTimes.Length; i += 1) { if (previousSleepTimes[i] > maxSleepTime) { maxSleepTime = previousSleepTimes[i]; } } worstCaseSleepPrecision = maxSleepTime; } previousSleepTimes[sleepTimeIndex] = timeSpentSleeping; sleepTimeIndex = (sleepTimeIndex + 1) & SLEEP_TIME_MASK; } #endregion #region Private Event Handlers private void OnComponentAdded( object sender, GameComponentCollectionEventArgs e ) { /* Since we only subscribe to ComponentAdded after the graphics * devices are set up, it is safe to just blindly call Initialize. */ e.GameComponent.Initialize(); CategorizeComponent(e.GameComponent); } private void OnComponentRemoved( object sender, GameComponentCollectionEventArgs e ) { IUpdateable updateable = e.GameComponent as IUpdateable; if (updateable != null) { lock (updateableComponents) { updateableComponents.Remove(updateable); } updateable.UpdateOrderChanged -= OnUpdateOrderChanged; } IDrawable drawable = e.GameComponent as IDrawable; if (drawable != null) { lock (drawableComponents) { drawableComponents.Remove(drawable); } drawable.DrawOrderChanged -= OnDrawOrderChanged; } } private void OnUpdateOrderChanged(object sender, EventArgs e) { // FIXME: Is there a better way to re-sort one item? -flibit IUpdateable updateable = sender as IUpdateable; lock (updateableComponents) { updateableComponents.Remove(updateable); SortUpdateable(updateable); } } private void OnDrawOrderChanged(object sender, EventArgs e) { // FIXME: Is there a better way to re-sort one item? -flibit IDrawable drawable = sender as IDrawable; lock (drawableComponents) { drawableComponents.Remove(drawable); SortDrawable(drawable); } } private void OnUnhandledException( object sender, UnhandledExceptionEventArgs args ) { ShowMissingRequirementMessage(args.ExceptionObject as Exception); } #endregion } }