#region License /* FNA - XNA4 Reimplementation for Desktop Platforms * Copyright 2009-2020 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 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; private static readonly TimeSpan MaxElapsedTime = TimeSpan.FromMilliseconds(500); #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); 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 Deconstructor ~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; } BeginRun(); // FIXME: Not quite right.. Tick(); EndRun(); } public void Run() { AssertNotDisposed(); if (!hasInitialized) { DoInitialize(); hasInitialized = true; } BeginRun(); gameTimer = Stopwatch.StartNew(); FNAPlatform.RunLoop(this); EndRun(); OnExiting(this, EventArgs.Empty); } 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. */ RetryTick: // Advance the accumulated elapsed time. long currentTicks = gameTimer.Elapsed.Ticks; accumulatedElapsedTime += TimeSpan.FromTicks(currentTicks - previousTicks); previousTicks = currentTicks; /* If we're in the fixed timestep mode and not enough time has elapsed * to perform an update we sleep off the the remaining time to save battery * life and/or release CPU time to other threads and processes. */ if (IsFixedTimeStep && accumulatedElapsedTime < TargetElapsedTime) { int sleepTime = ( (int)(TargetElapsedTime - accumulatedElapsedTime).TotalMilliseconds ); /* NOTE: While sleep can be inaccurate in general it is * accurate enough for frame limiting purposes if some * fluctuation is an acceptable result. */ System.Threading.Thread.Sleep(sleepTime); goto RetryTick; } // 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.GraphicsDevice != null) { graphicsDeviceService.DeviceDisposing += (o, e) => UnloadContent(); 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); } #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 } }