If you want to get inspired by what can be done with palletized framebuffers check out http://www.effectgames.com/demos/canvascycle/ (click Show Options) and the GDC presentation by the artist https://youtu.be/aMcJ1Jvtef0
With that you can fire up https://github.com/mriale/PyDPainter for that classic Deluxe Paint IIe vibe. Or, https://www.aseprite.org/ for something more modern.
I thought I could really level up with Claude and I started working on a boxing game. It's been a total disaster. .·°՞(˃ ᗜ ˂)՞°·.
Some details are a bit too cool for 1993, though, and assume high frame rate (won't work that well at low fps). Smooth weapon animations with a lot of frames, tiny per-pixel effects on bullet holes and flash sprites, smooth movement and object position calculations that use precise math instead of fast rough estimates resemble Chasm: The Rift or Quake (the concept of idle animations, e. g. objects moving in the starting view of difficulty selection room, assumes that there is some performance to waste on details that make the world less empty).
It's refreshing to dust up trigonometry and good old low-level optimization tricks. When the scratchbuffer has 1 KiB and the stack can only use a fraction of that, it makes me realize how spoiled I'm at work with the microcontrollers we have, with threads being allocated 8 KiB of stack and backtraces with over 50 functions of C++ templates on it.
This is my complaint with a lot of "graphical enhancement" mods for games like Deus Ex.
Unless they touch everything, the inconsistent level of detail is worse than consistently low-res meshes/textures.
I think the mix of highly rational reasoning and "it just feels right" is a killer combo too, it gives a rigorous basis for a lot of the decisions made, while also allowing for a strongly personal aesthetic to emerge. Very cool indeed.
The comments here are a cesspool unfortunately. People bickering about pronouns used for cats, how many shots it takes for a vase to explode, or whether or not some circa-1993 software was used or mentioned.
I did write a tool for generating the sprites from 3D models though[2]. It uses plain old OpenGL 1.1 to draw the sprite and grabs the framebuffer directly. It is drawn fullbright so i can paint the lighting directly on the sprite's texture (using a Krita plugin i wrote[3][4] - the model is something i threw together with Blender's default generated UV since i didn't care for the details).
I wonder if doing some sort of postprocessing (after rendering with with shading) like you do with your game would help with the finer details since i also found that rendering from 3D models to sprites creates very "mushy" results most of the time because of all the details getting lost. I notice the colors also become more saturated after postprocessing in your examples, is this after it finds the closest color in the palette or the result of the postprocess? I'd like to keep the overall hue+saturation of the model so maybe doing post-processing on a grayscale render to shade the shadows/dark areas but keep highlights as-is and then multiplying that with the fullbright image would produce results that wont shift the saturation.
[0] https://bad-sector.itch.io/post-apocalyptic-petra
[1] https://codeberg.org/badsector/PetraEngine/src/commit/14ca16...
[2] http://runtimeterror.com/pages/iv/images/95ddebc51e4dfa8a5af...
[3] http://runtimeterror.com/tools/kritaview3d/
[4] http://runtimeterror.com/pages/iv/images/535f0e09e590d8a1731...
What I don't like is to see claims like "no AI slop"
And yet it's riddled with emdashes and language "by hand"
Seeing the skills of the writer, he definitely should be able to, but then I don't understand the claim.
I think I'm gonna have to do it anyway, because some players claim they get nausea when playing at such low resolution (320x240), and the only way to give them higher resolutions that perform reasonably is to have it hardware accelerated.
Renderer is abstracted away already, but the real difference would probably be occlusion culling... With raycasting, I get it for free, but if I'd go down the hardware accelerated path I'd have to pick something more clever.
Raycasting and software rendering in general tends to scale poorly with resolution, even with vectorization and all the bells and whistles of modern CPUs.
what's unreasonable about this though?
I feel like the idea of fixed-point is under-utilized and very under appreciated. There are loads of applications where this is a better choice, let alone more performant.
The flight simulator / magic carpet easter egg in Microsoft Excel 97 used that same shaded-colormap palette trick, plus some dithering:
https://rezmason.github.io/excel_97_egg https://rezmason.github.io/excel_97_egg/about.html
I'm impressed by your sprite pipeline and gibs animations. Your attention to detail and navigation of constraints have really paid off, I can't wait to play this sometime
Those kind of constraints can lead to increased creativity, and can also influence the overall style of a game. It's part of the reason early 80's arcade games had so much diversity.
The author seems to consider open-sourcing the engine, I would also be interested in the mentioned scripts for asset creation. Those scripts would make a great toolset for asset creation in this style.
It's not that rare, is it? Off-hand, and very mainstream; Perfect Dark, Mirrors Edge, Dishonored (don't remember if it's the first or second one), Metroid and more are all kind of "shooters" with female protagonist, although maybe Mirror's Edge is more just "first-person" than "shooter" to be 100% accurate.
Not to mention the large selection of "RPG + FPS" where you can be either man or woman.
---------
Seems the author also realize the thing with the pattern and likely gender of the cat:
> After all, I do need to give the protagonist his fair share. [image] (Yes, I know it's a female, but call it convention rooted in dialect.)
I have no idea the names of nearly anyone but a CEO or lead Director in the games industry of the past 15 years.
[0] https://store.steampowered.com/app/1592280/Selaco/
[1] https://store.steampowered.com/app/1693280/Supplice/
[2] https://store.steampowered.com/app/1378290/The_Citadel/
[3] https://store.steampowered.com/app/3371240/Beyond_Citadel/
[4] https://store.steampowered.com/app/2443360/Zortch/
[5] https://store.steampowered.com/app/3807500/Zortch_2/
[6] https://store.steampowered.com/app/1051690/Nightmare_Reaper/
[7] https://store.steampowered.com/app/1785940/COVEN/
[8] https://store.steampowered.com/app/1406780/Viscerafest/
[9] https://store.steampowered.com/app/1072150/Hedon_Bloodrite/
No, this isn't a Perfect Dark game
It's the result of the Blender compositor postprocessing, just keep in mind it falls apart once you go low enough in resolution (it's an image space thing after all), so I'm not sure if that helps your case.
EDIT: Also, your project is very cool!
"boomer shooter" + "female protagonist": 106 matches.
So a bit less than 1/10 of the games tagged with "boomer shooter". With your caveats above about being able to choose a gender, or a single brief segment where you're a lady in a game where you're mostly a dude. Is that a lot? I dunno, doesn't feel like a lot to me. Probably feels like a lot to the people who inevitably show up in the Steam discussions of any successful game that makes you be a lady for most of its length and complain about it being "woke", even one game with a female protagonist seems to be too many for them.
If you tally all the FPS releases in a given year, a supermajority are going to have male protagonists.
Edit: I completed forgot Chell from Portal, too!
> If this sounds unreasonable to you, that is because it is.
Those listed, are tame. I don't understand this kind of faux modesty.
> My goal was to build a complete, shippable first-person shooter using techniques that were common in the early 90s
Goes on to explain how they used 3D blender...which wasn't available until 1998.
A vanity cat project being tailored and submitted for nostalgia clickbait. I don't think there's anything useful to take away from this other than some color shade selection ideas.
Blake Stone Rise of the Triad used later versions of the Wolf3D engine and had textured floors/ceilings
> Doom and IIRC Duke Nukem as well used a BSP engine which was much more flexible
Duke Nukem (Build engine) did not use BSP
So, a videogame?
Though your extender could make things a little more annoying on that front :-P
(DJGPP and Free Pascal -which use the same "go32" extender by DJ Delorie- do not do a full linear mapping so you need to do a bit more juggling to get stuff on screen there)
The Build engine didn't use BSP, it treated connections between sectors as portals and rasterized the walls as (90 degree rotated) trapezoids while performing clipping against those portals. This allowed it to have dynamic wall geometry (e.g. moving trains, rotating light fixtures, etc) as well as "room-over-room" setups as long as you couldn't see both rooms at the same time (in both Blood and Shadow Warrior they found a workaround for it allowing to create more "3D" spaces by making identically shaped sectors with the floor of one sector acting as a portal to the ceiling of the other sector - supposedly this wasn't "natively" supported by the engine, but it was flexible enough for the game studios who used it -without even having access to the source- to do it themselves).
The first level of Duke Nukem 3D does use a few Build tricks - e.g. another one is that sprites can be "axis aligned" instead of following the camera and they can also have collision - this can be used to create rudimentary 3D geometry by treating each sprite as an axis aligned quad and in the first level it is used to make a bridge between two buildings (right before the level exit button).
For floors, unfortunately there's no such luxury, and if I remember correctly DOOM subdivided floors into patches, and only did proper perspective at the corners, and interpolated inbetween.
As an example this[0] video shows the benchmark from Post Apocalyptic Petra running on my previous GPU (RX 5700 XT) which all it does is build a per-frame (client-side) vertex-buffer in OpenGL 1.1 (the engine was made for actual retro PCs running DOS and Win9x so it does some rudimentary occlusion culling but that mainly affects 90s hardware, not anything released since 2000 or so). If anything, the rendering has so little overhead that half of the framerate is "eaten" by the FPS counter overlay :-P.
But ignoring the GPU you have on your system is boring
Mirror's Edge has a female protagonist, but it's not an FPS (First Person Shooter). It's a parkour simulator which technically lets you shoot a gun in limited sections of the game, but the protagonist is a pacifist and you get a bonus for decommisioning guns rather than firing them.
If the thread would like some hard data:
- 19,526 games on Steam tagged "female protagonist" https://store.steampowered.com/search/?tags=7208&ndl=1
- 13,578 games on Steam tagged "FPS" https://store.steampowered.com/search/?tags=1663&ndl=1
- 727 games on Steam tagged both "female protagonist" and "FPS" https://store.steampowered.com/search/?tags=7208%2C1663&ndl=...
So it looks like the two categorisations, for the most part, don't intersect.
Notable counterexamples would include Rise of the Triad, Ion Fury, No One Lives Forever, Wolfenstein: Youngblood and Far Cry 6, but definitely rare. You'd be clutching at straws to describe Portal or Alien: Isolation as FPS (they're a puzzle game and survival horror game respectively), likewise the Resident Evil / Clock Tower / Fatal Frame / etc. games with the novelty option of switching to first-person view, they're naturally third-person perspective. Left 4 Dead has one female character out of four you can play. You might count that one DLC for Bioshock: Infinite where Elizabeth gets a shot (https://www.youtube.com/watch?v=1E1lh-pb6Is). You might count the few FPS RPGs that there are with customisable characters (so yes Fallout, but not Mass Effect as it's third-person). But female protagonists are massively more prevalent in survival horror, metroidvania, third-person shooters (Tomb Raider, Monster Hunter, Horizon Zero Dawn, etc) and other genres besides FPS.
Choosing one specific example when I also made more recent ones, isn't such a big dunk you think it is.
> If you tally all the FPS releases in a given year, a supermajority are going to have male protagonists.
Sure, I agree, I'm not saying it's more popular, just that I don't think it's that rare, but I guess ultimately I'm a bit nitpicky (sorry) and we're just disagreeing with the specific definition of "rare".
https://unrealarchive.org/wikis/the-liandri-archives/Prisone...
The BSP may have led to some floor subdivisions, especially as it needs convex sectors. I don't remember if the engine would coalesce adjacent floor spans into a single one, but I hope it did.
Inconsistent resolution isn't necessarily a bad thing, e.g. Elite for the BBC Micro changes video mode part way down the screen so it can display both high resolution monochrome wireframe 3D and a lower resolution color map/UI below, but it's not idiomatic to the MS-DOS style this game is going for.
Thinking about modern games, a single character model probably has more vertices than my entire level (and yours probably), so it's definitely reasonable to expect occlusion culling for such simple geometry might actually reduce performance rather than increase it.
To work around this, people used an unofficial tool to patch the maps to support transparent water:
Meanwhile more current games have much higher polycounts, easily going above 100K triangles - e.g. Dante from DMC5, a ~7 year old game, apparently has ~190K triangles and that had to run on the more anemic PS4/XBone hardware :-P (though i'm not sure if it used the full 190K model there or some cut down version).
[0] http://runtimeterror.com/pages/iv/images/1073c7062db40837240...
Catlantean 3D is a side-project I've been slowly building in my spare time for over a year, and I intend to release it on Steam next year.
Your browser does not support the video tag.
My goal was to build a complete, shippable first-person shooter using techniques that were common in the early 90s, while allowing myself the luxury of using a modern compiler and a platform abstraction layer.
What this actually means is, the constraints I have foolishly imposed upon myself are as follows:
If this sounds unreasonable to you, that is because it is.
But I'm doing it anyway, and today I'm gonna talk about something that is typically overlooked in development blogs, and that is asset creation.
Note: Everything displayed here is work-in-progress, and heavily subject to change.
Mode 13h on VGA hardware was the famous 320x200 256-color graphics mode that defined a generation of PC games. From a programmer's perspective it was wonderfully simple: you'd have a linear frame buffer where each pixel was represented by a single byte indexing into a palette of 256 colors.
If you wanted to draw a pixel, you wrote a byte at a specific address, and that was it, there were no shaders or VRAM, or anything like that.
One byte per pixel, and that byte is an index into a palette which contains actual RGB values that would be rendered to screen. This imposes some interesting limitations; when making assets for modern games, you can throw millions of colors at an image, but when your limitation is that every pixel on screen can only be one of 256 colors, asset creation becomes a very different problem because every color choice has to be careful and deliberate.

Games like Doom and Duke Nukem are good examples of this done right. There is a certain crispiness and clarity to these graphics that arises because of these technical limitations, not in spite of them. Restriction forces deliberate choices, and deliberate choices tend to look good.
Catlantean 3D is an attempt to reproduce that feeling, but with one caveat - I'm actually going for something closer to VGA Mode-X, which is 320x240. The reason for this is, if you display 320x200 on a 4:3 display, you end up with non-square pixels! While this would be most authentic, I've chosen not to deal with this out of preference rather than objective reason.
So how does one create graphics that work within these limits?
Everything begins with 768 bytes, carefully picked through many iterations of trial and error.

The main reasoning for picking these exact colors was the following:
The palette did not spring into life all at once; it involved a lot of back-and-forth during asset creation, testing, and re-iterating in general.
Below are some examples of sprites and textures from the actual game:



Catlantean 3D is a traditional raycaster. The map consists of tiles which are all identical in size; some are walls, others are just voids with a floor and ceiling. In order to render the map, the renderer uses the DDA algorithm for each column of screen, traversing the tilemap and determining where it hits the map geometry, and based on this, a wall column is rendered on screen with the appropriate texture, sampled from appropriate coordinates. Floors and ceilings are rendered after as horizontal scanlines, filling in the rest of the screen.
Raycasting has been done to death by other blogs and websites, so I'm not going to cover all of it, but I do want to cover what I think is its most overlooked aspect: lighting.
If we were to render the game world using just the palette, without any special effects, we would end up with something that looked rather flat and unimpressive:

But what we wanted was the following. Notice how the light diminishes the further away geometry is from player, and how one side of the map tiles is just slightly darker than the other. This gives an impression of depth.

With a modern hardware-accelerated renderer, this would be trivially done in a shader - based on how far the vertex is, we would multiply its color vector by a floating point factor and get a diminished color vector as a result.
But how do we achieve something like this with a palette renderer? It has no concept of color, just indices into palette. So if we wanted to find a darker shade of a certain color, we would need to loop through the palette and find the color that meets our criteria of "darker". This is just too much because we can't loop through the entire palette for every pixel we render onto the screen, it would be too slow.
What we could do instead was some preprocessing, to allow a fast color lookup based on distance at runtime.
If we were to lay out our palette into a single row like this...

We then choose the number of shade levels (32 in my case) meaning each color needs 31 darker variants, all sourced from the palette. We know each color's RGB values, so from this, and the shade index we can determine the closest target color of that shade:
// First shade index (0) is original color.
float darkening_factor = (32 - shade_index) / 32.0f;
target_darker_color.r = current_color.r * darkening_factor;
target_darker_color.g = current_color.g * darkening_factor;
target_darker_color.b = current_color.b * darkening_factor;
But that color might not exist in the palette. So we need to loop through the palette and find the closest color to it.
Definition of "close" actually changed for me during development - at first, I just took euclidean distance as a measure, but the problem with that was that almost everything had a tendency to gravitate towards the greys, simply due to the mathematics. Some older games actually did use Euclidean distance, but to me this didn't look very good. I can't explain why exactly, but a lot of darker shades appeared somewhat cold and lifeless. So instead, I converted my colors to Oklab color space, and leveraged its perceptual distance formula, which is closer to how humans perceive color differences. I also apply a small shift towards warmer hues the darker the color is (a common concept in pixel art called "hue shifting"). This is typically not necessary, but it does make the game look just a bit better.
How do I define "better" in this case? I have no idea, it just looks right. Frustrating, isn't it? It's hard to rationalize something subjective.
Back to our algorithm...
Essentially, for each color, we create a column that represents the shades of that color. What we end up with is a 2D matrix of palette indices called the colormap. Note that the colormap gradients are imperfect, because we're still restricted to colors from the palette:

So now, determining a darker shade of color N based on distance becomes trivial.
Given colormap row index (i.e. shade level) based on distance:
colormap_row = 32 * fragment_distance / view_distance
We pick N-th entry in row belonging to that shade - that is the palette index of the darkened color N.
And voila, O(1).

Also, instead of calculating the colormap row index for every pixel, the cost is further reduced by performing calculation:
So we're doing colormap row index calculation 320 times for walls, at most 240 times for floors, and once per visible sprite (raycasting gives free occlusion culling). That is cheap, and the payoff is great.
Doom and many other titles used similar approaches.

Textures and sprites in Catlantean 3D fall into three categories:
I am working a full-time job and have a decently active life, so my time to work on the game is limited. Thus, I wanted to minimize the time I spend reiterating when making complex sprites that involve animations. I rarely get something right on the first attempt, so naturally, reiteration is expected, and it is hard to reiterate when you need to make changes to many frames of an animation.
The more efficient approach was to create sprites in Blender as 3D models, rig and animate them there, and then render them to a series of textures with special Python scripts that leverage Blender's Python API. Reiteration then involved making changes in the model, and the rendering scripts did the rest, which was a lot of time saved.


The main hurdle was that rendered sprites came out very blurry and washed out.
One might think that the obvious answer to this was to render the sprite in high resolution, and then downscale with filtering, but I've had mixed success with this; details would often be suppressed by filtering, and edge clarity would be lost. What I found to be the most effective and reusable was to leverage Blender's compositing functionality to get the right amount of contrast and clarity:


Once the image was ready, it would be sent through a special Python script which performed palette quantization, creating a 1-byte-per-pixel image used by the engine. For every pixel in the source image, the script finds the closest color in our palette (perceptually closest - Oklab), and uses the index of that color for that pixel. The index array, along with the dimensions, is then packed into the very simple TEX format that is used by the game.

A similar workflow was used for enemy sprites. Note: some of these nodes are either redundant, or plain useless, simply because I had used them at some point, and then changed my mind. I like leaving them in just in case I need them again.


Enemy sprites are rendered in a special way. Sprite can have multiple animations, and each animation must have frames for each of the 8 directions sprite can face. So, for every animation (walk, fire, die, etc.), the Python script that uses Blender's API rotates the sprite, renders all frames of an animation, rotates the sprite again, and so on. Sprites are saved with a special convention that denotes sprite name, action name, direction and frame index:
![]()
Nice thing about this approach is that I don't need to keep rendered sprites in the repository - they're actually .gitignored. Whenever I switch locations and use another computer, I simply run the compilation script which renders every model and produces the sprites. It is reasonably fast and runs in ~10 seconds for about 15 models on RTX 3070.
Earlier in development, I created this vaguely cat-shaped head with the texture of my cat Vilko, to use as a status bar face. After all, why would I draw something like this by hand, if Blender could render it in such a vivid likeness of life?

The obvious answer is that it looked lazy and low effort, which it was. It didn't do a very good job at conveying emotion and it didn't have soul. When I was collecting feedback on the vibes, this was usually the first thing people pointed out.

Some things just need to be drawn by hand. I'm no artist, but I'm quite confident that the hand-drawn variant with animations looks far better, and I could never quite reproduce the same result if I animated this thing in Blender. Due to the dimensions of the sprite, every pixel needs to be deliberate, so there's no room for leaving this work for the Blender renderer.


I applied the same logic to most of the pickups, which were previously pre-rendered, but at a scale where Blender's compositor can't reliably produce good results. After human touch, their clarity and readability has much improved.

You might think, why simply not increase sprite resolution? The game rasterizer will take care of the scaling, right?
Well, it would work, but the result would look terrible because pixel scale is no longer consistent. At any given row or column on screen, you subconsciously expect the pixels to remain the same size as you move towards or away from them. If pixel scale varied from one sprite to the other, that's no longer true, and it just looks off. This is probably one of biggest reasons why many asset flippers or low-effort indie games look bad; they slap together assets with different scales that don't work together at all.
Thus, one unit in Catlantean 3D world is 64 pixels, and every sprite is made in relation to that scale. So if we want a sprite that is as tall as the quarter of a world unit, it must be 64/4=16 pixels tall.
The HUD and its elements are almost entirely hand-drawn. This includes:
For example, this work-in-progress score screen at the end of the level:
Your browser does not support the video tag.
The HUD is hand-drawn in the sense that everything is deliberate and placed by me, but I use Affinity Photo's layer effects and compositing heavily to get there, rather than painting everything from scratch. These effects include:
I typically work in truecolor first, in Affinity Photo. Notice the layers - most of these elements are literally just single-color rectangles, with special effects and blending magic applied on top.

While Affinity Photo is great, images exported from it contained some strange artifacts most likely related to anti-aliasing, which I couldn't turn off. Or couldn't figure out how to turn off consistently. So it's not great for pixel-correct work, which means I had to do another pass in Aseprite that involved things like:

Some textures are straightforward or specific enough to draw by hand. But many textures in Catlantean 3D share a common structure: a base material with variations in wear, dirt, and surface detail. Drawing each variant by hand would be tedious and inconsistent, so I wrote Python scripts to generate them.
The generation pipeline takes several inputs:
From these, the script produces a final texture, palette-quantized and ready for the engine. Tweaking a texture then becomes a matter of adjusting parameters rather than repainting pixels, which is a significant time saver when you're the only person working on this.

Some examples of generated textures:

I also have a special pipeline for creating gibbing animations. Gibbing an enemy is typically triggered by applying excessive damage, such as a point-blank shotgun blast or an explosion. To convey impact of such damage, enemies are blasted into bloodied bits:

The pipeline is driven by a Python script. Given a sprite, a palette, and a set of parameters, it produces a series of frames that end up in the game data as an animation. Here's how it works.
The sprite is partitioned into chunks. K seed pixels are chosen at random from the sprite's opaque body, and every pixel is assigned to its nearest seed. Each resulting cell becomes one flying piece.

Chunk borders (pixels adjacent to a different chunk) are marked as wounds at depth zero. A BFS fans inward, assigning increasing depth values. At render time, pixels near the boundary get their color blended toward blood, sampled from a ramp derived from the game palette. The deeper into the chunk, the more the original sprite color is preserved.
The ramp selection from palette is parametrized, so I can also have green or blue "blood" for certain enemies.

Each chunk gets a centroid, a velocity directed away from the sprite center with randomized spread, a spin rate, gravity, and drag. Then, the simulation starts running using these few parameters. There is no collision detection, but chunks simply stop when colliding with the floor. It's crude, but it's good enough.

Chunk count, explosion force, gravity, drag, spread, and wound depth are all tunable via parameters, e.g.
"seed": 295312884,
"frames": 20,
"chunks": 48,
"explode": 3,
"gravity": 1.4,
"drag": 0.22,
"spread": 1.15,
"spin": 9,
"woundDepth": 2,
It takes a bit of trial and error to get a seed that looks good, but it's certainly faster than drawing these animations by hand. The same technique is also used for destructible environment objects (flower pots, barrels, crates, etc).
Like pre-rendered animations, these also don't live in the repository, but are instead regenerated after repository is checked out. The execution time is negligible.
While most particle effects are hand-drawn in Aseprite, some of them are generated and baked the same way as gibs: a Python script runs a simulation and produces a series of PNG frames, which are then quantized to TEX. There is no runtime particle system; everything is pre-baked so that it can be rendered as fast as possible by the software rasterizer.
Your browser does not support the video tag.
The word "particle" is a bit misleading here, because the pipeline doesn't actually simulate particles at all. Instead, each frame is synthesized by computing a radial energy field pixel by pixel, with several independent layers summed together:
The accumulated energy per pixel is then quantized against the palette ramp specified as script parameter. Each row in the palette is treated as a light-to-dark gradient, simply because I arranged it that way when designing the palette, so the darkening of each pixel is done without any blending or alpha math, just palette index arithmetic. Above a certain threshold, pixels are pushed towards white, so that they give the impression of a white-hot core.
Also, optionally, a small number of sparkles are scattered on top, cross shapes that drift outward and fade over their own lifetime.
The animation supports two modes: one-shot, which ramps up and decays like an explosion or teleport flash, and loop, which samples the noise field along a circular path, so the first and last frames match and the loop is seamless (useful for persistent looping effects like plasma bolts, energy projectiles, etc.)


Map editing started in Tiled, which is a perfectly reasonable tool until you start having very specific needs.
What it lacked was any concept of the things my game actually needed: light level painting per cell, cell flags and properties, which I initially worked around by abusing object properties. On top of that, the workflow required a Python script to convert Tiled's JSON output into the binary format the engine uses, which is an extra moving part that exists purely to compensate for the mismatch.
There's also the idea of shipping it to players along with the game. Expecting someone to install Tiled, learn its interface, set up the conversion scripts, and wrangle all of this just to make a map is unreasonable. It would have killed any chance of the editor being something people actually use.
So I wrote my own. It has native support for light level painting, cell flags, and all the entity and property types the game knows about. Development became noticeably more enjoyable because I'm no longer restricted to thinking within Tiled's limitations, and when the game ships, players get the same editor I used.

It's plug and play, you can even launch the level straight from the editor:
Your browser does not support the video tag.
Yes, I am aware the toolbar icons are terrible. Which is exactly why I'm keeping them.
The editor is built with wxPython, which turned out to be a decent choice for this kind of tool. It worked better than tkinter (which I tried first), particularly widgets, event handling, layout, it just felt better to work with and the end result looks more native. Iteration was fast, and structuring it around the MVP pattern kept the UI logic cleanly separated from the map data, which matters when you're frequently changing both (my map format isn't quite stable just yet). It hits a good balance for a single-developer internal tool that also needs to be shipped to end users.
Not everything in the editor is written in Python. Much of the model relies on my pybast library which is basically Python bindings for engine internals (via pybind), which includes things like:
This is mostly to prevent having to re-implement these things in Python where I already have them implemented in C++. As such, the engine and its tooling form a small, tight ecosystem.
I expect to publish Catlantean 3D sometime in Q1 2027. Right now, I'm focusing on level design, adding some more enemies and weapons, and polishing as I go.
I'm aiming for somewhere in $5-$8 price range. I intend to release the game source code on GitHub as open-source, but you'd have to buy the game to get the actual data archive (with graphics, levels, sounds, music, etc.), which I think is fair.
After all, I do need to give the protagonist his fair share.

(Yes, I know it's a female, but call it convention rooted in dialect.)
Jokes aside, transparency about process is one of the few things that actually builds lasting trust, I think.
Indie games live and die on it in a way AAA simply doesn't, because that market has demonstrated it will accept whatever it's given. Indie doesn't have that luxury. The audience is smaller, but they're also more willing to follow a project, root for it, and tell other people about it when they feel like they're genuinely in on the process rather than being marketed at.
Showing your work is the most honest thing you can do, and I think people can tell when someone actually cares about what they're making.
There will be more to come.
Thanks for reading!
If you enjoyed this article, feel free to follow me on social media.