MonoGame.Framework/Game.cs
Rama develop · réplica de lectura del repositorio original.
// MonoGame - Copyright (C) MonoGame Foundation, Inc
// This file is subject to the terms and conditions defined in
// file 'LICENSE.txt', which is part of this source code package.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input.Touch;
namespace Microsoft.Xna.Framework
{
/// <summary>
/// This class is the entry point for most games. Handles setting up
/// a window and graphics and runs a game loop that calls <see cref="Update"/> and <see cref="Draw"/>.
/// </summary>
public partial class Game : IDisposable
{
private GameComponentCollection _components;
private GameServiceContainer _services;
private ContentManager _content;
internal GamePlatform Platform;
private SortingFilteringCollection<IDrawable> _drawables =
new SortingFilteringCollection<IDrawable>(
d => d.Visible,
(d, handler) => d.VisibleChanged += handler,
(d, handler) => d.VisibleChanged -= handler,
(d1 ,d2) => Comparer<int>.Default.Compare(d1.DrawOrder, d2.DrawOrder),
(d, handler) => d.DrawOrderChanged += handler,
(d, handler) => d.DrawOrderChanged -= handler);
private SortingFilteringCollection<IUpdateable> _updateables =
new SortingFilteringCollection<IUpdateable>(
u => u.Enabled,
(u, handler) => u.EnabledChanged += handler,
(u, handler) => u.EnabledChanged -= handler,
(u1, u2) => Comparer<int>.Default.Compare(u1.UpdateOrder, u2.UpdateOrder),
(u, handler) => u.UpdateOrderChanged += handler,
(u, handler) => u.UpdateOrderChanged -= handler);
private IGraphicsDeviceManager _graphicsDeviceManager;
private IGraphicsDeviceService _graphicsDeviceService;
private bool _initialized = false;
private bool _isFixedTimeStep = true;
private TimeSpan _targetElapsedTime = TimeSpan.FromTicks(166667); // 60fps
private TimeSpan _inactiveSleepTime = TimeSpan.FromSeconds(0.02);
private TimeSpan _maxElapsedTime = TimeSpan.FromMilliseconds(500);
private bool _shouldExit;
private bool _suppressDraw;
partial void PlatformConstruct();
/// <summary>
/// Create a <see cref="Game"/>.
/// </summary>
public Game()
{
_instance = this;
LaunchParameters = new LaunchParameters();
_services = new GameServiceContainer();
_components = new GameComponentCollection();
_content = new ContentManager(_services);
Platform = GamePlatform.PlatformCreate(this);
Platform.Activated += OnActivated;
Platform.Deactivated += OnDeactivated;
_services.AddService(typeof(GamePlatform), Platform);
// Calling Update() for first time initializes some systems
FrameworkDispatcher.Update();
// Allow some optional per-platform construction to occur too.
PlatformConstruct();
}
/// <summary/>
~Game()
{
Dispose(false);
}
[System.Diagnostics.Conditional("DEBUG")]
internal void Log(string Message)
{
if (Platform != null) Platform.Log(Message);
}
#region IDisposable Implementation
private bool _isDisposed;
/// <inheritdoc cref="IDisposable.Dispose()"/>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
EventHelpers.Raise(this, Disposed, EventArgs.Empty);
}
/// <summary/>
protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
// Dispose loaded game components
for (int i = 0; i < _components.Count; i++)
{
var disposable = _components[i] as IDisposable;
if (disposable != null)
disposable.Dispose();
}
_components.ComponentAdded -= Components_ComponentAdded;
_components.ComponentRemoved -= Components_ComponentRemoved;
_components = null;
if (_content != null)
{
_content.Dispose();
_content = null;
}
if (_graphicsDeviceManager != null)
{
(_graphicsDeviceManager as GraphicsDeviceManager).Dispose();
_graphicsDeviceManager = null;
}
if (Platform != null)
{
Platform.Activated -= OnActivated;
Platform.Deactivated -= OnDeactivated;
_services.RemoveService(typeof(GamePlatform));
Platform.Dispose();
Platform = null;
}
ContentTypeReaderManager.ClearTypeCreators();
if (SoundEffect._systemState == SoundEffect.SoundSystemState.Initialized)
{
SoundEffect.Shutdown();
}
}
#if ANDROID
Activity = null;
#endif
_isDisposed = true;
_instance = null;
}
}
[System.Diagnostics.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 IDisposable Implementation
#region Properties
#if ANDROID
public static AndroidGameActivity Activity { get; internal set; }
#endif
private static Game _instance = null;
internal static Game Instance { get { return Game._instance; } }
/// <summary>
/// The start up parameters for this <see cref="Game"/>.
/// </summary>
public LaunchParameters LaunchParameters { get; private set; }
/// <summary>
/// A collection of game components attached to this <see cref="Game"/>.
/// </summary>
public GameComponentCollection Components
{
get { return _components; }
}
/// <summary>
/// Gets or sets time to sleep between frames when the game is not active
/// </summary>
public TimeSpan InactiveSleepTime
{
get { return _inactiveSleepTime; }
set
{
if (value < TimeSpan.Zero)
throw new ArgumentOutOfRangeException("The time must be positive.");
_inactiveSleepTime = value;
}
}
/// <summary>
/// The maximum amount of time we will frameskip over and only perform Update calls with no Draw calls.
/// MonoGame extension.
/// </summary>
public TimeSpan MaxElapsedTime
{
get { return _maxElapsedTime; }
set
{
if (value < TimeSpan.Zero)
throw new ArgumentOutOfRangeException(
"The time must be positive.");
if (value < _targetElapsedTime)
throw new ArgumentOutOfRangeException(
"The time must be at least TargetElapsedTime");
_maxElapsedTime = value;
}
}
/// <summary>
/// Indicates if the game is the focused application.
/// </summary>
public bool IsActive
{
get { return Platform.IsActive; }
}
/// <summary>
/// Indicates if the mouse cursor is visible on the game screen.
/// </summary>
public bool IsMouseVisible
{
get { return Platform.IsMouseVisible; }
set { Platform.IsMouseVisible = value; }
}
/// <summary>
/// The time between frames when running with a fixed time step. <seealso cref="IsFixedTimeStep"/>
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">Target elapsed time must be strictly larger than zero.</exception>
public TimeSpan TargetElapsedTime
{
get { return _targetElapsedTime; }
set
{
// Give GamePlatform implementations an opportunity to override
// the new value.
value = Platform.TargetElapsedTimeChanging(value);
if (value <= TimeSpan.Zero)
throw new ArgumentOutOfRangeException(
"The time must be positive and non-zero.");
if (value > _maxElapsedTime)
throw new ArgumentOutOfRangeException(
"The time can not be larger than MaxElapsedTime");
if (value != _targetElapsedTime)
{
_targetElapsedTime = value;
Platform.TargetElapsedTimeChanged();
}
}
}
/// <summary>
/// Indicates if this game is running with a fixed time between frames.
///
/// When set to <code>true</code> the target time between frames is
/// given by <see cref="TargetElapsedTime"/>.
/// </summary>
public bool IsFixedTimeStep
{
get { return _isFixedTimeStep; }
set { _isFixedTimeStep = value; }
}
/// <summary>
/// Get a container holding service providers attached to this <see cref="Game"/>.
/// </summary>
public GameServiceContainer Services {
get { return _services; }
}
/// <summary>
/// The <see cref="ContentManager"/> of this <see cref="Game"/>.
/// </summary>
/// <exception cref="ArgumentNullException">If Content is set to <code>null</code>.</exception>
public ContentManager Content
{
get { return _content; }
set
{
if (value == null)
throw new ArgumentNullException();
_content = value;
}
}
/// <summary>
/// Gets the <see cref="GraphicsDevice"/> used for rendering by this <see cref="Game"/>.
/// </summary>
/// <exception cref="InvalidOperationException">
/// There is no <see cref="Graphics.GraphicsDevice"/> attached to this <see cref="Game"/>.
/// </exception>
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;
}
}
/// <summary>
/// The system window that this game is displayed on.
/// </summary>
public GameWindow Window
{
get { return Platform.Window; }
}
#endregion Properties
#region Internal Properties
// FIXME: Internal members should be eliminated.
// Currently Game.Initialized is used by the Mac game window class to
// determine whether to raise DeviceResetting and DeviceReset on
// GraphicsDeviceManager.
internal bool Initialized
{
get { return _initialized; }
}
#endregion Internal Properties
#region Events
/// <summary>
/// Raised when the game gains focus.
/// </summary>
public event EventHandler<EventArgs> Activated;
/// <summary>
/// Raised when the game loses focus.
/// </summary>
public event EventHandler<EventArgs> Deactivated;
/// <summary>
/// Raised when this game is being disposed.
/// </summary>
public event EventHandler<EventArgs> Disposed;
/// <summary>
/// Raised when this game is exiting.
/// </summary>
public event EventHandler<ExitingEventArgs> Exiting;
#endregion
#region Public Methods
/// <summary>
/// Exit the game at the end of this tick.
/// </summary>
#if IOS
[Obsolete("This platform's policy does not allow programmatically closing.", true)]
#endif
public void Exit()
{
_shouldExit = true;
_suppressDraw = true;
}
/// <summary>
/// Reset the elapsed game time to <see cref="TimeSpan.Zero"/>.
/// </summary>
public void ResetElapsedTime()
{
Platform.ResetElapsedTime();
if (_gameTimer != null)
{
_gameTimer.Reset();
_gameTimer.Start();
}
_accumulatedElapsedTime = TimeSpan.Zero;
_gameTime.ElapsedGameTime = TimeSpan.Zero;
_previousTicks = 0L;
}
/// <summary>
/// Supress calling <see cref="Draw"/> in the game loop.
/// </summary>
public void SuppressDraw()
{
_suppressDraw = true;
}
/// <summary>
/// Run the game for one frame, then exit.
/// </summary>
public void RunOneFrame()
{
if (Platform == null)
return;
if (!Platform.BeforeRun())
return;
if (!_initialized)
{
DoInitialize ();
_gameTimer = Stopwatch.StartNew();
_initialized = true;
}
BeginRun();
//Not quite right..
Tick ();
EndRun ();
}
/// <summary>
/// Run the game using the default <see cref="GameRunBehavior"/> for the current platform.
/// </summary>
public void Run()
{
Run(Platform.DefaultRunBehavior);
}
/// <summary>
/// Run the game.
/// </summary>
/// <param name="runBehavior">Indicate if the game should be run synchronously or asynchronously.</param>
public void Run(GameRunBehavior runBehavior)
{
AssertNotDisposed();
if (!Platform.BeforeRun())
{
BeginRun();
_gameTimer = Stopwatch.StartNew();
return;
}
if (!_initialized) {
DoInitialize ();
_initialized = true;
}
BeginRun();
_gameTimer = Stopwatch.StartNew();
switch (runBehavior)
{
case GameRunBehavior.Asynchronous:
Platform.AsyncRunLoopEnded += Platform_AsyncRunLoopEnded;
Platform.StartRunLoop();
break;
case GameRunBehavior.Synchronous:
// XNA runs one Update even before showing the window
DoUpdate(new GameTime());
Platform.RunLoop();
break;
default:
throw new ArgumentException(string.Format(
"Handling for the run behavior {0} is not implemented.", runBehavior));
}
}
private TimeSpan _accumulatedElapsedTime;
private readonly GameTime _gameTime = new GameTime();
private Stopwatch _gameTimer;
private long _previousTicks = 0;
private int _updateFrameLag;
/// <summary>
/// Run one iteration of the game loop.
///
/// Makes at least one call to <see cref="Update"/>
/// and exactly one call to <see cref="Draw"/> if drawing is not supressed.
/// When <see cref="IsFixedTimeStep"/> is set to <code>false</code> this will
/// make exactly one call to <see cref="Update"/>.
/// </summary>
public void Tick()
{
// NOTE: This code is very sensitive and can break very badly
// with even 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:
if (!IsActive && (InactiveSleepTime.TotalMilliseconds >= 1.0))
{
System.Threading.Thread.Sleep((int)InactiveSleepTime.TotalMilliseconds);
}
// Advance the accumulated elapsed time.
if (_gameTimer == null)
{
_gameTimer = new Stopwatch();
_gameTimer.Start();
}
var currentTicks = _gameTimer.Elapsed.Ticks;
_accumulatedElapsedTime += TimeSpan.FromTicks(currentTicks - _previousTicks);
_previousTicks = currentTicks;
if (IsFixedTimeStep && _accumulatedElapsedTime < TargetElapsedTime)
{
// Sleep for as long as possible without overshooting the update time
var sleepTime = (TargetElapsedTime - _accumulatedElapsedTime).TotalMilliseconds;
// We only have a precision timer on Windows, so other platforms may still overshoot
#if WINDOWS && !DESKTOPGL
MonoGame.Framework.Utilities.TimerHelper.SleepForNoMoreThan(sleepTime);
#elif DESKTOPGL || ANDROID || IOS
if (sleepTime >= 2.0)
System.Threading.Thread.Sleep(1);
#endif
// Keep looping until it's time to perform the next update
goto RetryTick;
}
// Do not allow any update to take longer than our maximum.
if (_accumulatedElapsedTime > _maxElapsedTime)
_accumulatedElapsedTime = _maxElapsedTime;
if (IsFixedTimeStep)
{
_gameTime.ElapsedGameTime = TargetElapsedTime;
var stepCount = 0;
// Perform as many full fixed length time steps as we can.
while (_accumulatedElapsedTime >= TargetElapsedTime && !_shouldExit)
{
_gameTime.TotalGameTime += TargetElapsedTime;
_accumulatedElapsedTime -= TargetElapsedTime;
++stepCount;
DoUpdate(_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--;
// 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.
_gameTime.ElapsedGameTime = _accumulatedElapsedTime;
_gameTime.TotalGameTime += _accumulatedElapsedTime;
_accumulatedElapsedTime = TimeSpan.Zero;
DoUpdate(_gameTime);
}
// Draw unless the update suppressed it.
if (_suppressDraw)
_suppressDraw = false;
else
{
DoDraw(_gameTime);
}
if (_shouldExit)
{
var exitingEventArgs = new ExitingEventArgs();
OnExiting(this, exitingEventArgs);
if (!exitingEventArgs.Cancel)
{
UnloadContent();
Platform.Exit();
EndRun();
}
_shouldExit = false;
}
}
#endregion
#region Protected Methods
/// <summary>
/// Called right before <see cref="Draw"/> is normally called. Can return <code>false</code>
/// to let the game loop not call <see cref="Draw"/>.
/// </summary>
/// <returns>
/// <code>true</code> if <see cref="Draw"/> should be called, <code>false</code> if it should not.
/// </returns>
protected virtual bool BeginDraw() { return true; }
/// <summary>
/// Called right after <see cref="Draw"/>. Presents the
/// rendered frame in the <see cref="GameWindow"/>.
/// </summary>
protected virtual void EndDraw()
{
Platform.Present();
}
/// <summary>
/// Called after <see cref="Initialize"/>, but before the first call to <see cref="Update"/>.
/// </summary>
protected virtual void BeginRun() { }
/// <summary>
/// Called when the game loop has been terminated before exiting.
/// </summary>
protected virtual void EndRun() { }
/// <summary>
/// Override this to load graphical resources required by the game.
/// </summary>
protected virtual void LoadContent() { }
/// <summary>
/// Override this to unload graphical resources loaded by the game.
/// </summary>
protected virtual void UnloadContent() { }
/// <summary>
/// Override this to initialize the game and load any needed non-graphical resources.
///
/// Initializes attached <see cref="GameComponent"/> instances and calls <see cref="LoadContent"/>.
/// </summary>
protected virtual void Initialize()
{
// TODO: This should be removed once all platforms use the new GraphicsDeviceManager
#if !(WINDOWS && DIRECTX) && !NATIVE
applyChanges(graphicsDeviceManager);
#endif
// According to the information given on MSDN (see link below), 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
// Initialize all existing components
InitializeExistingComponents();
_graphicsDeviceService = (IGraphicsDeviceService)
Services.GetService(typeof(IGraphicsDeviceService));
if (_graphicsDeviceService != null &&
_graphicsDeviceService.GraphicsDevice != null)
{
LoadContent();
}
}
private static readonly Action<IDrawable, GameTime> DrawAction =
(drawable, gameTime) => drawable.Draw(gameTime);
/// <summary>
/// Called when the game should draw a frame.
///
/// Draws the <see cref="DrawableGameComponent"/> instances attached to this game.
/// Override this to render your game.
/// </summary>
/// <param name="gameTime">A <see cref="GameTime"/> instance containing the elapsed time since the last call to <see cref="Draw"/> and the total time elapsed since the game started.</param>
protected virtual void Draw(GameTime gameTime)
{
_drawables.ForEachFilteredItem(DrawAction, gameTime);
}
private static readonly Action<IUpdateable, GameTime> UpdateAction =
(updateable, gameTime) => updateable.Update(gameTime);
/// <summary>
/// Called when the game should update.
///
/// Updates the <see cref="GameComponent"/> instances attached to this game.
/// Override this to update your game.
/// </summary>
/// <param name="gameTime">The elapsed time since the last call to <see cref="Update"/>.</param>
protected virtual void Update(GameTime gameTime)
{
_updateables.ForEachFilteredItem(UpdateAction, gameTime);
}
/// <summary>
/// Called when the game is exiting. Raises the <see cref="Exiting"/> event.
/// </summary>
/// <param name="sender">This <see cref="Game"/>.</param>
/// <param name="args">The arguments to the <see cref="Exiting"/> event.</param>
protected virtual void OnExiting(object sender, ExitingEventArgs args)
{
EventHelpers.Raise(sender, Exiting, args);
}
/// <summary>
/// Called when the game gains focus. Raises the <see cref="Activated"/> event.
/// </summary>
/// <param name="sender">This <see cref="Game"/>.</param>
/// <param name="args">The arguments to the <see cref="Activated"/> event.</param>
protected virtual void OnActivated (object sender, EventArgs args)
{
AssertNotDisposed();
EventHelpers.Raise(sender, Activated, args);
}
/// <summary>
/// Called when the game loses focus. Raises the <see cref="Deactivated"/> event.
/// </summary>
/// <param name="sender">This <see cref="Game"/>.</param>
/// <param name="args">The arguments to the <see cref="Deactivated"/> event.</param>
protected virtual void OnDeactivated (object sender, EventArgs args)
{
AssertNotDisposed();
EventHelpers.Raise(sender, Deactivated, args);
}
#endregion Protected Methods
#region Event Handlers
private void Components_ComponentAdded(
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 Components_ComponentRemoved(
object sender, GameComponentCollectionEventArgs e)
{
DecategorizeComponent(e.GameComponent);
}
private void Platform_AsyncRunLoopEnded(object sender, EventArgs e)
{
AssertNotDisposed();
var platform = (GamePlatform)sender;
platform.AsyncRunLoopEnded -= Platform_AsyncRunLoopEnded;
}
#endregion Event Handlers
#region Internal Methods
// FIXME: We should work toward eliminating internal methods. They
// break entirely the possibility that additional platforms could
// be added by third parties without changing MonoGame itself.
#if !(WINDOWS && DIRECTX) && !NATIVE
internal void applyChanges(GraphicsDeviceManager manager)
{
Platform.BeginScreenDeviceChange(GraphicsDevice.PresentationParameters.IsFullScreen);
if (GraphicsDevice.PresentationParameters.IsFullScreen)
Platform.EnterFullScreen();
else
Platform.ExitFullScreen();
var viewport = new Viewport(0, 0,
GraphicsDevice.PresentationParameters.BackBufferWidth,
GraphicsDevice.PresentationParameters.BackBufferHeight);
GraphicsDevice.Viewport = viewport;
Platform.EndScreenDeviceChange(string.Empty, viewport.Width, viewport.Height);
}
#endif
internal void DoUpdate(GameTime gameTime)
{
AssertNotDisposed();
if (Platform.BeforeUpdate(gameTime))
{
FrameworkDispatcher.Update();
Update(gameTime);
//The TouchPanel needs to know the time for when touches arrive
TouchPanelState.CurrentTimestamp = gameTime.TotalGameTime;
}
}
internal void DoDraw(GameTime gameTime)
{
AssertNotDisposed();
// Draw and 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 (Platform.BeforeDraw(gameTime) && BeginDraw())
{
Draw(gameTime);
EndDraw();
}
}
internal void DoInitialize()
{
AssertNotDisposed();
if (GraphicsDevice == null && graphicsDeviceManager != null)
_graphicsDeviceManager.CreateDevice();
Platform.BeforeInitialize();
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.
CategorizeComponents();
_components.ComponentAdded += Components_ComponentAdded;
_components.ComponentRemoved += Components_ComponentRemoved;
}
#endregion Internal Methods
internal GraphicsDeviceManager graphicsDeviceManager
{
get
{
if (_graphicsDeviceManager == null)
{
_graphicsDeviceManager = (IGraphicsDeviceManager)
Services.GetService(typeof(IGraphicsDeviceManager));
}
return (GraphicsDeviceManager)_graphicsDeviceManager;
}
set
{
if (_graphicsDeviceManager != null)
throw new InvalidOperationException("GraphicsDeviceManager already registered for this Game object");
_graphicsDeviceManager = value;
}
}
// NOTE: InitializeExistingComponents really should only be called once.
// Game.Initialize is the only method in a position to guarantee
// that no component will get a duplicate Initialize call.
// Further calls to Initialize occur immediately in response to
// Components.ComponentAdded.
private void InitializeExistingComponents()
{
for(int i = 0; i < Components.Count; ++i)
Components[i].Initialize();
}
private void CategorizeComponents()
{
DecategorizeComponents();
for (int i = 0; i < Components.Count; ++i)
CategorizeComponent(Components[i]);
}
// FIXME: I am open to a better name for this method. It does the
// opposite of CategorizeComponents.
private void DecategorizeComponents()
{
_updateables.Clear();
_drawables.Clear();
}
private void CategorizeComponent(IGameComponent component)
{
if (component is IUpdateable)
_updateables.Add((IUpdateable)component);
if (component is IDrawable)
_drawables.Add((IDrawable)component);
}
// FIXME: I am open to a better name for this method. It does the
// opposite of CategorizeComponent.
private void DecategorizeComponent(IGameComponent component)
{
if (component is IUpdateable)
_updateables.Remove((IUpdateable)component);
if (component is IDrawable)
_drawables.Remove((IDrawable)component);
}
/// <summary>
/// The SortingFilteringCollection class provides efficient, reusable
/// sorting and filtering based on a configurable sort comparer, filter
/// predicate, and associate change events.
/// </summary>
class SortingFilteringCollection<T> : ICollection<T>
{
private readonly List<T> _items;
private readonly List<AddJournalEntry<T>> _addJournal;
private readonly Comparison<AddJournalEntry<T>> _addJournalSortComparison;
private readonly List<int> _removeJournal;
private readonly List<T> _cachedFilteredItems;
private bool _shouldRebuildCache;
private readonly Predicate<T> _filter;
private readonly Comparison<T> _sort;
private readonly Action<T, EventHandler<EventArgs>> _filterChangedSubscriber;
private readonly Action<T, EventHandler<EventArgs>> _filterChangedUnsubscriber;
private readonly Action<T, EventHandler<EventArgs>> _sortChangedSubscriber;
private readonly Action<T, EventHandler<EventArgs>> _sortChangedUnsubscriber;
public SortingFilteringCollection(
Predicate<T> filter,
Action<T, EventHandler<EventArgs>> filterChangedSubscriber,
Action<T, EventHandler<EventArgs>> filterChangedUnsubscriber,
Comparison<T> sort,
Action<T, EventHandler<EventArgs>> sortChangedSubscriber,
Action<T, EventHandler<EventArgs>> sortChangedUnsubscriber)
{
_items = new List<T>();
_addJournal = new List<AddJournalEntry<T>>();
_removeJournal = new List<int>();
_cachedFilteredItems = new List<T>();
_shouldRebuildCache = true;
_filter = filter;
_filterChangedSubscriber = filterChangedSubscriber;
_filterChangedUnsubscriber = filterChangedUnsubscriber;
_sort = sort;
_sortChangedSubscriber = sortChangedSubscriber;
_sortChangedUnsubscriber = sortChangedUnsubscriber;
_addJournalSortComparison = CompareAddJournalEntry;
}
private int CompareAddJournalEntry(AddJournalEntry<T> x, AddJournalEntry<T> y)
{
int result = _sort(x.Item, y.Item);
if (result != 0)
return result;
return x.Order - y.Order;
}
public void ForEachFilteredItem<TUserData>(Action<T, TUserData> action, TUserData userData)
{
if (_shouldRebuildCache)
{
ProcessRemoveJournal();
ProcessAddJournal();
// Rebuild the cache
_cachedFilteredItems.Clear();
for (int i = 0; i < _items.Count; ++i)
if (_filter(_items[i]))
_cachedFilteredItems.Add(_items[i]);
_shouldRebuildCache = false;
}
for (int i = 0; i < _cachedFilteredItems.Count; ++i)
action(_cachedFilteredItems[i], userData);
// If the cache was invalidated as a result of processing items,
// now is a good time to clear it and give the GC (more of) a
// chance to do its thing.
if (_shouldRebuildCache)
_cachedFilteredItems.Clear();
}
public void Add(T item)
{
// NOTE: We subscribe to item events after items in _addJournal
// have been merged.
_addJournal.Add(new AddJournalEntry<T>(_addJournal.Count, item));
InvalidateCache();
}
public bool Remove(T item)
{
if (_addJournal.Remove(AddJournalEntry<T>.CreateKey(item)))
return true;
var index = _items.IndexOf(item);
if (index >= 0)
{
UnsubscribeFromItemEvents(item);
_removeJournal.Add(index);
InvalidateCache();
return true;
}
return false;
}
public void Clear()
{
for (int i = 0; i < _items.Count; ++i)
{
_filterChangedUnsubscriber(_items[i], Item_FilterPropertyChanged);
_sortChangedUnsubscriber(_items[i], Item_SortPropertyChanged);
}
_addJournal.Clear();
_removeJournal.Clear();
_items.Clear();
InvalidateCache();
}
public bool Contains(T item)
{
return _items.Contains(item);
}
public void CopyTo(T[] array, int arrayIndex)
{
_items.CopyTo(array, arrayIndex);
}
public int Count
{
get { return _items.Count; }
}
public bool IsReadOnly
{
get { return false; }
}
public IEnumerator<T> GetEnumerator()
{
return _items.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return ((System.Collections.IEnumerable)_items).GetEnumerator();
}
private static readonly Comparison<int> RemoveJournalSortComparison =
(x, y) => Comparer<int>.Default.Compare(y, x); // Sort high to low
private void ProcessRemoveJournal()
{
if (_removeJournal.Count == 0)
return;
// Remove items in reverse. (Technically there exist faster
// ways to bulk-remove from a variable-length array, but List<T>
// does not provide such a method.)
_removeJournal.Sort(RemoveJournalSortComparison);
for (int i = 0; i < _removeJournal.Count; ++i)
_items.RemoveAt(_removeJournal[i]);
_removeJournal.Clear();
}
private void ProcessAddJournal()
{
if (_addJournal.Count == 0)
return;
// Prepare the _addJournal to be merge-sorted with _items.
// _items is already sorted (because it is always sorted).
_addJournal.Sort(_addJournalSortComparison);
int iAddJournal = 0;
int iItems = 0;
while (iItems < _items.Count && iAddJournal < _addJournal.Count)
{
var addJournalItem = _addJournal[iAddJournal].Item;
// If addJournalItem is less than (belongs before)
// _items[iItems], insert it.
if (_sort(addJournalItem, _items[iItems]) < 0)
{
SubscribeToItemEvents(addJournalItem);
_items.Insert(iItems, addJournalItem);
++iAddJournal;
}
// Always increment iItems, either because we inserted and
// need to move past the insertion, or because we didn't
// insert and need to consider the next element.
++iItems;
}
// If _addJournal had any "tail" items, append them all now.
for (; iAddJournal < _addJournal.Count; ++iAddJournal)
{
var addJournalItem = _addJournal[iAddJournal].Item;
SubscribeToItemEvents(addJournalItem);
_items.Add(addJournalItem);
}
_addJournal.Clear();
}
private void SubscribeToItemEvents(T item)
{
_filterChangedSubscriber(item, Item_FilterPropertyChanged);
_sortChangedSubscriber(item, Item_SortPropertyChanged);
}
private void UnsubscribeFromItemEvents(T item)
{
_filterChangedUnsubscriber(item, Item_FilterPropertyChanged);
_sortChangedUnsubscriber(item, Item_SortPropertyChanged);
}
private void InvalidateCache()
{
_shouldRebuildCache = true;
}
private void Item_FilterPropertyChanged(object sender, EventArgs e)
{
InvalidateCache();
}
private void Item_SortPropertyChanged(object sender, EventArgs e)
{
var item = (T)sender;
var index = _items.IndexOf(item);
_addJournal.Add(new AddJournalEntry<T>(_addJournal.Count, item));
_removeJournal.Add(index);
// Until the item is back in place, we don't care about its
// events. We will re-subscribe when _addJournal is processed.
UnsubscribeFromItemEvents(item);
InvalidateCache();
}
}
private struct AddJournalEntry<T>
{
public readonly int Order;
public readonly T Item;
public AddJournalEntry(int order, T item)
{
Order = order;
Item = item;
}
public static AddJournalEntry<T> CreateKey(T item)
{
return new AddJournalEntry<T>(-1, item);
}
public override int GetHashCode()
{
return Item.GetHashCode();
}
public override bool Equals(object obj)
{
if (!(obj is AddJournalEntry<T>))
return false;
return object.Equals(Item, ((AddJournalEntry<T>)obj).Item);
}
}
}
}