Familiar Castaways

Devlog 3: Cameras & jumps

Happy February! I hope the world is treating you well. ❤️

It’s a cliché at this point, but I’ve got a fresh filter coffee in my favourite mug, Lofi Girl on the TV, and I’m ready to draw a line under the last few weeks.

Broad goals

Last time we talked, my broad goals were to build…

I’m calling the first pass of these done! Some bits are jankier than others, but for a first pass? Definitely done.

But first… let’s sort out this resolution.

Resolution

Back in January, I was torn between 320×180 and 480×270, but I settled on 480×270 for the time being:

320×180 is such a classic resolution, with Celeste, Animal Well and Pipsqueak absolutely rocking it, and I probably would’ve settled on it by default not for Scourgebringer.

As best I can figure out, Scourgebringer runs at 480×270. It’s still got those crunchy pixels, but you can fit a few more of them on-screen than 320×180. 640×360, of course, can fit a lot more pixels, but there’s no crunch. I want crunch.

The truth is, writing that at the time really made me second-guess myself. I hinted at my reservations with:

The major downside of 480×270 vs 320×180 is that it doesn’t scale to 720p or 1440p – but I’m not terribly worried about that right now.

…but, dear reader: I was worried about it.

First worry: 1440p

In the latest Steam Hardware Survey (January 2026, at the time of writing), 1920×1080 is the most popular screen resolution at 52%, but 2560×1440 is second at 21% – and up from the previous survey. Less than 5% of the gamers surveyed play in 4K.

My gut tells me 1440p is going to get more and more popular as folks want to bump from 1080p but can’t afford the upgrades to 4K. It’s going to be a long time before AI data centres stop hoovering up silicon, so gamers will be making do with what they can get – and 1440p will be the best upgrade that most folks can get. Just a hunch.

Second worry: 800p

I love my Steam Deck. Love it! The only reason I don’t play it more often is my crippling misophonia needs me to wear noise-cancelling headphones all the time, and Bluetooth lag is rotten. I’m pretty sure my Bose QuietComfort Ultra support noise cancellation via the cable, but I’m not sure where that cable is in my den. I need to sort that out.

But – my point is, it’s super important to me that the game plays great on my favourite console, which means the game needs to have perfect integer scaling that fits a 1280×800 screen as fully as possible.

Now, scaling 480×270 into 1280×800 is rotten:

MultiplierResolutionWasted screen height
×1480×270530 pixels / 66% of the screen’s height
×2960×540260 pixels / 32% of the screen’s height
×31440×810Doesn’t fit

I’d need to choose between non-integer scaling (which I’m sure is probably fine in reality, but I’m not into it) or lose a third of the screen to an empty border. Speaking as someone who believes some borders do far more harm than good, 480×270 isn’t going to cut it.

Not a worry: 720p

As for 480×270 not scaling into 720p… I’m actually okay with that. I feel like 720p’s days are numbered.

If I was starting this game ten years ago then for sure I’d care, because I’d want the game on the Nintendo Switch. But now, with the Switch 2 running at 1080p, my gut tells me that’s last we’re going to see of 720p.

Not even a quarter of a percent of Steam players use 720p, and that’s dropping too. It’s probably still worth targetting for mobile games, but I don’t know because I’m not interested. For PC and console gaming, I feel like 720p’s done.

It’s 320×180, then

So, after that mathematical marathon, I’m calling the 480×270 experiment done and I’m committing to 320×180.

It’s a lower resolution than I’d like, but it’s closer to the image in my head than 640×360 would give me, and it scales perfectly (or at least, well enough) to all the screens I care about:

MultiplierResolutionScreen
×1320×180
×2640×360
×3960×540
×41280×720Close enough to 800p for Steam Deck
×51600×900
×61920×1080Nintendo Switch 2 and 1080p on PC and consoles
×72240×1260
×82560×14401440p on PC and consoles
×92880×1620
×103200×1800
×113520×1980
×123840×21604K on PC and consoles

Perfect! To accommodate the slightly lower resolution, I also decreased the tile size from 16×16 to 8×8. The most unexpected side effect of that, which surprised me at the time but is super obvious in hindsight, is that the file size of my map chunks increased despite their pixel size decreasing. That’s because each chunk records a byte for each tile’s source texture index, and there are more tiles now, hence more bytes. But I’ll get into those compiled map files later.

With the resolution settled, let’s talk about the camera.

A smooth camera

When I say this game is a true pixel art game, I mean every pixel is perfectly aligned on a grid and objects move in perfect pixel increments. When a character moves from left to right, there’s no point where any of their pixels partially obscure any background pixels. It’s old-school, and not everyone likes it, but that’s okay. I’m not building this game for them.

But, that said… a camera moving around in perfect pixel increments just looks awful. It’s too jarring. Games like Animal Well avoid that by moving the player between distinct rooms. The camera never pans; it jumps from one room to the next. I really like that as a player, and I super like it as a developer. Oh, it would make my life so much easier! Don’t think for a second that I’m not tempted. But I want to give this one-shot open-world a try, so I’m sticking with it.

So, to avoid the jarring of a pixel-perfect camera, the camera isn’t pixel-perfect. The camera is the only entity that moves in “screen” pixels rather than “game” pixels.

I already had the foundations for this in place since the beginning of development. After the game’s update loop, it renders everything to a 320×180 pixel texture off-screen, then performs an integer scale of that texture up to the viewport’s resolution and renders that to the screen. Everything inside the texture renders pixel-perfectly, and the texture itself can be positioned at sub-game-pixel positions by offsetting it within the viewport.

Say the game is being rendered at ×8 resolution for 1440p. If the camera needs to be positioned at 54.25×68.75 exactly, it renders the game within the texture at 54×68 “game” pixels, then offsets the texture by 2 “screen” pixels to the left (because 2 is 0.25 of 8) and 6 “screen” pixels down (because 6 is 0.75 of 8).

Got it? Good.

A bounded camera

If I want the camera to follow the player as they explore the world, why would I also want to restrict where the camera can go?

Let’s say the player is walking through this corridor:

A screenshot of a tiled platformer layout in LDtk. The room is a corridor running from left to right.
A lovely corridor

Generally, we want the camera to keep the player near the centre of the screen. So, generally, if the player jumped onto that hill in the road, we’d want the camera to slide up to follow them.

Generally, yes. But in this case, moving the camera up or down won’t reveal any more of the map. There’s nothing up or down there. Keeping the camera locked vertically on that corridor keeps the relevant stuff visible and just feels better.

Likewise, let’s say you’re climbing this tower:

A screenshot of a tiled platformer layout in LDtk. The room is a tower running from top to bottom.
A lovely tower

Generally, we want the camera to keep the player near the centre of the screen. So, generally, if the player ran left or right, we’d want the camera to slide horizontally to follow them.

Generally, yes. But in this case, moving the camera left or right won’t reveal any more of the map. There’s nothing there. Keeping the camera locked horizontally on that tower keeps the relevant stuff visible and just feels better.

My first pass at bounding the camera feels right in theory, but it’s janky in play so it needs some tuning for sure.

In LDtk, I added a new integer grid with four values for the camera’s top (red), bottom (blue), left (cyan) and right (yellow) boundaries, then drew them out:

A screenshot of a tiled platformer layout in LDtk. The world has tiles, collisions and marked camera bounds.
The new world

During each frame, the camera runs through a series of systems to figure out where it needs to be:

The result technically works, but definitely needs some tuning:

Good enough for now, though!

The map chunk file format

Last month, the map was described on-disk as a set of chunks, each a binary file containing:

  1. 880× (one for each of the chunk’s 40×22 tiles) bytes to describe (from left to right, top to bottom) the texture to render from the greybox tile set.
  2. 880× bytes to describe whether each tile is a collision or not.

When I came to add the camera boundaries to the map compiler, I realised it was super-wasteful to add 880 more bytes to every chunk whether they had boundaries or not. That made me realise that I already had a ton of empty chunks that were full of zeroes that the game was spending time loading. If I added more layers and kept following this pattern, every chunk would get bigger and bigger whether it needed to or not.

Now, I don’t really care about the disk space being used. A four kilobyte file versus a two kilobyte file ain’t worth sweating about. But the game streams a lot of files a lot of the time, and I don’t want to hog disk IO just to read zeros.

So, I spent a couple of days in the compiler and reader to clean up the file format a bit. Now, from beginning to end, the chunks are:

  1. An unsigned 16-bit integer to describe the number of bytes to read for the camera bounds layer.
  2. An unsigned 16-bit integer to describe the number of bytes to read for the collisions layer.
  3. An unsigned 16-bit integer to describe the number of bytes to read for the tiles layer.
  4. All the bytes that describe the camera bounds.
  5. All the bytes that describe the collisions.
  6. All the bytes that describe the tiles.

Sure, it only shaved a few kilobytes off the existing chunks, but it should prevent the file sizes exploding as I add more layers in the future.

Player spawn position

The player’s spawn position used to be hard-coded, which was fine when the map was small – but since I started experimenting with different layouts and shifting tiles around, it was a pain in the bum to keep spawning inside collisions.

So now, the LDtk map has an entities layer, with just a single entity for the player’s spawn position.

As I add more entities, they’ll probably get added to each chunk’s binary file. For the player’s spawn position, though, I didn’t want to have to scan every file to find it – so this one gets written to the map’s metadata file. It’s the first external file that the game reads on startup, and discovering that position now triggers the creation of the player entity and camera.

You wouldn’t tell from looking, but it’s a heck of a time saver!

The tools you use don’t matter, until they do

This isn’t going to be a rant. It’s just an anti-recommendation.

I’ve been trying Craft as a way of sharing notes between my phone and laptops. It’s lovely for everyday casual notes, but I need to throw out a strong anti-recommendation for technical writers – and that includes you, game devs.

Craft will take every opportunity it can to replace your text with emojis, and you can’t stop it. When I was drafting this post, I tried to type:

(2 is 0.25 of 8)

…but Craft replaced it with:

(2 is 0.25 of 😎

You can’t undo it. You can’t turn it off. It’s a feature, not a bug.

I filed a support ticket to be sure, and they confirmed:

Hi Cariad,

Thanks for reaching out! Unfortunately, there’s currently no setting in Craft to disable the automatic emoticon-to-emoji conversion.

We understand this can be frustrating, especially for technical writing where characters like 8) need to stay as-is. We’ve passed this along as a feature request to our team.

They suggested some workarounds, but workarounds suck when you’re brain-dumping in flow state. I’m keeping an eye out for a note-taking platform to replace Craft, and until they fix it I wouldn’t recommend any technical writers use it. It’s beautiful, and I’m gutted, but I can’t use a writing app that I need to fight and coerce to keep my text as I write it.

Mental health

I’ll be real with you. I know I can trust you. I haven’t felt great about this project over the last few weeks.

I know I’ve made progress, but it doesn’t feel like much. It doesn’t feel like enough, you know?

It doesn’t always feel great to spend a week in the innards of the map compiler without anything real to show for it. Sometimes, it feels positively awful to spend a week on a camera bounding system that’s all janked up.

In my brain, I know this is the nature of software development. But in my heart, even after building software professionally for over twenty years, it still hurts.

There are days I wonder if I’d be happier if I dropped this custom engine and moved back to Unity or Godot, or learned some other engine for a bit.

It’s been tempting, for sure, but I’m not going to. I’d have to make too many compromises on the vision I’ve got for the game.

But also… is that vision really going to be worth the pain? I know I’m building a game that I’d want to play, but I have to admit that my loves are niche. I don’t think I can look you in the eye and say it won’t hurt if I release this thing and the world collectively shrugs at another mediocre indie pixel-art platformer.

I dunno, buddies. I know I have bipolar disorder. I know I get the morbs. I can handle making a bad decision, but I can’t shake the feeling that I’m a bit of an idiot. I could use a little bit of success to pull me out of it.

What’s next?

I’ve got a bit of the old “blank canvas” paralysis going on right now, so I’m not entirely certain where to go next.

I think I’ll take a shot at some interactable objects, like:

Take care of yourselves, live with love and solidarity, and I’ll see you later! ❤️