I think about this on occasion. Most recently I ran into an issue during a personal project: 2d sprites for RTS units were packed on spritesheets in a consistent manner: 5 sprites for 8 directions (you mirror 3). Packed in order of: stand, move, attack, die. So I made a loader that understands how to take action + direction and offer an array of sprites to play through.
But then I came across more cases: sprites with no directionality (an explosion), and corpse sprites (which were only 4 directions, 2 mirrors, and most except the first four were shared by both orcs and humans).
I agonized for a little bit on what the hell the common abstraction is for all this. In the end, I factored out some of the loading code, and made a UnitLoader, CorpseLoader, EffectLoader and moved on. Now, there's probably a better abstraction in there because all 3 loaders have to reason about the same things a little bit. But I will discover that abstraction later on and it's easier to just de-duplicate the code then, rather than try to identify the abstraction now and make some complicated EverythingLoader that handles all those cases.
I like this quote, "things should be made as simple as possible, but no simpler."
I think the natural instinct with programming is to try and simplify the code by means of generalization. But we often over-simplify, and reality is messy. Or as TFA mentions, time passes and new requirements arise, so it turns out that we have simplified prematurely!
Sounds like this should be an aphorism. Premature abstraction is the root of much suck!
You probably already have the common abstraction factored - the code to load pixels for a single sprite, and to display it? It makes sense to me that the level above that, interpreting the sprite sheet layout and modes of playback, come in different flavors and don’t have a common abstraction that fits all cases.
Personally I prefer what you’re doing over trying to come up with a non-obvious abstraction or trying to make an imperfect abstraction fit. Waiting til the abstraction is totally obvious and the need is crystal clear is a good thing.
The flipside (antidote?) of DRY is WET - write everything twice/thrice. More important, IMO, is to abstract only over things I have an actual, demonstrated use case for, usually demonstrated first via duplication, and not speculate about possible future uses I might want. Code written for future use cases we don’t have is so often the code that gets in the way of abstracting the things we do have, and it cracks me up when that happens.
> Waiting til the abstraction is totally obvious and the need is crystal clear is a good thing.
I discovered this after a few early years of my career being a bit of a “best practices” zealot. The thing I say often at work is, “let’s get this shipped to prod so we can start learning all the things we don’t yet know about it.”
This is the way. Making games is supposed to be fun. You can do the hard boring stuff when you get to the final 10% of the project.
Besides, sometimes your duplication creates "bugs" which may turn out to be fun features that players enjoy.