projects / banim

BAnim

Frame-by-frame sprite animation library for C# WinForms, built during my A-level project as a stand-in for a Unity animation engine I wasn't allowed to use.

Overview

BAnim is a small frame-by-frame animation library for C# WinForms. You give it the frames of an animation and the PictureBox to draw into, and an Animator steps every registered animation forward on a timer. I wrote it in April 2023, during my A-level Computer Science project, as a separate class library with two small example apps.

The problem

The A-level project was worth 20% of the grade, and one of the ideas I explored early on was a platform exploration game with different areas to move through. Around the same time I watched aarthificial's videos about the custom frame-by-frame animation system he built in Unity, and I wanted something like that for my game.

Unity wasn't allowed for the project, though. The coursework had to be in C#, WinForms was what I could use, and WinForms has nothing for sprite animation beyond showing an image. So I wrote the animation part myself, and I made it a separate library so the game, or anything else, could reference it.

In the end I picked a different idea for the graded project, so the game never happened. BAnim is what was left over from that exploration.

How it works

There are two layers. The Animator owns the timing, and the animations own their frames.

One tick of the animator
BAnim/Animator.csview source ↗
        private Timer FrameTimer { get; }                private readonly IDictionary<IAnimation, Action> _animations = new Dictionary<IAnimation, Action>();        // ...        public Animator(double frame_change)        {            FrameTimer = new Timer(frame_change);            FrameTimer.Elapsed += NextFrameCall;            FrameTimer.Start();        }        // ...        private void NextFrameCall(object sender, ElapsedEventArgs e)        {            foreach (var entry in _animations)            {                entry.Value();            }        }        // ...        public void Add(IAnimation item) =>             _animations[item] = () => item.NextFrame();

The animator has one timerL5-10 for everything, set in milliseconds per frame. Each animation is stored with an actionL20-21 that moves it on, and every tickL12-18 runs all of them. One shared clock means every sprite on screen changes frame together, and changing the speed of the whole scene is a single call (UpdateFrameChange). The example app uses that for a slider.

Every animation implements IAnimation (start, stop, toggle, next frame, and an event for when the frames loop). Most of the work happens in BaseAnimation.

BAnim/Animation/Support Systems/BaseAnimation.csview source ↗
        public virtual void StartAnimation()        {            CurrentFrame = 0;            ToggleAnimation(true);        }        /// <summary>        /// This method should not be being called outside of the Animator Class.         /// </summary>        public virtual void NextFrame()        {            if (!IsPlaying) return;            if (CurrentFrame == Frames.Count)            {                OnFramesFinished?.Invoke(this);                CurrentFrame = 0;            }            var frame = Frames[CurrentFrame];            Box.Image = frame;            CurrentFrame++;        }

A paused animation does nothingL12 on a tick. When it runs past the last frameL14-19, it raises OnFramesFinished and wraps around, which is the hook a game would use to go from a "jump" animation back to "idle". Then it puts the current frame into the PictureBoxL21-23. The constructors take frames as file paths, Image objects or raw bytes, and load them all into memory up front, so a tick never touches the disk.

On top of that there are two kinds of animation.

  • SimpleAnimation is one list of frames that loops.
  • SwitchAnimation holds several groups of frames, and SwitchAnimations() moves to the next group without restarting the timer. The example app has a character's idle animation with and without a hat, swapped with a button.
The example app in April 2023: the idle animation loops, Toggle Animation swaps in the hat without restarting the timer, and the other buttons stop, start and toggle the player

Outcome

It worked for what it was. It gave me smooth looping sprites in WinForms, a speed slider and swapping animations at runtime, all in a library separate from any one app. Most of it was written in two days (9 and 10 April 2023), with switch animations and the shared interface added over the following week.

Looking at it now, it also has some loose ends.

  • Movement animations (moving or resizing a control over time) were started and never finished. Every method in BaseMovementAnimation still throws NotImplementedException.
  • Dynamic access. I tried to make the animator dynamic, so you could write animator.idle. It looks up a string-keyed dictionary that the animator doesn't actually have, so as far as I can tell it never worked.
  • Threading. System.Timers.Timer fires on a thread-pool thread, and WinForms controls should only be touched from the UI thread. A WinForms Timer or Control.Invoke would have been the safer choice.

It was also the first time I split code out into a library with its own interface and examples. I took that further a year later with Savers, the database library I did end up using in my A-level project.