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.
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.
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.
SimpleAnimationis one list of frames that loops.SwitchAnimationholds several groups of frames, andSwitchAnimations()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.
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
BaseMovementAnimationstill throwsNotImplementedException. - Dynamic access. I tried to make the animator
dynamic, so you could writeanimator.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.Timerfires on a thread-pool thread, and WinForms controls should only be touched from the UI thread. A WinFormsTimerorControl.Invokewould 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.