Let it never be said that Rao Dao Zao picks the easy option. Following on from the recent discussion of the general direction in which I’d like to carry my interface, I took a stab at actually building it.
Naturally, things did not go entirely to plan.
To start with, Unity’s new interface stuff is pretty fabulous. You drop a Canvas onto the world, set it to Screen Space Overlay and it Just Works — no faffing, no fuss, just all your components drawn on top of the game world.
In the spirit of making the Minimum Viable Game first, which currently appears to be some kind of micro-lite singleplayer version of Unreal Tournament (seriously, did you expect something else?), my first test component was a hit points box. I’ve chosen the lower-left corner because that’s where Operation: Na Pali put it.
The delightful thing about slapping components down into a Canvas is a thing called “nine-slice scaling”. A way to make resizable UI blocks without the fuss, it scales different parts of the image in different ways — the corners don’t scale at all, the sides only scale vertically, and so on. Perfect for people like me, who love a bit of recycling, as one easy-constructed set of borders can be used across the board to ensure every component is completely consistent.
There are also a whole heap of mechanisms to cope with different resolutions and aspect ratios. It can be made to automatically scale the entire UI by height or width to ensure uniform scaling no matter what (unlike stupid, stupid WC3 that stretches to fit widescreen resolutions), keeping everything at the same visual size regardless of the actual dots-per-inch. Everything else is taken care of by anchoring components to the corners of the screen.
To try to facilitate colour-scheme selection, I made my hit-points box in layers. I’ve got the border on top and a tiling background below, with the actual content sandwiched in between, so I’ll be able to colourise different parts independently. Component tinting, luckily, comes of out the box.
The next step was to get my target-boxes engaged. A simple highlight when you mouse-over any actor other than yourself, these crosshairs will reveal whether the object is an ally, enemy or neutral and its health. Another nine-slice-scaled border, with only the corners painted at all, a bar of solid green above it for health, and job done. (Future thought: imagine the in-world explanation is that the health bar comes from visual analysis of the enemy unit, so the bar could be broken by enemies that have dazzle camouflage!)
I already had some code embedded to calculate an actor’s “bounds” from a previous stab at this functionality, and it required literally no porting at this stage — the coordinate system of the Screen Space Overlay matches WorldToScreenPoint calls. Input.MousePosition can also go in unmolested to tie a nice custom cursor into the deal. (I want the cursor to rotate to point away from the hero in the centre of the screen, but my trigonometry has gone stale again so we’ll save that one for later.)
Everything was going perfectly, just perfectly, until…
I want 3D objects in my UI too.
I’ve hated interface icons since I first started making custom models for Warcraft III. The fundamental disparity makes my skin crawl — that the icon can look like one thing that may not be anything remotely evocative of the actual 3D model in play. Of course it can, it’s a completely independent 2D image. Eurgh!
There’s also the matter of effort. It’s one thing to take a screenshot on a black background and slap a standard border around the edges, but it’s quite another to try for transparency and outlines and whatever other madness I might end up needing. No sirree, this is not the path I wish to tread.
I want to use the actual 3D models in the interface too. If you pick up a shard rifle, the same model as goes on the unit should grace the UI. If you swap it for a set of cannons that are red instead of grey, then there should now be red cannons on the HUD. I thought to myself,
How hard can it be?
Oh, I crack me up sometimes. It’s a balance thing — do I expend the effort now to smash through problems, or save the effort for later by having to make an icon (or three) for every single item I end up adding?
Well, knowing as we do how crap I am at 2D art, it was an easy answer. I know where I stand with 3D models. I know how they work. I know how to manipulate them. This is a price worth paying up front.
Screen Space Overlay mode, unfortunately, does not support 3D components, and this is where the pain begins in earnest. Screen Space Camera is what you need, which changes everything. Those object bounds coordinates I had? Nope. Getting the UI to even draw anymore? Don’t make me laugh!
As the name suggests, Screen Space Camera requires a camera. But I can’t use my actual game camera, because an interface needs an orthographic rather than perspective render.
Luckily, you can have multiple cameras! By setting their “depth” and their behaviour regarding unrendered parts, one will overlay the other without further ado. Since a heads-up display is mostly emptiness, this is perfect. The downside might be that the 3D UI now exists “in the world”, so the player’s avatar might physically stumble across it, but luckily you can make cameras ignore specific layers — main camera ignores the UI layer, interface camera only draws the UI layer. Job done!
I use this to indicate the current primary weapon, and I expect to do something similar for the 5 abilities that will one day grace the middle of the screen. Maybe there will be an armour indicator before then, and I can use the current blast shields model for that.
So after getting the UI to exist again, with its 3D payload safely attached, I had to synch up all my coordinates again — and I’m sad to say, it is ugly.
Mouse input works well enough, having to simply be transformed to match plane of the canvas instead of working immediately against the plane of the screen.
My actor bounding boxes, however, despite previously working directly in the same way as mouse coordinations, need to go through a bit of a wringer — width and height still work, but position has to be calculated with WorldToViewportPoint (a coordinate from 0 to 1 rather than 0 to resolution), then multiplied by the width and height of the canvas. I don’t really understand why.
Oh well. Dirt is a thing one must deal with in the muddled world of solo game development, and I’m sure if you saw my code you’d notice worse shockers than a few self-contained moments of mathematical wrangling.
There is a downside to nine-slice scaling, though, in that it really doesn’t work for textured interfaces. Remember those grungy metal panels I loved so much in Earth 2150: Lost Souls? Well, the middle section of a nine-slice texture does not tile, it stretches. That works well for a solid background colour, the same way stretching the sides only vertically works for the uniform stripe of a glowing outline. It does not work for a metal panel that has pits and patches of discolouration, because those patches are stretched (or squished) to fit the given space.
I would say that my course is set, but the advent of 3D raises another question — should I completely ignore 2D components and go the whole hog? Imagine those raised panels on a grungy industro-military interface not being painted on, but being real lumps and bumps, carved out of a very large plane of metallic texture. Would that solve all of my problems?
That will be another story for another day, methinks. Until then, here’s my blatant rip-off of UT…