Switching to a new asset manager system.

This commit is contained in:
Harrison Deng 2019-11-24 15:53:12 -05:00
parent 874e0d571d
commit 14ee906edd
9 changed files with 732 additions and 0 deletions

View File

@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
namespace RecrownedAthenaeum.Assets
{
/// <summary>
/// Wrapper for the content manager that helps with controlling it by adding automated multithreaded content loading.
/// </summary>
public class AssetManager
{
Thread thread;
readonly AssetManager contentManager;
readonly Queue<ContentData> queue;
Dictionary<string, Object> assets;
/// <summary>
/// Path modifiers to change the path in which the content manager looks to load a file. Used for better organizing things while not needing to type entire path.
/// </summary>
private readonly Dictionary<Type, IAssetPathResolver> contentPathModifier;
/// <summary>
/// Used when no path modifier is defined for that specific type.
/// </summary>
public IAssetPathResolver normalPathModifier = new NormalAssetPathResolver();
volatile float progress;
volatile bool running;
/// <summary>
/// Whether or not the queue is empty and all content is loaded.
/// </summary>
public bool Done { get { return !running && queue.Count == 0; } }
/// <summary>
/// The progress of the loading. 1 is complete while 0 is incomplete.
/// </summary>
public float Progress { get { return progress; } }
/// <summary>
/// Wraps the <see cref="ContentManager"/>.
/// </summary>
/// <param name="contentManager">The manager to wrap.</param>
public AssetManager()
{
assets = new Dictionary<string, Object>();
queue = new Queue<ContentData>();
contentPathModifier = new Dictionary<Type, IAssetPathResolver>();
}
/// <summary>
/// Adds a <see cref="IContentPathResolver"/> to this handler.
/// </summary>
/// <param name="assetType"></param>
/// <param name="contentResolver"></param>
public void AddContentPathResolver(Type assetType, IAssetPathResolver contentResolver) {
contentPathModifier.Add(assetType, contentResolver);
}
/// <summary>
/// Removes the <see cref="IContentPathResolver"/> for the key.
/// </summary>
/// <param name="assetType"></param>
public void RemoveContentResolver(Type assetType) {
contentPathModifier.Remove(assetType);
}
private void Load(string assetName, Type type, bool usePathModifier)
{
Debug.WriteLine("Loading asset: " + assetName);
string path = assetName;
if (usePathModifier)
{
IAssetPathResolver handler;
if (contentPathModifier.ContainsKey(type))
{
handler = contentPathModifier[type];
}
else
{
handler = normalPathModifier;
}
path = handler.Modify(assetName);
}
assets.Add(assetName, contentManager.Load<Object>(path));
}
/// <summary>
/// Gets the requested asset.
/// </summary>
/// <typeparam name="T">The type of the asset for an alternative way to cast.</typeparam>
/// <param name="assetName">The name of the asset.</param>
/// <returns>The asset casted to the type given with T.</returns>
public T Get<T>(string assetName)
{
lock (queue)
{
return (T)assets[assetName];
}
}
/// <summary>
/// Queues an asset to be loaded.
/// </summary>
/// <typeparam name="T">The type of the asset to be queued.</typeparam>
/// <param name="assetName">Name of asset to look for.</param>
/// <param name="usePathModifier">Whether or not to use the path modifiers.</param>
public void Queue<T>(string assetName, bool usePathModifier = true)
{
lock (queue)
{
if (!assets.ContainsKey(assetName))
{
queue.Enqueue(new ContentData(assetName, typeof(T), usePathModifier));
Debug.WriteLine("Queued asset: " + assetName);
}
else
{
throw new InvalidOperationException("Did not queue asset due to asset with same name being loaded: " + assetName);
}
}
}
/// <summary>
/// Called whenever a batch of assets should be loaded from the queue. Safe to call once every frame.
/// </summary>
public void Update()
{
if (queue.Count > 0 && (thread == null || !thread.IsAlive))
{
thread = new Thread(LoadBatch);
thread.Start();
}
}
private void LoadBatch()
{
running = true;
int totalTasks = queue.Count;
int tasksCompleted = 0;
while (queue.Count != 0)
{
lock (queue)
{
ContentData content = queue.Dequeue();
Load(content.assetName, content.type, content.usePathModifier);
tasksCompleted++;
progress = (float)tasksCompleted / totalTasks;
}
}
running = false;
}
/// <summary>
/// Removes the asset from the list of assets in the system.
/// Cannot remove from queue.
/// </summary>
/// <param name="name">the string name used to load the asset</param>
public void Remove(string name)
{
lock (queue)
{
if (assets.ContainsKey(name))
{
if (assets[name] is IDisposable)
{
((IDisposable)assets[name]).Dispose();
}
assets.Remove(name);
}
}
}
/// <summary>
/// Clears the queue.
/// </summary>
public void ClearQueue()
{
lock (queue)
{
queue.Clear();
}
}
/// <summary>
/// Unloads everything from both queue and loaded list while properly disposing of the assets loaded.
/// </summary>
public void UnloadAll()
{
lock (queue)
{
contentManager.Unload();
assets.Clear();
ClearQueue();
Debug.WriteLine("Unloaded all assets.");
}
}
private struct ContentData
{
internal Type type;
internal string assetName;
internal bool usePathModifier;
public ContentData(string assetName, Type type, bool usePathModifier)
{
this.type = type;
this.assetName = assetName;
this.usePathModifier = usePathModifier;
}
}
}
}

View File

@ -0,0 +1,15 @@
namespace RecrownedAthenaeum.Assets
{
/// <summary>
/// Modifies the given path based on a name. Used to simplify long paths for the <see cref="AssetManager"/>
/// </summary>
public interface IAssetPathResolver
{
/// <summary>
/// Returns the complete path with the content folder as root.
/// </summary>
/// <param name="contentPath">Is the asset's name</param>
/// <returns></returns>
string Modify(string contentPath);
}
}

View File

@ -0,0 +1,18 @@
namespace RecrownedAthenaeum.Assets
{
/// <summary>
/// A resolver that does nothing. Used for looking in the root by default.
/// </summary>
public class NormalAssetPathResolver : IAssetPathResolver
{
/// <summary>
/// Passes the path through without modification as this is the normal content resolver and is meant to just pass things on.
/// </summary>
/// <param name="contentPath">The path to modify.</param>
/// <returns></returns>
public string Modify(string contentPath)
{
return contentPath;
}
}
}

View File

@ -0,0 +1,32 @@
using System;
namespace RecrownedAthenaeum.Types
{
public struct Color
{
public byte r, g, b;
public float R {
set {
r = (byte)Math.Min(Math.Round(Byte.MaxValue * value), Byte.MaxValue);
}
get {
return r / (float)(byte.MaxValue);
}
}
public float G {
set {
g = (byte)Math.Min(Math.Round(Byte.MaxValue * value), Byte.MaxValue);
}
get {
return g / (float)(byte.MaxValue);
}
}
public float B {
set {
g = (byte)Math.Min(Math.Round(Byte.MaxValue * value), Byte.MaxValue);
}
get {
return g / (float)(byte.MaxValue);
}
}
}
}

View File

@ -0,0 +1,20 @@
using RecrownedAthenaeum.Render;
namespace RecrownedAthenaeum.Types
{
/// <summary>
/// A wrapper that makes sure anything implementing can be drawn with options.
/// </summary>
public interface ISpecialDrawable
{
/// <summary>
/// Should draw whatever implements this.
/// </summary>
/// <param name="spriteBatch">The batch to be used.</param>
/// <param name="destination">The location and dimensions to draw to.</param>
/// <param name="color">The color tint to draw with.</param>
/// <param name="rotation">The rotation to be used.</param>
/// <param name="origin">The origin for the rotation.</param>
void Draw(ConsistentSpriteBatch spriteBatch, Rectangle destination, Color color, float rotation = 0f, Vector2 origin = default(Vector2));
}
}

View File

@ -0,0 +1,128 @@
using RecrownedAthenaeum.Render;
using System;
namespace RecrownedAthenaeum.Types
{
/// <summary>
/// An object that represents a ninepatch.
/// </summary>
public class NinePatch : ISpecialDrawable
{
/// <summary>
/// Dimensions in ninepatch. May also represent position in texture atlas.
/// </summary>
public Rectangle textureRegion;
readonly Texture2D texture;
readonly int left, right, bottom, top;
Rectangle[] sourcePatches;
/// <summary>
/// A nine patch object.
/// </summary>
/// <param name="texture">Texture used for the nine patch. Dimensions must be greater than their sum border counter parts. If used as part of texture atlas, the texture should be the texture of the entire atlas.</param>
/// <param name="left">Left side.</param>
/// <param name="right">Right side.</param>
/// <param name="bottom">Bottom side.</param>
/// <param name="top">Top side.</param>
/// <param name="textureBounds">The dimensions and potentially the coordinates of the rectangle on an atlas. If left to default of null, will only be set to texture bounds.</param>
public NinePatch(Texture2D texture, int left, int right, int bottom, int top, Rectangle? textureBounds = null)
{
this.texture = texture;
if (textureBounds.HasValue) textureRegion = textureBounds.Value; else textureRegion = texture.Bounds;
if (left + right >= textureRegion.Width) throw new ArgumentOutOfRangeException("left and right values cannot be greater than or equal to the width of region. Left value is " + left + " and right value is " + right + ". Bounds of texture are: " + textureRegion);
if (bottom + top >= textureRegion.Height) throw new ArgumentOutOfRangeException("Bottom and top values cannot be greater than or equal to the width of region. Bottom value is " + bottom + " and top value is " + top + ".");
this.left = left;
this.right = right;
this.bottom = bottom;
this.top = top;
Rectangle[] patches =
{
new Rectangle(0, 0, left, bottom),
new Rectangle(left, 0, textureRegion.Width - left - right, bottom),
new Rectangle(textureRegion.Width - right, 0, right, bottom),
new Rectangle(0, bottom, left, textureRegion.Height - top - bottom),
new Rectangle(left, bottom, textureRegion.Width - left - right, textureRegion.Height - bottom - top),
new Rectangle(textureRegion.Width - right, bottom, right, textureRegion.Height - top - bottom),
new Rectangle(0, textureRegion.Height - top, left, top),
new Rectangle(left, textureRegion.Height - top, textureRegion.Width - left - right, top),
new Rectangle(textureRegion.Width - right, textureRegion.Height - top, right, top),
};
for (int i = 0; i < patches.Length; i++)
{
patches[i].X += textureRegion.X;
patches[i].Y += textureRegion.Y;
}
sourcePatches = patches;
}
private Rectangle[] GenenerateDestinationRectangles(int width, int height)
{
Rectangle[] patches =
{
new Rectangle(0, 0, left, bottom),
new Rectangle(left, 0, width - left - right, bottom),
new Rectangle(width -right, 0, right, bottom),
new Rectangle(0, bottom, left, height - bottom - top),
new Rectangle(left, bottom, width - left - right, height - top - bottom),
new Rectangle(width - right, bottom, right, height - top - bottom),
new Rectangle(0, height - top, left, top),
new Rectangle(left, height - top, width - left - right, top),
new Rectangle(width - right, height - top, right, top),
};
return patches;
}
/// <summary>
/// Draws the ninepatch.
/// </summary>
/// <param name="spriteBatch">Batch to use.</param>
/// <param name="color">The color of the patch.</param>
/// <param name="destination">Where to the patch.</param>
public void Draw(ConsistentSpriteBatch spriteBatch, Color color, Rectangle destination)
{
try
{
spriteBatch.End();
} catch (InvalidOperationException)
{
throw new InvalidOperationException("Begin must be called to draw a nine patch!");
}
spriteBatch.BeginWithoutSaving(samplerState: SamplerState.PointClamp);
Rectangle[] destinations = GenenerateDestinationRectangles(destination.Width, destination.Height);
for (int i = 0; i < destinations.Length; i++)
{
destinations[i].X += destination.X;
destinations[i].Y += destination.Y;
spriteBatch.Draw(texture, destinations[i], sourcePatches[i], color);
}
spriteBatch.End();
spriteBatch.Begin();
}
/// <summary>
/// </summary>
/// <param name="spriteBatch">Spritebatch to use.</param>
/// <param name="destination">The destination to draw the patch.</param>
/// <param name="color">The tint for each patch.</param>
/// <param name="rotation">Not considered for 9patches.</param>
/// <param name="origin">Not considered for 9patches.</param>
public void Draw(ConsistentSpriteBatch spriteBatch, Rectangle destination, Color color, float rotation = 0, Vector2 origin = default(Vector2))
{
if (rotation != 0) throw new NotImplementedException("Ninepatches can't be rotated.");
if (origin != default(Vector2))
{
destination.X -= (int)origin.X;
destination.Y -= (int)origin.Y;
}
Draw(spriteBatch, color, destination);
}
}
}

View File

@ -0,0 +1,24 @@
namespace RecrownedAthenaeum.Types
{
public struct Rectangle
{
public int Width {set; get;}
public int Height {set; get;}
public int X {set; get;}
public int Y {set; get;}
public int Area {
get {
return Width * Height;
}
}
public Rectangle(int x, int y, int width, int height) {
Width = width;
Height = height;
X = x;
Y = y;
}
}
}

View File

@ -0,0 +1,54 @@
using System;
namespace RecrownedAthenaeum.Types
{
/// <summary>
/// Holds a width and height while allowing for easier comparison between other <see cref="Resolution"/>s.
/// </summary>
public struct Resolution : IComparable<Resolution>
{
/// <summary>
/// Dimensions of resolution.
/// </summary>
public int Width, Height;
/// <summary>
/// Constructs resolution given the dimensions.
/// </summary>
/// <param name="width">Width of resolution.</param>
/// <param name="height">Height of resolution.</param>
public Resolution(int width, int height)
{
Width = width;
Height = height;
}
/// <summary>
/// Compares area of this resolution to another.
/// </summary>
/// <param name="other">The other resolution to compare to.</param>
/// <returns>A value less than 0 for this being the smaller resolution, 0 for the two being the same in area, and larger for this being the larger in area.</returns>
public int CompareTo(Resolution other)
{
return Area() - other.Area();
}
/// <summary>
/// Gets a string representation of this resolution.
/// </summary>
/// <returns>"WidthxHeight"</returns>
public override string ToString()
{
return Width + "x" + Height;
}
/// <summary>
/// Calculates area of resolution.
/// </summary>
/// <returns>Area of resolution.</returns>
public int Area()
{
return Width * Height;
}
}
}

View File

@ -0,0 +1,234 @@
using RecrownedAthenaeum.Render;
using System;
using System.Collections.Generic;
using System.Linq;
namespace RecrownedAthenaeum.Types
{
/// <summary>
/// Holds information about an image file that contains various textures in various regions in the file.
/// </summary>
public class TextureAtlas : IDisposable
{
private Texture2D texture;
private bool disposed;
private Dictionary<string, Region> dictionaryOfRegions = new Dictionary<string, Region>();
/// <summary>
/// Given a name, can return a <see cref="Region"/>.
/// </summary>
/// <param name="name">Name of <see cref="Region"/> to obtain.</param>
/// <returns><see cref="Region"/> based off name.</returns>
public Region this[string name] { get { if (name != null && dictionaryOfRegions.ContainsKey(name)) return dictionaryOfRegions[name]; else throw new KeyNotFoundException("Given key \"" + name + "\" does not exist."); } }
/// <summary>
/// Creates a texture atlas with given main texture as well as an array of <see cref="Region"/> to represent locations of which textures reside within the atlas. Region names will be used to refer to the regions within the dictionary.
/// </summary>
/// <param name="texture">The texture representing the overall atlas.</param>
/// <param name="regions">The sub regions that represent the individual textures.</param>
public TextureAtlas(Texture2D texture, Region[] regions)
{
this.texture = texture;
foreach (Region region in regions)
{
dictionaryOfRegions.Add(region.name, region);
}
}
/// <summary>
/// Creates a texture region given a dictionary of regions keyed to strings that can be used to refer to them.
/// </summary>
/// <param name="texture">The texture representing the overall atlas.</param>
/// <param name="dictionaryOfRegions"></param>
public TextureAtlas(Texture2D texture, Dictionary<string, Region> dictionaryOfRegions)
{
this.texture = texture;
this.dictionaryOfRegions = dictionaryOfRegions;
}
/// <summary>
/// Draw the region given by a string in the atlas onto a destination rectangle.
/// </summary>
/// <param name="name">Name of region to draw.</param>
/// <param name="batch">SpriteBatch to be used.</param>
/// <param name="destination">The location to draw this region.</param>
/// <param name="color">Color to use.</param>
/// <param name="rotation">Rotation of texture drawn.</param>
/// <param name="origin">Origin used by rotation.</param>
public void Draw(string name, ConsistentSpriteBatch batch, Rectangle destination, Color color = default(Color), float rotation = 0, Vector2 origin = new Vector2())
{
dictionaryOfRegions[name].Draw(batch, destination, color, rotation, origin);
}
/// <summary>
/// Creates or obtains a previously created texture of a region.
/// </summary>
/// <param name="name">Name of region.</param>
/// <param name="graphicsDevice">graphics device to be used to generate the texture.</param>
/// <returns>The texture from the region.</returns>
public Texture2D ObtainRegionAsTexture(string name, GraphicsDevice graphicsDevice)
{
return dictionaryOfRegions[name].AsTexture2D(graphicsDevice);
}
/// <summary>
/// Whether or not this atlas contains the given region name.
/// </summary>
/// <param name="regionName">The name of the region to check for.</param>
/// <returns>True if this atlas does contain the region given by name.</returns>
public bool ContainsRegion(string regionName)
{
return dictionaryOfRegions.ContainsKey(regionName);
}
/// <summary>
/// Disposes unmanaged resources for the texture atlas.
/// </summary>
public void Dispose()
{
Dispose(true);
}
/// <summary>
/// Overridable disposal method.
/// </summary>
/// <param name="disposing">Only true if user calls <see cref="Dispose()"/></param>
public virtual void Dispose(bool disposing)
{
disposed = true;
if (!disposed && disposing)
{
texture.Dispose();
for (int i = 0; i < dictionaryOfRegions.Count; i++)
{
Region region = dictionaryOfRegions.ElementAt(i).Value;
if (!region.Disposed)
{
dictionaryOfRegions.ElementAt(i).Value.Dispose();
}
}
dictionaryOfRegions.Clear();
}
}
/// <summary>
/// Destructor.
/// </summary>
~TextureAtlas()
{
Dispose(false);
}
/// <summary>
/// A region of a <see cref="TextureAtlas"/>.
/// </summary>
public class Region : ISpecialDrawable, IDisposable
{
/// <summary>
/// The name of the region. Mostly used to be refered to within the context of a <see cref="TextureAtlas"/>.
/// </summary>
public readonly string name;
/// <summary>
/// The location and dimensions of where the original texture resides on the texture representing the atlas.
/// </summary>
public readonly Rectangle sourceRectangle;
readonly NinePatch ninepatch;
Texture2D atlasTexture;
Texture2D regionTexture;
/// <summary>
/// If region has already been disposed.
/// </summary>
public bool Disposed { get; private set; }
/// <summary>
/// A specified region in a texture atlas.
/// </summary>
/// <param name="name">Name of region.</param>
/// <param name="sourceRegion">The location of the region on the atlas.</param>
/// <param name="ninePatch">A <see cref="NinePatch"/> definition for the region.</param>
/// <param name="atlasTexture">The texture that holds the image data for the atlas.</param>
public Region(string name, Rectangle sourceRegion, NinePatch ninePatch, Texture2D atlasTexture)
{
this.atlasTexture = atlasTexture ?? throw new ArgumentNullException("Name parameters can be null.");
this.name = name ?? throw new ArgumentNullException("Name parameters can be null.");
sourceRectangle = sourceRegion;
ninepatch = ninePatch;
}
/// <summary>
/// Draws the region. If ninepatch, rotation and origin are ignored.
/// </summary>
/// <param name="batch">The batch to use. Should be began.</param>
/// <param name="destination">The destination rectangle to draw to.</param>
/// <param name="color">The color to use.</param>
/// <param name="rotation">Rotation of the final drawing. Ignored if is a 9patch.</param>
/// <param name="origin">The origin of the drawing. Ignored if is a 9patch.</param>
public void Draw(ConsistentSpriteBatch batch, Rectangle destination, Color color, float rotation = 0, Vector2 origin = default(Vector2))
{
if (Disposed) throw new ObjectDisposedException(GetType().Name);
if (ninepatch != null)
{
ninepatch.Draw(batch, color, destination);
}
else
{
batch.Draw(atlasTexture, destination, sourceRectangle, color, rotation, origin, SpriteEffects.None, 0f);
}
}
/// <summary>
/// Create or obtains a previously created texture of this region.
/// </summary>
/// <param name="graphicsDevice">The graphics device to use to create the texture.</param>
/// <returns>The texture of the region.</returns>
public Texture2D AsTexture2D(GraphicsDevice graphicsDevice)
{
if (Disposed) throw new ObjectDisposedException(GetType().Name);
if (regionTexture == null)
{
Color[] data = new Color[sourceRectangle.Width * sourceRectangle.Height];
regionTexture = new Texture2D(graphicsDevice, sourceRectangle.Width, sourceRectangle.Height);
atlasTexture.GetData(0, sourceRectangle, data, 0, sourceRectangle.Width * sourceRectangle.Height);
regionTexture.SetData(data);
}
return regionTexture;
}
/// <summary>
/// Call this to dispose.
/// </summary>
public void Dispose()
{
if (Disposed) throw new ObjectDisposedException(GetType().Name);
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Overridable dispose.
/// </summary>
/// <param name="disposing">Whether or not this was a user made call.</param>
public virtual void Dispose(bool disposing)
{
if (disposing && !Disposed)
{
regionTexture?.Dispose();
}
Disposed = true;
}
/// <summary>
/// Destructor.
/// </summary>
~Region()
{
Dispose(false);
}
}
}
}