Spritesheet resources

Nice features, sounds great :slight_smile:

1 Like

Reserve your judgement for when you see it lol

1 Like

Record a video of it :slight_smile:

I think custom tools are nice because they can be adapted over time if you need more features. And also going with simple text files has advantages.

Hey @Kwyrky,

I have just added this to my samples, it might be a little clunky as I have ripped it from my engine, so does not come with my actual sprite code, but this sample should show how you can use this in your own project :slight_smile:

It’s by no means finished, but as I say, gives you something to look at.

I have done a tweet showing a gif of it running here.

It’s not checked in yet, will do it in the next 10 minutes, ill post again here when it’s in :slight_smile:

Hope you find this helpful. I will do a Patreon post on this (lowest subscription tutorial tier) to, so I can add a bit more detail to it.

1 Like

Checked in, it’s in my samples here.

Specificly the project that uses it is here.

Hope it’s useful :slight_smile:

1 Like

Nice I will check it out, thanks :slight_smile:

I worked out the actual values for the above animation I think. Here is a recording of it (using a custom shader now).

SpriteAnimation primm;
private const int Scale = 30;
private const int FrameWidth = 21;
private const int FrameHeight = 31;
private const int Frametime = 1000;
private const int FramesOffsetX = 96;
private const int FramesOffsetY = 33;
private const int FrameCountX = 6;
private const int FrameCountY = 1;
// ...
int positionX = (GraphicsDevice.PresentationParameters.BackBufferWidth / 2) - (int)((FrameWidth * Scale) / 2.0f);
int positionY = (GraphicsDevice.PresentationParameters.BackBufferHeight / 2) - (int)((FrameHeight * Scale) / 2.0f);
Vector2 position = new Vector2(positionX, positionY);
primm = new SpriteAnimation();
primm.Initialize(GraphicsDevice, SpriteSheet: primmSpriteSheet, Position: position, FrameWidth: FrameWidth, FrameHeight: FrameHeight, FrameCountX: FrameCountX, FrameCountY: FrameCountY, Frametime: Frametime, Color.White, Scale: Scale, Looping: true, SpriteSheetEffect: SpriteSheetEffect, FramesOffsetX: FramesOffsetX, FramesOffsetY: FramesOffsetY);
1 Like

The explosion animations look very good, impressive what can be done using sprite sheets :grinning:

1 Like

Got distracted playing around with the displacement / refraction / scrolling shader :smile:

2 Likes

I am using SpriteBatch with SourceRectangle to “select” the different frames and at the moment I am passing some values to the shader to calculate normalized UV coordinates / UV coordinates in the [0,1] range in both directions. Since I am using SpriteBatch with a custom effect I just have a pixel shader available I guess?! If I had access to the VertexShader I would probably pass a copy of the position or doing the calculation there (position to UV).

The question is if there is a simple way to access / get some normalized UV coordinates? Meaning without calculating the value by myself beforehand on the CPU or inside the pixelshader?

Another question came up. I noticed some timing and repetition values inside this SpriteSheet

in the second row the 8th animation “Grab+Shake+Throw down Chest”.

The animation can be seen here:

If I want to support animations which have different timings what is the canonical or recommended way of implementing this? Should I go with the option of having different Keyframes having 0,5x, 2x, 3x, 4x… etc speed?

Another question is if it is better in general to define one TimeSpan for one animation consisting of a bunch of Keyframes or to set a TimeSpan for each Keyframe on its own? I mean everything is possible but I have no experience with 2D animation and just thought maybe there is some recommended way which I should try to implement?

1 Like

That’s interesting…

But, are you using that sheet or are you creating your own? I suppose you could just duplicate the sprite for a number of frames, .5s is not critical, just an artist’s choice, but it would be interesting to see variable sprite animation timings in code form… I mean an array with a time flag or something could be one idea…

I implement 2D animation with a TimeSpan per animation, not per frame. I also set the timing based on the animation. For example, my walk animation is at 4 frames per second and my idle is at 16 frames per second. The actual walk animation is made up of 8 frames and the idle is made up of 28. Each has independent timers. I calculate the timing in the individual animation and don’t use a scaling factor. If you’re interested I can share the code.

At the moment I am using that sheet but it is just for testing out the 2D animation classes. Yes sure that would be possible to duplicate the frames a couple of times. But ideally one could just set a start and end keyframe and repetition value. What do you mean with an array with a time flag? How would that work then?

Sure if you like to share some code I would like to see how you have done it. I think to have one TimeSpan per animation is fine. And maybe only in those rare cases when you need different timings a setup of an animation consiting of multiple animations could be a solution to provide this functionality which would else require different TimeSpan values per keyframe. So I mean the animation from the SpriteSheet could be split into 3 animations which will be played one after another.

Something like this maybe?

Are you referring to the calculation of the animation step / current keyframe? So this here?

int frame = (int)((CurrentTimeInSeconds / (float)T) * N)

I have classes that I use for animation. This is one that has a single TimeSpan but shows how I handle multiple frame rates and different frames for each animation or sprite sheet. I have an editor that I use to build the frames that uses IntermediateSerializer and the animations are cloneable because I reuse the same sprites at times in my games.

SpriteFrame - Represents a list of source rectangles for an animated sprite

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;

namespace Psilibrary.SpriteClasses
{
    public class SpriteFrame
    {
        private string _textureName;
        private List<Rectangle> _frames;
        private int _frameCount;
        private int _frameRate;
        private int _frameWidth;
        private int _frameHeight;

        [ContentSerializer]
        public string TextureName 
        {
            get { return _textureName; }
            private set { _textureName = value; }
        }

        [ContentSerializer]
        public List<Rectangle> Frames 
        {
            get { return _frames; }
            private set { _frames = value; }
        }

        [ContentSerializer]
        public int FrameCount 
        {
            get { return _frameCount; }
            private set { _frameCount = value; }
        }

        [ContentSerializer]
        public int FrameRate
        {
            get { return _frameRate; }
            private set { _frameRate = value; }
        }

        [ContentSerializer]
        public int FrameWidth
        {
            get { return _frameWidth; }
            private set { _frameWidth = value; }
        }

        [ContentSerializer]
        public int FrameHeight
        {
            get { return _frameHeight; }
            private set { _frameHeight = value; }
        }

        private SpriteFrame()
        {
            _frames = new List<Rectangle>();
        }

        public SpriteFrame(string textureName, int textureWidth, int textureHeight, int frameWidth, int frameHeight, int frameCount, int frameRate = 8)
        {
            this._textureName = textureName;
            this._frameWidth = frameWidth;
            this._frameHeight = frameHeight;
            this._frameCount = frameCount;
            this._frameRate = frameRate;

            _frames = new List<Rectangle>();

            int xOffset = 0;
            int yOffset = 0;

            for (int i = 0; i < frameCount; i++)
            {
                Rectangle r = new Rectangle(xOffset, yOffset, frameWidth, frameHeight);
                _frames.Add(r);
                xOffset += frameWidth;
                if (xOffset >= textureWidth)
                {
                    xOffset = 0;
                    yOffset += frameHeight;
                }
            }
        }

        internal SpriteFrame Clone()
        {
            SpriteFrame frame = new SpriteFrame();

            foreach (var f in _frames)
            {
                frame._frames.Add(new Rectangle(f.X, f.Y, f.Width, f.Height));
            }

            frame._frameCount = _frameCount;
            frame._frameHeight = _frameHeight;
            frame._frameRate = _frameRate;
            frame._frameWidth = _frameWidth;
            frame._textureName = _textureName;

            return frame;
        }
    }
}

SpriteAtlas - Collection of all of the SpriteFrames and Texture2Ds. In my case the sprites are broken up into different Texture2Ds per animation.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

namespace Psilibrary.SpriteClasses
{
    public class SpriteAtlas
    {
        private Dictionary<string, SpriteFrame> _animations = new Dictionary<string, SpriteFrame>();
        private Dictionary<string, Texture2D> _textures = new Dictionary<string, Texture2D>();

        [ContentSerializer]
        public Dictionary<string, SpriteFrame> Animations
        {
            get { return _animations; }
            private set { _animations = value; }
        }

        public SpriteAtlas()
        {

        }

        public Texture2D Texture2D(string name)
        {
            if (_textures.ContainsKey(name))
                return _textures[name];

            return null;
        }

        public void LoadTextures(ContentManager content, string folder)
        {
            foreach (string s in _animations.Keys)
            {
                if (!_textures.ContainsKey(s))
                {
                    Texture2D texture = content.Load<Texture2D>(folder + _animations[s].TextureName);
                    _textures.Add(s, texture);
                }
            }
        }

        internal SpriteAtlas Clone()
        {
            SpriteAtlas atlas = new SpriteAtlas();

            foreach (var t in _textures.Keys)
                atlas._textures.Add(t, _textures[t]);

            foreach (var t in _animations.Keys)
                atlas._animations.Add(t, _animations[t].Clone());

            return atlas;
        }
    }
}

AnimatedAtlasSprite - The actual sprite where animation occurs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Psilibrary.SpriteClasses;
using SharpDX.Diagnostics;

namespace Psilibrary.TileEngine
{
    public class AnimatedAtlasSprite
    {
        private readonly SpriteAtlas _atlas;
        private string _currentAnimation;
        private int _currentFrame;
        public Vector2 Position;
        private TimeSpan _frameTimer;
        private TimeSpan _frameLength;

        public SpriteAtlas SpriteAtlas
        {
            get { return _atlas; }
        }

        public string CurrentAnimation
        {
            get { return _currentAnimation; }
        }

        public Vector2 Origin
        {
            get { return new Vector2(Engine.TileWidth / 2, Engine.TileHeight / 2); }
        }

        public int Width
        {
            get { return Engine.TileWidth; }
        }

        public int Height
        {
            get { return Engine.TileHeight; }
        }

        public AnimatedAtlasSprite(SpriteAtlas atlas)
        {
            this._atlas = atlas;
        }

        public Rectangle Bounds
        {
            get { return new Rectangle((int)Position.X, (int)Position.Y, Width, Height); }
        }

        public void ChangeAnimation(string animation)
        {
            if (_atlas.Animations.ContainsKey(animation))
            {
                _currentAnimation = animation;
                _currentFrame = 0;
                _frameTimer = TimeSpan.Zero;
                _frameLength = TimeSpan.FromSeconds(1 / (double)_atlas.Animations[animation].FrameRate);
            }
        }

        public void Update(GameTime gameTime)
        {
            _frameTimer += gameTime.ElapsedGameTime;

            if (_frameTimer >= _frameLength)
            {
                _frameTimer = TimeSpan.Zero;
                _currentFrame = (_currentFrame + 1) % _atlas.Animations[_currentAnimation].Frames.Count;
            }
        }

        public virtual void Draw(GameTime gameTime, SpriteBatch spriteBatch)
        {
            spriteBatch.Draw(
                _atlas.Texture2D(_currentAnimation),
                new Rectangle((int)Position.X, (int)Position.Y, Engine.TileWidth, Engine.TileHeight),
                _atlas.Animations[_currentAnimation].Frames[_currentFrame],
                Color.White);
        }

        public void LockToMap(Point mapSize)
        {
            Position.X = MathHelper.Clamp(Position.X, 0, mapSize.X - Engine.TileWidth);
            Position.Y = MathHelper.Clamp(Position.Y, 0, mapSize.Y - Engine.TileHeight);
        }

        public void Draw(GameTime gameTime, SpriteBatch spriteBatch, Rectangle destinationRect)
        {
            spriteBatch.Draw(
                _atlas.Texture2D(_currentAnimation),
                destinationRect,
                _atlas.Animations[_currentAnimation].Frames[_currentFrame],
                Color.White);
        }

        public bool ContainsPoint(Point p)
        {
            return Bounds.Contains(p);
        }

        public object Clone()
        {
            AnimatedAtlasSprite sprite = new AnimatedAtlasSprite(_atlas.Clone());

            sprite._currentAnimation = _currentAnimation;
            sprite._currentFrame = _currentFrame;
            sprite.Position = Position;
            sprite._frameLength = _frameLength;
            sprite._frameTimer = _frameTimer;

            return sprite;
        }
    }
}
1 Like

I am slowly making some “progress” reaching the working state I had again using my own classes which I try to keep very simple wrappers for the information needed to play an animation. Later I want to add more functionality when needed. Still wondering if it is possible to have my own Vertex shader when using an effect for SpriteBatch? I could then simplify the calculation of normalized uv coordinates (uv in the [0,1] range). At the moment I am simply providing precalculated parameters for each keyframe / source rectangle to the effect to translate and then scale the uv coordinates in the pixel shader. Next steps will be to do these calculations using matrices and to use indices in some form to “define” animations. It is now possible to play animations which have keyframes with different timings.

1 Like

Altough I wanted to upload something yesterday only today I found some time to make a short demo video. Hope it is a little entertaining :slight_smile:

The second and third keyframe is played 4 times at 5x speed. This is how I read the sprite sheet information above below the chest throw animation. I think it looks good this way :slight_smile:

The individual animation clips and current keyframes are marked in the sprite sheets with diagonal lines.

The background of the character Randi are the normalized uv or texture coordinates calculated for each keyframe by matrix multiplication in the pixel shader (u mapped to red, v mapped to green and blue set to 1). The time (keyframe index over total number of keyframes minus one) is used as a factor which makes the first keyframe black and the last keyframe full brightness.

With these sprite sheets I have to use source rectangles with different widths in some animations because using a single width for all keyframes would sometimes cut off / be to small or include part of the next keyframe / be to big. What is the best way to deal with this? I mean I could use different widths and use the widths also in the destination rectangles. Or I could rearrange the sprite sheet keyframes to occupy source rectangles of the same size. This way I won’t get stretching like when using the same size destination rectangle in combination with source rectangles of different size.