Comet Dev: Hard Mode
Hey guys, it's been a minute since we last spoke.
The game has gone through a major visual upgrade thanks to Kirill (our level & cutscene artist) and Jan (our programmer).
We were able to achieve this by building out support for more features in LDtk, the tool we use to design Comet, as well optimizing our fairly complicated drawing pipeline.
The result is a world that feels much more rich and organic.
Let’s go ahead and explain the journey we took to get here.
The Forest for the Trees
Every gameplay scene in Comet is put together from a fairly complicated set of 17 layers. All of these layers work together to:
- Ensure scenes look nice
Making sure tiles look organic and fun to walk around, while we cover all the edge cases which pop up. - Make sure everything fits together visually
That means making sure we have the right foreground tiles so Stella doesn’t appear in front of a building when she’s supposed to be behind it. - Support the unique lighting system
This is more complicated than you think! More on that soon.
Before Jan began helping us out 10 months ago, each new layer we created caused some performance challenges, so we stuck to a comfortable set of layers and did a few funky things to make them work. Now, we’re only limited by what we can draw, and we don’t even need workarounds!
One of our current layers is a ‘dark layer’ which we use to show unlit outlines of objects at night! Once Kirill finished upgrading the visuals of all our buildings, I asked him to update the look of our dark layer.
I was going for a dotted outline idea that wasn’t too detailed but I’m not much of an artist and was keen to see what Kirill could cook up, but as I’ve come to learn over the years Kirill is the sort of guy who likes to do things the hard way.
Before he could start working on the dark layer details Kirill told me he needed to redesign all the trees in the light layers…
This was a massive pain in the butt.
The Hard Way
Let me explain.
In most games, levels or rooms are designed to push and pull players in different directions using environmental tools.
Let’s take Link’s Awakening for example. Trees, rivers, and cliffs are used as the building blocks of a room to subtly guide the player. It makes traversal feel interesting and levels feel… right. They feel natural, but they don’t start that way.
In Comet, we use LDtk’s auto tile feature to easily iterate and shape nice levels.
Being able to ‘paint’ levels with auto tiling saves a lot of time, but in the spirit of doing things the hard way Kirill wanted to design about a dozen different trees.
This means no auto tiles, and manually replacing all trees that have already been placed in the game (Difficulty +1️⃣).
Then Kirill wanted to be able to flip some sprites horizontally for variety. He can do that in LDtk but the Playdate SDK doesn’t support flipped tiles. A workaround was needed to actually make them work in-game. The Playdate LDtkImporter we use (created by Nic Magnier) has some support for this, but it needs to be fed a mirrored tileset… and manually mirroring it with every update is messy and tedious.
That meant Jan needed to build a way to automate this, ideally in a seamless way for people working on level design (Difficulty +2️⃣).
Jan was more than ready to take on the task!
As we played around with these new trees, I felt like there was an issue. The trees weren’t dense enough and it would be easy for a player to get frustrated not knowing which areas they could pass though and which they couldn’t.
I want people to read an area easily so they can focus on the puzzles.
Kirill fixed this by playing around with larger tree sprite building blocks
but the building blocks weren’t flexible enough, the variety couldn’t effectively fit where every tree previously stood.
“No worries”, Kirill said, “I’ll just place tiles on top of one another.”
But this, you guessed it, led to another issue. The culprit this time was: tile stacking 😓 (Difficulty +3️⃣).
UnStacking
Tile stacking is the holy grail for LDtk flexibility. It’s something I wanted for Comet back in 2022 when the project first moved to LDtk but it felt like too much work for the project at the time, so we settled on a funky workaround instead.
What’s tile stacking, you ask? Let me show you.
This feature was wholly unsupported by Nic’s importer but once again, Jan was willing to take on the challenge.
Jan told us to start building out levels, and trust that he would find a way to make tile stacking work for us (I’ll talk more about the technical challenge of tile stacking later).
So, we did some more tests and found a balance that looked great and was easily readable.
But after manually placing hundreds of trees in a light layer, you also need to manually place hundreds of tree outlines in the same location in the dark layer (Difficulty +4️⃣).
Kirill is smart, so he started recording his mouse inputs for placing light layer trees, then he’d shift to the dark layer and automate his mouse inputs to place dark layer trees in the same positions (Difficulty back down to +3️⃣!)
This is something we could have automated, but there were gaps in our programming support. It would have been a few months until tile stacking support was able to come online, so Kirill pushed forward to make this happen the hard-ish way.
Oh, but you also need Stella’s light layer sprite, and dark layer counterpart, to appear in front of, or behind, the tree sprites in the correct way. That’s two MORE layers that need to be set up in each room! (Difficulty +4️⃣).
Mirror Mirror
We moved ahead of Jan, who was working on the tile stacking conundrum, but we still needed to keep the game in a stable state so we could develop other areas and test.
The solution? We made ‘mirror levels’ (duplicate versions of levels we could safely edit) which would need to be swapped in once they were done (Difficulty +5️⃣).
Then, Jan came out of his cave and told us he’d cracked it. Tile stacking was in!
But with one issue…
Within each layer, the game would only see collision data from the top tile in the stack (Difficulty +6️⃣).
In other words: If you had a layer with 2 sprites on top of each other (e.g. the solid bottom stump of a tree you should walk in front of, and the non-solid top trunk/leaves of another tree that you should walk behind), the game would detect that the top sprite (the tree trunk/leaves top) was a non-solid object, and would make the bottom sprite (the stump) non-solid too. So the player would walk into, and on top of, the tree.
Sooooo we had to go through every level and manually check all collision surfaces and if there were issues, we had to place some ‘blocker’ entities to fix them. It was more manual work, but I was happy to take on the task.
And that brings us up to today.
We might have chosen the hard way to do things, and it took a lot of manual work, but the world of Comet is looking better than ever thanks to a lot of hard work by Kirill & Jan, so it was worth it.
Now I’ll throw over to Jan to unpack the more technical systems that enable this.
A Hat on a Hat
It’s relatively easy to understand the world that Stella runs around in.
As Donald explained earlier we have light layers and dark layers, but what you might not understand is that there’s actually a whole other world on top of that which we draw uniquely, then PUNCH through to create our lighting effect.
The dark layer has its own “dark Stella” that needs to be above some dark objects and behind other dark objects, just like regular Stella in the light. Without this effort, the world just feels wrong.
Left: Dark Stella working as intended!
Right: Dark Stella without any adjustments.
Accidental Efficiency
When compiling Comet for playtesting we needed to create three tilemaps for the Playdate device to run (tilemaps are grids of assets that make it easier to load things).
While I was working on the light and dark layers, I converted some of the assets into pre-renders for compiling.
Now when compiling a build for Comet, each level would be pre-processed into four static images:
- Light background
- Light foreground
And if the level is at night:
- Dark background
- Dark foreground
Game performance was basically the same after adding pre-renders… until I discovered that we can drop a lot of logic from the game to add a performance gain! Basically, adding pre-renders means we can remove tilemaps, which reduces the overall size of the game by 2 megabytes and gives us better performance!
Every Frame a Sequence
Let’s break down the steps even further.
For each frame of the game, we:
- Update the state
- Draw the light layer
- Prepare the dark layer
- Apply the lights
- Draw the completed dark layer
1. Update the state:
When running the game, we first process the current state of all entities that can move, animate, and all background animations. (There are special logic layers to more efficiently animate large areas such as water backgrounds or grass on a windy day).
Each sprite needs to update their position and their current appearance.
2. Draw the light layer:
We draw the background animations, then the background image, all the entities, and then the foreground image.
All entities are drawn based on their z-index (depth), so that they can overlap nicely.
3. Prepare the dark layer:
Then, separate from all of the previous drawings, we prepare the dark layer image. It is drawn similarly to the light layer, except only some entities are represented with a dark version of their sprite.
But the dark layer would cover it all, if we didn’t…
4. Apply the lights:
Lamps, torches, house windows!
All the light sources in the game have flickering animations so first we choose the correct image out of the animation, and then we stamp these lights into the dark layer. This includes Stella’s Lamp.
We use these stamps to create transparent "holes" in the dark layer so the light layer is visible.
The game needs to know if any given tile is lit or unlit so we can use it for game logic (such as when Stella is afraid of the dark).
For that purpose, we prepare the map of light cutouts before the layers — right after the entity update (all lights are also entities, so they get updated with all the others).
5. Draw the completed dark layer:
After the lights are in, we draw the fully prepared dark layer on top of the light world.
Once everything is done we have a full frame of the game.
As you can see in the infographic below, there are a lot of layers to this drawing onion.
Stack it, Flip it, and Reverse it
As we said above, we improved the support for tile flipping and added support for tile stacking to the PlaydateLDtkImporter.
When there’s time, we’ll try to add this functionality into the importer for everyone to enjoy!
We use a very old version of the tool that we’ve been rewriting for years, so integrating the new stuff back into the current version is not a small feat.
Some final technical details for the kind of people still reading this.
Tile flipping is supported by the Importer but you need to manually produce the flipped tilemap (the image with all the tiles flipped by both axes) and that is a drag, especially when the artists work directly on the tiles.
Instead, we added an automatic step that produces flipped tilemaps at the start of the game (it could also pre-render them into a file in the build process, but we don’t need that anymore).
Tile stacking is a bit more tricky.
Tile stacking is basically layering tiles on a single layer.
The LDtk editor allows the designers to place multiple images into one location on a single layer. However, the tilemaps in Playdate SDK are one-dimensional — it’s just a long list of numbers that assigns each location a specific tile image.
We didn’t want to rewrite the whole game logic and drop the standard tilemaps, so we added just a tiny rendering trick — when importing from LDtk, we set the stacked tiles aside and then render them on top of the traditionally rendered tilemap. (One of the reasons why stacking took us so long was that I prepared for a total rewrite. In the end, a smaller addition was absolutely enough for our purposes.)
Now, back to Donald.
I want to thank everyone who voted for Comet to win the Most Anticipated Game in the Playdate Community Awards 2024.
It’s a big honor.
We know lots of people are waiting to get their hands on the game.
Rest assured we’re still working hard on this project and I won't let it die!
If you haven’t already, be sure to wishlist Comet on Catalog.
Until next time 👋
Get Comet
Comet
A Light-hearted puzzle adventure.
Status | In development |
Authors | guv_bubbs, aloebach, Will Aryitey, rowdy41, xmenekai, Mouflon Cloud |
Genre | Adventure, Puzzle |
Tags | 1-bit, Female Protagonist, Narrative, Pixel Art, Playdate, Relaxing, Sokoban, Story Rich, Top-Down |
Languages | English |
Accessibility | Subtitles, High-contrast |
More posts
- Extreme Makeover: Comet EditionOct 31, 2024
- Milestone 6 | Light Mechanics & Engine UpgradesSep 12, 2024
- Playtesting & Character CreationJul 22, 2024
- Milestone 5 | Chapter 2 Systems, Design & FlavourJun 23, 2024
- Behind the scenes - Announcement TrailerMar 31, 2024
- Milestone 4 | Chapter 2 DraftFeb 28, 2024
- Milestone 3 | Chapter 1 PolishDec 01, 2023
- Camera workNov 06, 2023
- How we use LDtk in CometOct 16, 2023
Leave a comment
Log in with itch.io to leave a comment.