Updated: Aug 22, 2019
I knew from the start that I wanted Alien Cow Farm to be a couch-co-op multiplayer game, which meant it needed splitscreen. Unity makes it very easy to adjust a camera's bounds on the screen, so setting up a side-by-side system like we used for S.P.A.R.K. would have been trivial. But I wanted to try something different; for one, there's absolutely no good way to do 3-players in that setup, and for another, learning and experimenting is what making games is all about. The more stuff I figure out how to do for this title, the more I'll have in my repertoire for the next one...and that's especially good if they continue to be solo projects. I decided on a diagonal design that maximizes screen space for all combinations of 1-4 players.
In order to work with irregular screen shapes, I would need to leverage some sort of masking technique. A mask is a special object used to selectively block or show the other objects behind it onscreen...it's a technique used frequently with Photoshop layers. If I didn't want to use Unity's built-in functionality for the simple, two camera case, I'd set up my view so that the first camera to render showed the entire screen, and the second one rendered on top of that with a mask that only allowed half of the screen to show. Put another way, the bottom camera is rendering everything like normal, but the top camera covers up the half of it that the mask allows to render. Now, don't get confused: when I say "top" and "bottom", I'm talking about the render order of the cameras, which can be set however you want.
When I went looking for help with this, the first and most-accepted solution I stumbled upon was the Dynamic-Splitscreen project.
While this is certainly cool, I played around with it for a few hours and decided it wasn't quite right. First, I didn't need the dynamic effect...or at least not that type of dynamic effect. I was perfectly okay with having the split line be a hard one that never rotated around, but I did still want to be able to manually change the view within each window (after I worked so hard to craft the perfect camera for a single-player view). It was easy to modify the code to make the split fixed, but keeping my other camera controls would have been harder because of the second catch: the masking in this example is done by using a physical object in the scene. There's actually a plane floating in the world, parented to one of the cameras, that has a special shader on it. This shader tells the first camera to render nothing there, and tells the second one to ignore it and render the scene behind. I just did not like the idea of something being in the scene like that; I imagined it would cause problems with my free camera and I was already noticing jittering even when just playing around with the sample scene provided with the project download. I didn't know exactly how to do it, but I was thinking that a much better solution would involve the UI system. If I could somehow mask that, it would easily be both fixed and a little more consistent.
It turns out that you can set Unity cameras to render not directly to the screen, but to a texture instead. Then you can apply that texture to a UI image component, and voila. Imagine it like a projection, you're taking the camera's output and putting it on an image somewhere else in the screen. The most common use for this feature (and the Unity tutorial that I followed to implement it) is to setup a minimap. Now, a rectangular minimap could just use the simple camera-bounds trick, but for a round or other irregularly shaped one, the last step is to apply a UI mask to it. Here we go. Now I could set up cameras and masks in the UI to my heart's content.
There was just one more problem. You see how (in this image, where I've fixed the issue I'm about to describe) both sections technically cover both the full width and height of the screen? This meant that both of the render texture images needed to be the full screen size, and couldn't be offset in any direction. But if I did that, the result would look more like this instead:
Both cameras are centered on their UFO, and both UFOs are centered onscreen, so that it actually looks like a single image rather than two separated by the split. Obviously this wouldn't work. The missing piece was a little script that could shift each camera's vanishing point off of the center by an adjustable offset. It will still behave correctly in relation to the UFO it's tracking, but the entire system is shifted away from the center of the screen, even while the image remains exactly where it should be! The result is the correct image above.
Okay, now there was just one more problem, and that was how to deal with everything I mentioned last time, about changing aspect ratios. There weren't a lot of options I could work into the complicated setup I've just described, but I could still avoid ever showing black bar borders by choosing to "Match Height" with my 1920x1080 default. So if the ratio becomes smaller, the screen's side edges creep in. That in turn causes the UFOs to no longer be centered in their sections based on the offset I calculated, and worse, by reducing the total screen area, the UFOs appear to grow larger and take up more space within their sections. The two-part solution ended up being a pair of equations that are simple enough to look at but were hard to derive. The first changes each UFO's offset based on the current screen aspect ratio (thankfully a value that can be accessed from within Unity), and the second changes each individual camera's field of view based on the number of players. Together, these adjustments ensure that the UFOs are always centered in their sections and that the area of world visible around them is always the same, regardless of aspect ratio, number of players, and general position onscreen.