SakeTami
Domo Yoro
Domo Yoro

patreon


How Bottlecaps Became One of the Most Complex Systems in the Game

A post about a very long journey to animate infinite bottlecaps.

Note: this will have some spoilers for bottlecaps in the upcoming build.

The Bottlecap Concept

Following the theming of a penguin strapped to a soda bottle, bottlecaps immediately made sense as a collectible. This mirrors the real-world hobby of bottlecap collecting: countless logos and brands and designs stamped on uniform metal crowns.

My own bottlecap collection. I only started collecting in September 2024, well after this post was made.

Bottlecaps were in the game as early as the game’s 3 month prototype. Pictured below are some of the iterations they (and the vending machines) went through.

Designing the bottlecaps

Real bottlecaps have a very distinctive shape- it was difficult to capture that with pixel art. With the front-facing view, they could be interpreted as small saws or ribbons. In order to better convey the bottlecap form, I opted for a turnaround animation. Plus, the animation helps make the bottlecaps stand out against the scenery- classic collectible design 101.

Some early designs for the bottlecap. I tried plastic for a bit!

While I had always wanted to do multiple bottlecap labels, it wasn’t a priority for the first two years of the game’s development. That changed when I wanted to put out the game’s first public demo, and needed to demonstrate the game visually. It was finally time to try multiple bottlecap designs!

3D Rotation

If the goal is to have tons of different bottlecap designs, then a 3D model makes a ton of sense- the label is a separate texture that could easily be swapped out. So let’s try that!

In-editor view

Looking good so far! Let’s see what it looks like in-game…

In-game view

Hmm, this feels a lot messier. The pixels are jagged on every frame of animation, and it’s hard to read the label.

What a mess!

Of course, this is only a quick mockup; they’re really just 3 Sprite Renderers stacked on top of each other. If I had experience making 3D models, maybe I would be able to get something that looked acceptable… but I don’t! So let’s revisit the 2D animation concept again.

A 2D Animation of A 3D Rotation

Bottlecaps are small- they’re 20x20px sprites. The labels are even smaller, measuring at just 14x14px circles. Being able to read these labels at such a small size is vital to their appeal.

And you know what? It turns out that animating these unique bottlecap label rotations is a very manageable workload. Even better, key details of the label can be preserved which makes it easy to “read'' even at the more extreme angles.

Much better!

While the 3D model rotation is technically more correct, the readability here is vastly preferable. As said by a professional animator friend- good animation is cheating!

Now we’ve figured out how the bottlecap assets are going to work, we’ve gotta figure out how to implement them.

How Sprites Are Animated In Unity

The way animations are set up within the Unity engine involve a few different things:

Texture- a 2D image file. In SPP, these often represent a single animation or a group of objects.

Sprite- a rectangular slice of a texture. Each white rectangle here is its own sprite.

Animation- has data for the order to display these sprites in, how long to display them, etc.

Animator Controller- controls which animations are playing, often contains many animations.

For complex animators like the penguin, this is great for having a lot of control. For simple objects- like balloons- that only have one or two animations, this can be pretty tedious to set up.

Note that even though all three balloons have the same four-frame idle and popping animation, all of them need their own animation files and animator controllers

“Tedious” wouldn’t even begin to explain what setting all of this up for every unique bottlecap would be like. If I were to create individual animation files and animators for every bottlecap, it’d become a mess- and 100% prone to error. There’d be duplicated frames, off-center animations, mislabeled files, and so on. So we need to find a way to animate these bottlecap labels procedurally.

The Plan

Let’s go back to what a sprite is. A sprite isn’t image data- it’s a portion of a larger texture.

As shown here, it’s just pixel coordinates defining a rectangle. What if we re-used these pixel coordinates on a different texture?

Here’s the plan: the bottlecap will use a “base” texture/ animation. After we’ve drawn the base animation, we’ll take the pixel coordinates of those sprites and apply them to a different texture- the label texture- and from there, we’ll draw the label on top of the base animation.

Re-using the pixel coordinates from the bottlecap base for the bottlecap label also means that the textures have to have the exact same size- and given that we’re working with pixel art here, extra texture space really isn’t a big deal.

In order two draw two textures at once, we’re going to use a shader.

Source: https://www.alanzucconi.com/2015/06/10/a-gentle-introduction-to-shaders-in-unity3d/

We’ll pass the label texture in through the material. These materials will be generated at runtime, meaning that they’ll only exist for as long as the bottlecap is loaded.

(Materials are a group of data that gets passed into the shader- usually, this is a texture with some tiling / offset information, but really they can hold anything)

It works!

…oh, the number of draw calls has doubled.

(Draw calls are CPU commands to the GPU to render geometry.)

A normal scene in SPP only has 10-20 draw calls, but now every bottlecap has its own draw call too.  So if you decided to collect every bottlecap in the level and get a long bottlecap train going, this would strain the CPU. This would be particularly upsetting if the game started lagging and this led to a death- destroying the bottlecap train you had going.

Simulation of what could happen.

I might also still be frustrated about that one time where I died to lag halfway through Celete’s 3A chapter while carrying a golden strawberry.

So… we gotta find a way to batch these sprites together into a single draw call. Meaning: all bottlecaps must use the same material- and the same texture.

How Are Sprite Renderers Supposed To Be Batched?

So, hold on. In order for normal Sprite Renderers to be batched together, they must have the same material- and the same texture. But wait- the balloon sprites are stored in a different texture than the level decorations, so how is the game getting away with only 10 or so draw calls?

The answer? They’re not in a different texture- they’ve actually been merged together in a single master texture via the Sprite Atlas.

A cropped view of the master Sprite Atlas. The sprite edges can get pretty funky, but it looks fine in-game

The Sprite Atlas is a very useful tool that takes tons of sprites from different textures, and packs them together as tightly as it can. This means that the GPU only needs to keep a single, larger texture in memory, instead of a bunch of smaller textures.

What this all means is that the data for which sprite to display is being encoded into the mesh itself. Perhaps we can do the same thing for the bottlecaps!

Wait, what’s a mesh?

The Mesh

Let’s get something out of the way first: the mesh.

Meshes are classes of data that contain points which form triangles- these triangles then have a texture mapped onto them. In Unity, even 2D sprites are built as flat meshes.

This means that a Sprite Renderer is really just a mesh generator- it takes a sprite, generates a mesh for it, and correctly maps the texture.

The New Plan: One Material & Per-Renderer Data

New plan: let’s merge all of the bottlecap label textures into a single master texture with a Sprite Atlas, then encode the coordinates for the bottlecap label we want inside the mesh data.

Here, the colored number represents the pixel height that we intend to grab the bottlecap from.

Well, technically, the master texture looks something like this:

So we have two pieces of data to encode: the X position and Y position of our label sprite.

For a normal 3D game, it’d be something as simple as setting the mesh’s secondary texture mapping- often used for normal maps. But we’re using Sprite Renderers, which has full control over the mesh. Long story short: modifying the texture mapping isn’t an option.

Let’s revisit this diagram:

We don’t have control over the UV data (texture mapping) or the Normals (vertex directions) here.

In order to encode the data, we have to find a property of the mesh that we’re willing to give up, or is unused. Since this is a 2D game… what if we encoded our data in the Z-axis?

Encoding the Z-Axis

The Sprite Renderer mesh is flat, so all vertex positions should have the same Z coordinate. This means that we can set the Z position of the entire Sprite Renderer, and access the data within the shader.

That said, we still have two pieces of data to encode- X and Y pixel coordinates. How do we do that with a single value?

Even though this is one “number”, I could have the pixel coordinates represented by having the X position be the whole number, and the Y position be the decimal. (Especially since there’s a lot more rows than columns.)

And hey, it works in the editor!... but there’s something funky going on in the game view. Sometimes shaders have differences between being rendered in-editor and in-game.

For example, the alpha isn’t clipped properly in the file select menu in the editor game view… but in the editor scene view, and in the game build, it’s fine!

Left: scene view. Right: in-editor game view.

But nope, the build view of the Z-encoded bottlecaps are identical to the game view in Unity editor.

After thinking about it, I realized: Because the game uses a 2D Pixel Perfect renderer, all meshes have their positions snapped to pixel perfect coordinates- which is good! But it also means that all Z values are snapped as well, even though you never see it.

So vertex positions won’t work, and we don’t have control vertex normals or texture mapping; that just leaves vertex colors. This is exposed as a single color property on the Sprite Renderer, which controls all vertex colors on the mesh.

Encoding Colors

Each Sprite Renderer in Unity has a color property- encoded in RGBA. This color property is a multiplier on the sprite’s colors- if you multiply a sprite by pure red (1, 0, 0), then all colors will be somewhere between black (0, 0, 0) and red (1, 0, 0).

Red (1, 0, 0) x Blue (0, 0, 1) = Black (0, 0, 0)

White (1, 1, 1) x Red (1, 0, 0) = Red (1, 0, 0)

For 99% of sprites in the game, this is set to (1, 1, 1, 1)- white, with full alpha. For the pixel art medium, it is important that every pixel color is intentionally picked from a finite palette. So we're not really using the color property at all here.

As it turns out- this Sprite Renderer color property actually controls the mesh’s vertex colors. We can do something with that!

Thankfully, vertex colors don’t have to affect the sprite’s output color. The color multiplication has to be manually implemented in every shader- the default materials just have it by default. So if we don’t want this color multiplication… we just don’t implement it!

Manual implementation of color multiplication. We’ll be removing the line in the middle.

There is one small snag- our values have to be floats between 0 and 1. We’re trying to encode pixel coordinates that can range from 0 to 512, with 512 being the atlas height and width. Let’s just divide our pixel coordinates by those dimensions, and put them into the red and green channels.

Initial tests with a handful of bottlecaps went great- the labels were dead-on. However, once I dumped all of the bottlecap labels into the Sprite Atlas- and the Sprite Atlas texture dimensions changed from 512x512px to 1024x1024px- I ran into some problems.

Invisible Offset

Non-integer pixel offsets

So- this was mostly working. The offsets are definitely a problem, but the offset is consistent for each frame of animation. This means we can deduce that the vertex color is to blame, and the data I was sending in through the sprite renderer’s color was being modified somehow.

I already spent a whole weekend trying to solve this problem- which is way too long for an optimization meant for a demo. I had to settle for a quick solution, so…

A Pattern?

I realized that the offset of each bottlecap corresponded to its position within the Sprite Atlas. If the bottlecap was in the upper half of the Sprite Atlas, then its offset would be positive- and if it were in the lower half of the Sprite Atlas, then the offset would be negative. The offset would get stronger as it got closer to the middle.

It was almost as if there was an invisible row in the middle of the Sprite Atlas

If there’s a pattern, then I can write an equation to counteract this pattern. I took some measurements and came up with the following code:

A very, very temporary solution- this equation would break if I added any more bottlecaps beyond those found in the first Patreon Build. In fact, I had these Pizza Tower themed bottlecaps sitting around unused for several months.

That said, the game doesn’t need unique bottlecaps to be added when developing levels. This was a task that would be saved for another day.

Solving The Problem For Real For Real This Time

That finally brings us to the present: developing the next Patreon build. Even though the level’s visuals are a placeholder, I still want every bottlecap to have a unique visual. So it was finally time to solve this problem, for real.

This time, instead of scaling up from a small example, it’d be better to scale down from a large example- that way I can be confident that this problem remains solved for the full scope of the game. So I duplicated the entire bottlecap labels folder 4 times for somewhere around ~250 labels, generating a Sprite Atlas the size of 2048x2048px

Unlike the smaller atlas of 1024x1024px, the offset error of this larger atlas had a much more complex pattern. It almost seemed as if these positions were being snapped to some other grid.

Even though I’m using floats (0 to 1) as an input in the Sprite Renderer color property, and receiving floats (0 to 1) as an output in the shader, somewhere along the way the numbers are being rounded to specific intervals.

Values of 1.4 being rounded to intervals of 1

The real numbers are values of (1/2048) being rounded to intervals of (1 / ?)

Hold on… there is a common color format that might explain this. You might know it as hexidecimal color values. Here, each color channel can range from 0 to 255, with (255, 255, 255) being pure white. Unity has a format like that called Color32, with the 32 representing the number of bits the data uses.

Sure enough… this is the solution I was looking for. My floats (0 to 1) were being rounded to intervals of 1 / 255.

One Last Optimization

So, the numbers we’ve been trying to encode this whole time have been (pixel x coordinate / texture width, pixel y coordinate / texture height). The problem is that these numbers are far too granular to fit into the Color32 format- where each value ranges from 0 to 255. We’re trying to send in things like (386 / 2048, 306 / 2048), but we’ll lose information no matter what.

So instead of trying to send pixel coordinates, why don’t we send label coordinates? Every label sheet is arranged neatly in a grid, after all.

Final Result

Finally!

It was incredibly frustrating to have to reverse-engineer what was going on with my numbers; there is no mention of vertex colors being limited to a 32 byte format. Every reference to vertex colors I could find accepted both Color and Color32. There are no compiler warnings for using Color over Color32. I was on my own.

But, I am happy to have finally finished this project. New bottlecap labels are incredibly easy to add, change, and remove. All together, they only use one draw call. The world is at peace once again.

Questions And Answers

Why does the label appear behind the bottlecap rim?

The data for the rim’s sprite rectangle is available as a part of the mesh, before the sprite is drawn. So I actually draw the label before the rim.

I figured it’d be better this way in case the label has any extra pixels at the edges. I’ll likely change this in the future, as drawing the label second means that I can customize the back of the bottlecap as well.

The bottlecap has some other animations- how do those work?

While the red and green channels are being used for sprite positions, the blue channel actually controls the transparency of the label. The animation sets the sprite’s blue channel, which then hides the label.

The alpha channel is used normally- for transparency of the entire sprite.

Could you create animations and animator controllers at runtime?

Yes, but it’d likely mean using a lot more memory. Every animation would have the same data for the timing of the keyframes- with the only thing changing between animations being the sprite itself. If we’re only changing the sprite, then a shader just makes the most sense!

Why do bottlecaps made in January 2023 show up when most of the work was done in 2022?

All of the visuals here are recreations of the problems I encountered. While I did save some evidence at the time, it’d be easier to explain using new visuals.

How Bottlecaps Became One of the Most Complex Systems in the Game

More Creators