using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using System.Collections.Generic; using System.Text.Json; namespace SemiColinGames { public static class Sprites { public static Sprite Executioner; public static Sprite Ninja; public static void Load(ContentManager content) { Executioner = new Sprite( Textures.Executioner, content.LoadString("sprites/ccg/executioner_female.json")); Ninja = new Sprite( Textures.Ninja, content.LoadString("sprites/ccg/ninja_female.json")); } } enum AnimationDirection { Forward, PingPong } struct SpriteAnimation { public readonly int Start; public readonly int End; public readonly double Duration; public readonly AnimationDirection Direction; public SpriteAnimation(int start, int end, double duration, AnimationDirection direction) { Start = start; End = end; Duration = duration; Direction = direction; } } struct Frame { public readonly Rectangle Source; public readonly double Duration; public Frame(Rectangle source, double duration) { Source = source; Duration = duration; } } public class Sprite { public readonly int Width; public readonly int Height; // Measures the empty pixels between the ground (where the sprite's feet rest in idle pose) // and the bottom of the sprite's image. public readonly int GroundPadding = 7; public readonly TextureRef Texture; private readonly Dictionary animations; private readonly List frames; public Sprite(TextureRef texture, string metadataJson) { Texture = texture; animations = new Dictionary(); JsonElement jsonRoot = JsonDocument.Parse(metadataJson).RootElement; frames = new List(); foreach (JsonElement child in jsonRoot.GetProperty("frames").EnumerateArray()) { JsonElement frame = child.GetProperty("frame"); Rectangle source = new Rectangle( frame.GetProperty("x").GetInt32(), frame.GetProperty("y").GetInt32(), frame.GetProperty("w").GetInt32(), frame.GetProperty("h").GetInt32()); double duration = child.GetProperty("duration").GetDouble() / 1000; frames.Add(new Frame(source, duration)); } // We assume that all frames are the same size (which right now is assured by the // Aseprite-based spritesheet export process). Width = frames[0].Source.Width; Height = frames[0].Source.Height; JsonElement frameTags = jsonRoot.GetProperty("meta").GetProperty("frameTags"); foreach (JsonElement child in frameTags.EnumerateArray()) { string name = child.GetProperty("name").GetString(); int start = child.GetProperty("from").GetInt32(); int end = child.GetProperty("to").GetInt32(); string directionString = child.GetProperty("direction").GetString(); AnimationDirection direction = directionString == "pingpong" ? AnimationDirection.PingPong : AnimationDirection.Forward; double duration = 0; for (int i = start; i <= end; i++) { duration += frames[i].Duration; } // A PingPong animation repeats every frame but the first and last. // Therefore its duration is 2x, minus the duration of the first and last frames. if (direction == AnimationDirection.PingPong) { duration = duration * 2 - frames[start].Duration - frames[end].Duration; } animations[name] = new SpriteAnimation(start, end, duration, direction); } } public Rectangle GetTextureSource(string animationName, double time) { SpriteAnimation animation = animations[animationName]; time %= animation.Duration; for (int i = animation.Start; i <= animation.End; i++) { double frameTime = frames[i].Duration; if (time < frameTime) { return frames[i].Source; } time -= frameTime; } if (animation.Direction == AnimationDirection.PingPong) { for (int i = animation.End - 1; i > animation.Start; i--) { double frameTime = frames[i].Duration; if (time < frameTime) { return frames[i].Source; } time -= frameTime; } } // We shouldn't get here, but if we did, the last frame is a fine thing to return. return frames[animation.End].Source; } } }