Feature Experiment: Map Zooming (Patreon)
Content
Oh no, I've been sidetracked again xD
A few years back I introduced an experiment to demonstrate a potential "overmap" implementation that still obeys the rules of the terminal interface. What about the opposite--a way to zoom the map itself? Obviously this would be intended for a completely different purpose, addressing one of the more common complaints about Cogmind, that on some displays and play environments everything is rather hard to see.
Prior to this I've always framed the zooming discussion as a full-UI thing, where not just the map but everything needs to be larger, which is kind of a show-stopper when there is a minimum number of text elements required to be visible at all times for proper play. But maybe if we only zoomed the map it'd work for some people who otherwise can't play?
It's hard to say whether this would satisfy some people since there's still the text elements, but maybe for example using an alternative font like Terminus is sufficient for those parts, and the map is what we should be focused on. Anyway, it could be worth experimenting with, and I've moved up the timeline for doing that.
Why now?
I've always been interested in experimenting with larger alternative interface layouts, though because I didn't see much promise in them, and doing so deviates from the core design, the idea was to wait until at least a likely engine update down the road, as well as the completion of most of Cogmind's content.
Well, this year money issues have had an influence on my near-term direction :P
Cogmind lost its Overwhelmingly Positive rating on Steam in recent months, something I've expected for a long while but it managed to hold out quite a lot longer than expected (despite okay sales over the past couple years, very few people actually leave reviews and I don't push for them much--the rate is much lower than it was earlier on). I've always been super curious what that would mean for the game, how much of an impact it really had, but now I have an idea: at that same point Cogmind revenue fell off a cliff**. While I can't claim that it was the sole reason, I'm fairly certain that is a significant factor given the timing (in a general sense we also have longer release cycles now, plus there was no discount for a few months). I mean it's been in development for many years, but now that Cogmind is being developed at a bigger loss I need to start worrying about revenue again!
**This particular association I'm claiming here is not public info, and I'd rather it not be, please don't spread it around outside patreon.
On that note, I must thank all patrons for making the ongoing expansions much more feasible. I'll admit expansion-level content releases for a niche game without explicitly charging for them isn't really feasible forever, but I don't want to split the game world into DLCs--it's all or nothing, and there is just so much cool stuff still to add. It must be done. It will be done :)
It's time to experiment!
Mockups
Any proper UI work is likely to start out with mockups--might as well play around with relatively simple images before investing a greater effort into code...
There you have it, a mockup depicting Cogmind using a size 18 font (Terminus for better readability overall) combined with all map tiles doubled in size (1080p@16:9, the most common Cogmind player resolution).
Okay so each tile actually occupies four times the usual amount of space, but what we mean here is that the cell dimensions are doubled. Something in between 1.0x and 2.0x might be more ideal from a size and visual balance perspective, but doubling is more feasible for retaining the actual aesthetics, both in terms of cell alignment and pixel accuracy, and might also provide us with other benefits later.
The mockup is also missing some components that eventually must be considered, such as what text over the map might look like as far as object labels and other info overlays, but that's not important right now, more of a detail to consider when the time comes, assuming the fundamental feature even works out at all.
Architecture
It's time to enter... the zoomiverse!
Okay that's an early blooper, we'll get to that in a moment ;)
Just how to implement selective zooming in a terminal emulator that is made to do no such thing is a bit of a dilemma.
Terminal-based engines don't behave like a normal game engine where you have individual windows represented essentially as images layered on top of one another and their contents can therefore be scaled individually. Instead there are many layers of cell data from different subconsoles that feed into a single root console which is then converted to the final image.
Under this kind of architecture it's not all that reasonable to insert images into the mix, or make modifications to entire subconsole properties of the variety which are easy and obvious to apply to images. We can't go "oh sure just tell the computer to zoom that window and we're done with it!"
Sticking to the terminal system's constraints is great for helping maintain visual consistency, and keeping the overall architecture and interface relatively simple, but if we want to zoom the map things are going to get complicated beyond the scope of what the engine can normally do on its own.
Over time I've brainstormed 4 different theoretical approaches to zooming the map, and most recently expanded that to 5~6 (an exact count depends on how different one needs to be in order to be considered unique). Some are more involved than others, and each comes with their own tradeoffs, though having no actual experience with this feature in practice, its true complexity and scope are not immediately apparent. Therefore it makes sense to start with the easiest, least intrusive option regardless of all other factors, just as an experiment to gain a better understanding of what the results feel like, and collect a list of design issues that would need to be tackled to make this a reality.
Real-time Scaling
The first and simplest method is absolute brute force (of course :P). Let's stretch some pixels.
Sounds easy enough to take the map area and blow it up, yeah? Well, not really xD. Cogmind doesn't know anything about images, and the engine doesn't know anything about Cogmind and its UI structure, so we're going to need a little extra communication between the two on this point.
Basic steps describe the cooperative process:
- 1. Cogmind registers a callback function with the engine, letting it know that it wants to zoom an area of the interface every frame.
- 2. When the engine is about to render a frame, it first lets this function know about it and expects to be handed an image in return. That image is created by Cogmind itself by forcing the interface to render [mostly] normally in between frames, copying a central portion of the map view directly to an image, then scaling it up to fit the normal view dimensions.
- 3. The engine finishes rendering its normal frame, then at the end of that frame takes the zoomed image sent by Cogmind and copies it over to the desired area before displaying the final results on the screen.
If this sounds terrible, that's because it is :P
The performance of realtime software scaling is no good, with a first iteration tanking my FPS from 240 to 40, and that one wasn't even working right. Once it got "fixed" the real FPS was more like 24, definitely far below acceptable.
But it did work! Hacky and incomplete though it may be...
Here it is in action:
I say "in action," but really only keyboard input would work there since the mouse doesn't know anything about this image resize weirdness.
Essentially in this form it's nowhere near complete, and not performant either. While there are plenty of potential optimizations, optimizing this kind of architecture makes little sense since much of the work would become obsolete by an inevitable switch to hardware acceleration, yeah? Scaling and copying a few images would be nothing for a GPU, but for now Cogmind is CPU-bound and that isn't changing in the near term.
A few other problems I noted:
- Tons of artifacts created when toggling the zoom (I believe any such zoom feature would need realtime toggling).
- An image-based approach is not directly compatible with some other interactive visual systems that expect to have cell-specific knowledge at various locations, so there would need to be a layer of translation that tends to complicate things.
- Not only is an image-based map unable to take advantage of the normal dirty rect system, in fact it requires turning off that system completely, meaning the engine is always rendering every frame in full. That's pretty slow, compounding with the software scaling work. In my fullscreen tests, forcing a full render every frame in the normal game gives an FPS of 60*, while real-time image scaling drops it to 25. I managed to up it to 30 with one optimization, but it's still far from ideal, plus you don't really want to constantly be rendering at max speed in the first place, even as a cost of getting a map zooming feature. *These speeds are in my dev build, which has a lower FPS than released versions. (Aside: "Dirty rects" are an important concept in gamedev, whereby you keep track of known areas of the screen that have changed since the last frame, for example defined by a list of rectangles, and only update those areas during the render, since any unchanged areas should remain the same and don't need to be updated. This is also where artifacts may originate in a game's display, where perhaps an area changed but was never marked "dirty" for updating along with everything else.)
- The zoomed area would need more nuance, probably in the form of a mask, since it doesn't distinguish what's in the target area at all and simply scales everything, so you try to hack a machine and get this :P
As with any project dealing with rendering work, this one had its fair share of funky bloopers along the way. One of the main issues was a logic error causing the map to repeatedly zoom itself, resulting in a recursive scaling effect.
Had some funky problems with coordinates for zoom centering, too :P
The Next Step
The map zooming implementation as shared here isn't ideal, or even acceptable, though the good news is I do have that longer list of possible methods, and will be working with one of the better, if more involved, ones that could solve all the issues presented here.
A smarter approach should play by the engine's rules, but will take a while longer to implement. The important thing is that playing with this idea showed me what it would feel like and gave me an opportunity to formulate concepts for some promising complementary features which might make this more feasible in a UX sense as well.
Before properly testing out those ideas, however, I'll need to put together an implementation that won't try to fry my CPU ;)