Rewrite sequence loading logic.
Multiple layers of Lazy<T>ness are replaced with an explicit two-part loading scheme. Sequences are parsed immediately, without the need for the sprite assets, and tell the SpriteCache which frames they need. Use-cases that want the actual sprites can then tell the SpriteCache to load the frames and the sequences to resolve the sprites.
This commit is contained in:
@@ -22,7 +22,7 @@ namespace OpenRA.Graphics
|
||||
public string Name { get; private set; }
|
||||
public bool IsDecoration { get; set; }
|
||||
|
||||
readonly SequenceProvider sequences;
|
||||
readonly SequenceSet sequences;
|
||||
readonly Func<WAngle> facingFunc;
|
||||
readonly Func<bool> paused;
|
||||
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenRA.FileSystem;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
using Sequences = IReadOnlyDictionary<string, Lazy<IReadOnlyDictionary<string, ISpriteSequence>>>;
|
||||
using UnitSequences = Lazy<IReadOnlyDictionary<string, ISpriteSequence>>;
|
||||
|
||||
public interface ISpriteSequence
|
||||
{
|
||||
string Name { get; }
|
||||
int Length { get; }
|
||||
int Facings { get; }
|
||||
int Tick { get; }
|
||||
int ZOffset { get; }
|
||||
int ShadowZOffset { get; }
|
||||
Rectangle Bounds { get; }
|
||||
bool IgnoreWorldTint { get; }
|
||||
float Scale { get; }
|
||||
|
||||
Sprite GetSprite(int frame);
|
||||
Sprite GetSprite(int frame, WAngle facing);
|
||||
(Sprite, WAngle) GetSpriteWithRotation(int frame, WAngle facing);
|
||||
Sprite GetShadow(int frame, WAngle facing);
|
||||
float GetAlpha(int frame);
|
||||
}
|
||||
|
||||
public interface ISpriteSequenceLoader
|
||||
{
|
||||
IReadOnlyDictionary<string, ISpriteSequence> ParseSequences(ModData modData, string tileSet, SpriteCache cache, MiniYamlNode node);
|
||||
}
|
||||
|
||||
public class SequenceProvider : IDisposable
|
||||
{
|
||||
readonly ModData modData;
|
||||
readonly string tileSet;
|
||||
readonly Lazy<Sequences> sequences;
|
||||
readonly Lazy<SpriteCache> spriteCache;
|
||||
public SpriteCache SpriteCache => spriteCache.Value;
|
||||
|
||||
readonly Dictionary<string, UnitSequences> sequenceCache = new Dictionary<string, UnitSequences>();
|
||||
|
||||
public SequenceProvider(IReadOnlyFileSystem fileSystem, ModData modData, string tileSet, MiniYaml additionalSequences)
|
||||
{
|
||||
this.modData = modData;
|
||||
this.tileSet = tileSet;
|
||||
sequences = Exts.Lazy(() =>
|
||||
{
|
||||
using (new Support.PerfTimer("LoadSequences"))
|
||||
return Load(fileSystem, additionalSequences);
|
||||
});
|
||||
|
||||
spriteCache = Exts.Lazy(() => new SpriteCache(fileSystem, modData.SpriteLoaders));
|
||||
}
|
||||
|
||||
public ISpriteSequence GetSequence(string unitName, string sequenceName)
|
||||
{
|
||||
if (!sequences.Value.TryGetValue(unitName, out var unitSeq))
|
||||
throw new InvalidOperationException($"Unit `{unitName}` does not have any sequences defined.");
|
||||
|
||||
if (!unitSeq.Value.TryGetValue(sequenceName, out var seq))
|
||||
throw new InvalidOperationException($"Unit `{unitName}` does not have a sequence named `{sequenceName}`");
|
||||
|
||||
return seq;
|
||||
}
|
||||
|
||||
public IEnumerable<string> Images => sequences.Value.Keys;
|
||||
|
||||
public bool HasSequence(string unitName)
|
||||
{
|
||||
return sequences.Value.ContainsKey(unitName);
|
||||
}
|
||||
|
||||
public bool HasSequence(string unitName, string sequenceName)
|
||||
{
|
||||
if (!sequences.Value.TryGetValue(unitName, out var unitSeq))
|
||||
throw new InvalidOperationException($"Unit `{unitName}` does not have any sequences defined.");
|
||||
|
||||
return unitSeq.Value.ContainsKey(sequenceName);
|
||||
}
|
||||
|
||||
public IEnumerable<string> Sequences(string unitName)
|
||||
{
|
||||
if (!sequences.Value.TryGetValue(unitName, out var unitSeq))
|
||||
throw new InvalidOperationException($"Unit `{unitName}` does not have any sequences defined.");
|
||||
|
||||
return unitSeq.Value.Keys;
|
||||
}
|
||||
|
||||
Sequences Load(IReadOnlyFileSystem fileSystem, MiniYaml additionalSequences)
|
||||
{
|
||||
var nodes = MiniYaml.Load(fileSystem, modData.Manifest.Sequences, additionalSequences);
|
||||
var items = new Dictionary<string, UnitSequences>();
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
// Nodes starting with ^ are inheritable but never loaded directly
|
||||
if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
var key = node.Value.ToLines(node.Key).JoinWith("|");
|
||||
|
||||
if (sequenceCache.TryGetValue(key, out var t))
|
||||
items.Add(node.Key, t);
|
||||
else
|
||||
{
|
||||
t = Exts.Lazy(() => modData.SpriteSequenceLoader.ParseSequences(modData, tileSet, SpriteCache, node));
|
||||
sequenceCache.Add(key, t);
|
||||
items.Add(node.Key, t);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public void Preload()
|
||||
{
|
||||
foreach (var sb in SpriteCache.SheetBuilders.Values)
|
||||
sb.Current.CreateBuffer();
|
||||
|
||||
foreach (var unitSeq in sequences.Value.Values)
|
||||
foreach (var seq in unitSeq.Value.Values) { }
|
||||
|
||||
foreach (var sb in SpriteCache.SheetBuilders.Values)
|
||||
sb.Current.ReleaseBuffer();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (spriteCache.IsValueCreated)
|
||||
foreach (var sb in SpriteCache.SheetBuilders.Values)
|
||||
sb.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
119
OpenRA.Game/Graphics/SequenceSet.cs
Normal file
119
OpenRA.Game/Graphics/SequenceSet.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenRA.FileSystem;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
public interface ISpriteSequence
|
||||
{
|
||||
string Name { get; }
|
||||
int Length { get; }
|
||||
int Facings { get; }
|
||||
int Tick { get; }
|
||||
int ZOffset { get; }
|
||||
int ShadowZOffset { get; }
|
||||
Rectangle Bounds { get; }
|
||||
bool IgnoreWorldTint { get; }
|
||||
float Scale { get; }
|
||||
void ResolveSprites(SpriteCache cache);
|
||||
Sprite GetSprite(int frame);
|
||||
Sprite GetSprite(int frame, WAngle facing);
|
||||
(Sprite, WAngle) GetSpriteWithRotation(int frame, WAngle facing);
|
||||
Sprite GetShadow(int frame, WAngle facing);
|
||||
float GetAlpha(int frame);
|
||||
}
|
||||
|
||||
public interface ISpriteSequenceLoader
|
||||
{
|
||||
int BgraSheetSize { get; }
|
||||
int IndexedSheetSize { get; }
|
||||
IReadOnlyDictionary<string, ISpriteSequence> ParseSequences(ModData modData, string tileSet, SpriteCache cache, MiniYamlNode node);
|
||||
}
|
||||
|
||||
public sealed class SequenceSet : IDisposable
|
||||
{
|
||||
readonly ModData modData;
|
||||
readonly string tileSet;
|
||||
readonly IReadOnlyDictionary<string, IReadOnlyDictionary<string, ISpriteSequence>> images;
|
||||
public SpriteCache SpriteCache { get; }
|
||||
|
||||
public SequenceSet(IReadOnlyFileSystem fileSystem, ModData modData, string tileSet, MiniYaml additionalSequences)
|
||||
{
|
||||
this.modData = modData;
|
||||
this.tileSet = tileSet;
|
||||
SpriteCache = new SpriteCache(fileSystem, modData.SpriteLoaders, modData.SpriteSequenceLoader.BgraSheetSize, modData.SpriteSequenceLoader.IndexedSheetSize);
|
||||
using (new Support.PerfTimer("LoadSequences"))
|
||||
images = Load(fileSystem, additionalSequences);
|
||||
}
|
||||
|
||||
public ISpriteSequence GetSequence(string image, string sequence)
|
||||
{
|
||||
if (!images.TryGetValue(image, out var sequences))
|
||||
throw new InvalidOperationException($"Image `{image}` does not have any sequences defined.");
|
||||
|
||||
if (!sequences.TryGetValue(sequence, out var seq))
|
||||
throw new InvalidOperationException($"Image `{image}` does not have a sequence named `{sequence}`.");
|
||||
|
||||
return seq;
|
||||
}
|
||||
|
||||
public IEnumerable<string> Images => images.Keys;
|
||||
|
||||
public bool HasSequence(string image, string sequence)
|
||||
{
|
||||
if (!images.TryGetValue(image, out var sequences))
|
||||
throw new InvalidOperationException($"Image `{image}` does not have any sequences defined.");
|
||||
|
||||
return sequences.ContainsKey(sequence);
|
||||
}
|
||||
|
||||
public IEnumerable<string> Sequences(string image)
|
||||
{
|
||||
if (!images.TryGetValue(image, out var sequences))
|
||||
throw new InvalidOperationException($"Image `{image}` does not have any sequences defined.");
|
||||
|
||||
return sequences.Keys;
|
||||
}
|
||||
|
||||
IReadOnlyDictionary<string, IReadOnlyDictionary<string, ISpriteSequence>> Load(IReadOnlyFileSystem fileSystem, MiniYaml additionalSequences)
|
||||
{
|
||||
var nodes = MiniYaml.Load(fileSystem, modData.Manifest.Sequences, additionalSequences);
|
||||
var images = new Dictionary<string, IReadOnlyDictionary<string, ISpriteSequence>>();
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
// Nodes starting with ^ are inheritable but never loaded directly
|
||||
if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
images[node.Key] = modData.SpriteSequenceLoader.ParseSequences(modData, tileSet, SpriteCache, node);
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
public void LoadSprites()
|
||||
{
|
||||
SpriteCache.LoadReservations(modData);
|
||||
foreach (var sequences in images.Values)
|
||||
foreach (var sequence in sequences)
|
||||
sequence.Value.ResolveSprites(SpriteCache);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SpriteCache.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
174
OpenRA.Game/Graphics/SpriteCache.cs
Normal file
174
OpenRA.Game/Graphics/SpriteCache.cs
Normal file
@@ -0,0 +1,174 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using OpenRA.FileSystem;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
public class SpriteCache : IDisposable
|
||||
{
|
||||
public readonly Dictionary<SheetType, SheetBuilder> SheetBuilders;
|
||||
readonly ISpriteLoader[] loaders;
|
||||
readonly IReadOnlyFileSystem fileSystem;
|
||||
|
||||
readonly Dictionary<int, (int[] Frames, MiniYamlNode.SourceLocation Location)> spriteReservations = new Dictionary<int, (int[], MiniYamlNode.SourceLocation)>();
|
||||
readonly Dictionary<int, (int[] Frames, MiniYamlNode.SourceLocation Location)> frameReservations = new Dictionary<int, (int[], MiniYamlNode.SourceLocation)>();
|
||||
readonly Dictionary<string, List<int>> reservationsByFilename = new Dictionary<string, List<int>>();
|
||||
|
||||
readonly Dictionary<int, ISpriteFrame[]> resolvedFrames = new Dictionary<int, ISpriteFrame[]>();
|
||||
readonly Dictionary<int, Sprite[]> resolvedSprites = new Dictionary<int, Sprite[]>();
|
||||
|
||||
readonly Dictionary<int, (string Filename, MiniYamlNode.SourceLocation Location)> missingFiles = new Dictionary<int, (string, MiniYamlNode.SourceLocation)>();
|
||||
|
||||
int nextReservationToken = 1;
|
||||
|
||||
public SpriteCache(IReadOnlyFileSystem fileSystem, ISpriteLoader[] loaders, int bgraSheetSize, int indexedSheetSize, int bgraSheetMargin = 1, int indexedSheetMargin = 1)
|
||||
{
|
||||
SheetBuilders = new Dictionary<SheetType, SheetBuilder>
|
||||
{
|
||||
{ SheetType.Indexed, new SheetBuilder(SheetType.Indexed, indexedSheetSize, indexedSheetMargin) },
|
||||
{ SheetType.BGRA, new SheetBuilder(SheetType.BGRA, bgraSheetSize, bgraSheetMargin) }
|
||||
};
|
||||
|
||||
this.fileSystem = fileSystem;
|
||||
this.loaders = loaders;
|
||||
}
|
||||
|
||||
public int ReserveSprites(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location)
|
||||
{
|
||||
var token = nextReservationToken++;
|
||||
spriteReservations[token] = (frames?.ToArray(), location);
|
||||
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).Add(token);
|
||||
return token;
|
||||
}
|
||||
|
||||
public int ReserveFrames(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location)
|
||||
{
|
||||
var token = nextReservationToken++;
|
||||
frameReservations[token] = (frames?.ToArray(), location);
|
||||
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).Add(token);
|
||||
return token;
|
||||
}
|
||||
|
||||
static ISpriteFrame[] GetFrames(IReadOnlyFileSystem fileSystem, string filename, ISpriteLoader[] loaders, out TypeDictionary metadata)
|
||||
{
|
||||
metadata = null;
|
||||
if (!fileSystem.TryOpen(filename, out var stream))
|
||||
return null;
|
||||
|
||||
using (stream)
|
||||
{
|
||||
foreach (var loader in loaders)
|
||||
if (loader.TryParseSprite(stream, filename, out var frames, out metadata))
|
||||
return frames;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadReservations(ModData modData)
|
||||
{
|
||||
foreach (var sb in SheetBuilders.Values)
|
||||
sb.Current.CreateBuffer();
|
||||
|
||||
var spriteCache = new Dictionary<int, Sprite>();
|
||||
foreach (var (filename, tokens) in reservationsByFilename)
|
||||
{
|
||||
modData.LoadScreen?.Display();
|
||||
var loadedFrames = GetFrames(fileSystem, filename, loaders, out _);
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (frameReservations.TryGetValue(token, out var r))
|
||||
{
|
||||
if (loadedFrames != null)
|
||||
{
|
||||
if (r.Frames != null)
|
||||
{
|
||||
var resolved = new ISpriteFrame[loadedFrames.Length];
|
||||
foreach (var i in r.Frames)
|
||||
resolved[i] = loadedFrames[i];
|
||||
resolvedFrames[token] = resolved;
|
||||
}
|
||||
else
|
||||
resolvedFrames[token] = loadedFrames;
|
||||
}
|
||||
else
|
||||
{
|
||||
resolvedFrames[token] = null;
|
||||
missingFiles[token] = (filename, r.Location);
|
||||
}
|
||||
}
|
||||
|
||||
if (spriteReservations.TryGetValue(token, out r))
|
||||
{
|
||||
if (loadedFrames != null)
|
||||
{
|
||||
var resolved = new Sprite[loadedFrames.Length];
|
||||
var frames = r.Frames ?? Enumerable.Range(0, loadedFrames.Length);
|
||||
foreach (var i in frames)
|
||||
resolved[i] = spriteCache.GetOrAdd(i,
|
||||
f => SheetBuilders[SheetBuilder.FrameTypeToSheetType(loadedFrames[f].Type)].Add(loadedFrames[f]));
|
||||
|
||||
resolvedSprites[token] = resolved;
|
||||
}
|
||||
else
|
||||
{
|
||||
resolvedSprites[token] = null;
|
||||
missingFiles[token] = (filename, r.Location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spriteCache.Clear();
|
||||
}
|
||||
|
||||
spriteReservations.Clear();
|
||||
frameReservations.Clear();
|
||||
reservationsByFilename.Clear();
|
||||
|
||||
foreach (var sb in SheetBuilders.Values)
|
||||
sb.Current.ReleaseBuffer();
|
||||
}
|
||||
|
||||
public Sprite[] ResolveSprites(int token)
|
||||
{
|
||||
var resolved = resolvedSprites[token];
|
||||
resolvedSprites.Remove(token);
|
||||
if (missingFiles.TryGetValue(token, out var r))
|
||||
throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename);
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
public ISpriteFrame[] ResolveFrames(int token)
|
||||
{
|
||||
var resolved = resolvedFrames[token];
|
||||
resolvedFrames.Remove(token);
|
||||
if (missingFiles.TryGetValue(token, out var r))
|
||||
throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename);
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
public IEnumerable<(string Filename, MiniYamlNode.SourceLocation Location)> MissingFiles => missingFiles.Values.ToHashSet();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var sb in SheetBuilders.Values)
|
||||
sb.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,7 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using OpenRA.FileSystem;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
@@ -67,94 +64,6 @@ namespace OpenRA.Graphics
|
||||
bool DisableExportPadding { get; }
|
||||
}
|
||||
|
||||
public class SpriteCache
|
||||
{
|
||||
public readonly Cache<SheetType, SheetBuilder> SheetBuilders;
|
||||
readonly ISpriteLoader[] loaders;
|
||||
readonly IReadOnlyFileSystem fileSystem;
|
||||
|
||||
readonly Dictionary<string, List<Sprite[]>> sprites = new Dictionary<string, List<Sprite[]>>();
|
||||
readonly Dictionary<string, ISpriteFrame[]> unloadedFrames = new Dictionary<string, ISpriteFrame[]>();
|
||||
readonly Dictionary<string, TypeDictionary> metadata = new Dictionary<string, TypeDictionary>();
|
||||
|
||||
public SpriteCache(IReadOnlyFileSystem fileSystem, ISpriteLoader[] loaders)
|
||||
{
|
||||
SheetBuilders = new Cache<SheetType, SheetBuilder>(t => new SheetBuilder(t));
|
||||
|
||||
this.fileSystem = fileSystem;
|
||||
this.loaders = loaders;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first set of sprites with the given filename.
|
||||
/// If getUsedFrames is defined then the indices returned by the function call
|
||||
/// are guaranteed to be loaded. The value of other indices in the returned
|
||||
/// array are undefined and should never be accessed.
|
||||
/// </summary>
|
||||
public Sprite[] this[string filename, Func<int, IEnumerable<int>> getUsedFrames = null]
|
||||
{
|
||||
get
|
||||
{
|
||||
var allSprites = sprites.GetOrAdd(filename);
|
||||
var sprite = allSprites.FirstOrDefault();
|
||||
|
||||
if (!unloadedFrames.TryGetValue(filename, out var unloaded))
|
||||
unloaded = null;
|
||||
|
||||
// This is the first time that the file has been requested
|
||||
// Load all of the frames into the unused buffer and initialize
|
||||
// the loaded cache (initially empty)
|
||||
if (sprite == null)
|
||||
{
|
||||
unloaded = FrameLoader.GetFrames(fileSystem, filename, loaders, out var fileMetadata);
|
||||
unloadedFrames[filename] = unloaded;
|
||||
metadata[filename] = fileMetadata;
|
||||
|
||||
sprite = new Sprite[unloaded.Length];
|
||||
allSprites.Add(sprite);
|
||||
}
|
||||
|
||||
// HACK: The sequence code relies on side-effects from getUsedFrames
|
||||
var indices = getUsedFrames != null ? getUsedFrames(sprite.Length) :
|
||||
Enumerable.Range(0, sprite.Length);
|
||||
|
||||
// Load any unused frames into the SheetBuilder
|
||||
if (unloaded != null)
|
||||
{
|
||||
foreach (var i in indices)
|
||||
{
|
||||
if (unloaded[i] != null)
|
||||
{
|
||||
sprite[i] = SheetBuilders[SheetBuilder.FrameTypeToSheetType(unloaded[i].Type)].Add(unloaded[i]);
|
||||
unloaded[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
// All frames have been loaded
|
||||
if (unloaded.All(f => f == null))
|
||||
unloadedFrames.Remove(filename);
|
||||
}
|
||||
|
||||
return sprite;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a TypeDictionary containing any metadata defined by the frame
|
||||
/// or null if the frame does not define metadata.
|
||||
/// </summary>
|
||||
public TypeDictionary FrameMetadata(string filename)
|
||||
{
|
||||
if (!metadata.TryGetValue(filename, out var fileMetadata))
|
||||
{
|
||||
FrameLoader.GetFrames(fileSystem, filename, loaders, out fileMetadata);
|
||||
metadata[filename] = fileMetadata;
|
||||
}
|
||||
|
||||
return fileMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
public class FrameCache
|
||||
{
|
||||
readonly Cache<string, ISpriteFrame[]> frames;
|
||||
|
||||
Reference in New Issue
Block a user