News

Blog: How painting works in Chicory: A Colorful Tale


The following blog post, unless otherwise noted, was written by a member of Gamasutra’s community.
The thoughts and opinions expressed are those of the writer and not Gamasutra or its parent company.

 

 

 

Chicory: A Colorful Tale is a game that’s allllll about painting. Making it look good and feel good is a top priority for this game, but that brings lots of tough technical challenges. Multiple players have to be able to draw anywhere on the screen, any time, all at the same time together. All the objects in the game have to react to it instantly. And it has to do all of this while running as fast as possible, 60 times a second!

Figuring out how to make it work was one of my very first considerations, and as the game’s design has developed, the inner workings have developed with it to match. You probably aren’t going to make a mechanic like this in your own game, but if you’re anything like me, you know it’s super fun to learn how things were made — and along the way, I’ll get to show off lots of little techniques that might come in handy for you. 🙂 So let’s talk about how it works!

THE HIDDEN GRID

Chicory

Visualization for the underlying grid.

Internally, there’s an array which represents all the paint on the screen. As shown, you can imagine a small grid overlaying the level. The content of each cell tells us what color that spot is; 0 is unpainted, 1–4 represent the 4 possible colors that spot could be painted. (You can’t mix colors or anything like that — if you paint a new color over an area, it replaces the colors there completely).

It’s really important that we maintain the state of the paint this way, rather than just having a surface the player draws on. There are two good reasons: one, surfaces are not stable. If the player minimizes the game, or anything goes wrong with their graphics card, all the surfaces can disappear and we’ll have to be able to reconstruct them. Two, all the gameplay logic cares quite a bit about the paint, so we need an optimal way to check for things like “Is this spot painted?” or “How much of X color is in this area?,” etc. Checks like this happen many many times every frame, and sometimes they can get quite complex, with some object doing things like pathfinding through painted areas… so, keeping it all in data is super important.

 

Chicory

The smallest dot the player can make, roughly 12×12 pixels.

Using 12×12 pixel cells also means that not every pixel on the screen is unique; the player actually has a relatively low “resolution” that they can paint on. This is really helpful for optimization (if we used every single pixel, it would go 144x slower!), but it’s also helpful for gameplay. 12×12 pixels was chosen because it’s still quite small, but big enough that one dot has good clarity on the screen… I was worried about puzzle elements getting into ambiguous states because of tiny unintended pixels of paint interfering with them. It’s important that “painted” and “not painted” are clear and easy to distinguish. Making it a bit bigger helps with that a lot.

BRINGING IT TO LIFE

If we rendered the paint as 12×12 pixel squares, it would just look like pixel art blocks on the ground… but we had to make it look nice and curvy and blobby, like paint! So how did we do that? If you’re not familiar with this technique, I’m really excited to introduce you to it: it’s called Marching Squares!

Chicory

Image taken from Wikipedia

Marching Squares is really cool because it can take lower-resolution data, like our paint squares, and interpolate nice-looking shapes from it. It’s fast to compute, and you can recalculate just a small section of your data set without having to re-examine the entire thing. It’s useful for all sorts of graphics things, and procedural terrain generation, and luckily enough for us, for rendering paint!

 

Chicory

The sprite set we use for paint rendering

So, when rendering the paint, we go through square by square and look at each corner — then render a 12×12 pixel shape to represent the shape of the paint in that tiny area. We do a pass like this for each color on the ground. Most tiles will be either empty or filled, but when there’s a bit of both, Marching Squares creates a nice set of edges!

Chicory

 

The same grid, offset to show how the rendering thinks about it. Notice how each cell is either filled with paint, empty, or contains simple lines through its center

Here’s a simplified version of how it looks in code:Chicory

After rendering the paint edges to a surface this way, we render that surface to the screen with a shader that randomly offsets the pixels, making the paint shapes more random and jaggedy so there are no straight lines.

UPDATING THE STATE

Like I said before, this game is ALL about painting… Players will be using their cursor to draw on the screen, and it has to feel expressive like a real art tool, so it’s vitally important that the game updates as fast as possible to make it feel real and reactive to the player’s actions. That means it needs to be able to update and render it, perfectly, every frame, 60 times a second, on any platform. All of my technical decisions follow from this rule.

To keep things fast and organized, I keep all paint changes to a specific order. All updates to the paint array happen in the Step event. All objects then react to any paint changes in the End Step event, for example to change color because they were painted. Then all the changes are rendered and discarded in the Draw event.

Chicory

There are a TON of objects which manipulate the paint surface, painting or erasing things as needed… the player’s brush is an obvious example, but there are all kinds of plants, creatures and environmental elements which do something to the paint on the screen — making a splashy burst of paint, eating and erasing it in an area, etc. Despite the wide variety of interactions, there’s just one script in the game which directly handles paint manipulation, which all those different mechanics eventually reduce to. Here’s what it looks like:

Chicory

Slightly modified/simplified for clarity. “paint_res” is typically 12, which converts the array space to the screen space — the input x and y are always rounded/scaled down to the array space.

This script takes a spot in the paint array and updates it with a new color. For events that paint multiple points (ex. the lines the player draws or an explosion), they’ll iterate over the area they’re painting and run this script point by point.

Besides updating the given spot with the new paint color, it also makes sure that the new color is different from the one already down — and if it is, it logs that the paint has updated this frame, logs that paint updated at this specific spot, and also logs the top-left and bottom-right corner of the region where all paint has updated this frame. All of this information is used by other objects to check, update and react to the new paint changes during their End Step events.

OPTIMIZING THE RENDERING

So now every frame we have an array telling us each spot in the room whose paint updated, as well as the bounds for the area where the changes happened. The most straightforward way to render the paint would be to iterate through those bounds and render each spot inside there, right? As you can probably tell, this article has a bit longer to go, so, actually no.

Consider if there are two players painting together, one on the top left corner of the screen and one on the bottom right. Even if they’re just making tiny dots, the bounds of those changes would include the entire screen of paint, and we’d end up re-rendering the entire screen every frame… not optimal. I had to go deeper. So rendering happens in 3 basic steps.

Using the array of changes and the bounds, find a set of smaller rectangular areas within which we need to re-render the paint.
Erase all the paint currently inside those areas.
Finally, render the paint inside those areas using the current paint array.

The reason why 2 and 3 are separate steps, rather than just erasing and re-rendering as we go, is because to switch from “erase” to “draw” we have to change blend modes, which would slow down our game quite a bit if we did it many times per frame. (Full disclosure — I actually started out doing it that way, until I later realized how much it was slowing us down).

You can see it in action here:

Chicory

The first thing we do is make 2 queues, “rend” and “alt_rend.” Using our paint changes and bounds, the script “queue_subdiv” populates “rend” with x and y values for rectangular areas that need to be updated. We only need to figure that out once, then that information is copied over to alt_rend and used for the erase step and again for the render step. ds_queue_copy is extremely fast, as well as ds_queue_dequeue, so we get good speed from this method, although it may seem a bit strange.

Here’s what the guts of queue_subdiv looks like:

Chicory

As you can see, it’s a recursive function. The methodology for it was inspired by Quadtrees. It has a target “size” for its rectangular areas, which I chose entirely using trial and error with speed profiling. It looks at the current rectangle, and if it’s bigger than that target size, it splits it in half and half again, then searches inside each of those 4 pieces. At each step it also searches from the corners inward and shrinks the bounds to only the changed area inside the rectangle, reducing wasted rendering as much as possible. By the end it’s found a set of rectangles that snugly contain all of the paint changes for the current frame.

THE END

There’s just one last step… resetting it so we can start all over on the next frame!

 

Chicory

Rather than deleting/remaking the array, or iterating through the entire thing, we clear ONLY the cells that updated this frame… with trial and error, we found this was the most optimal way to do it.

…And that’s how it works! Along the way making this, there were many many missteps and dead ends and poor attempts before we arrived at the current methods. Who knows, it might even continue to change as we go. But for now it runs pretty great and the system is working! Thanks for reading, I hope it helped you learn something!

Chicory

Aaaaand PS- If the game sounds interesting to you, please consider supporting us on Kickstarter! =)

Show More
Back to top button