Tower Tanks: Third Camera Revision
My goal with this task is to implement our new plan for the in-level camera manager. Figuring out a camera system that works for this game has proven to be among the most stubbornly difficult-to-design-around problems we've faced during this development. The camera needs to be able to present a very clear and readable view of the inside of the player tank at all times so that players can navigate it smoothly as a level, however it also needs to be able to fit an engaged enemy tank onscreen. Keep in mind this enemy tank may be boarded at any time by the players, and thus also becomes a level which must be navigable without having to squint too hard. On top of that, because tank combat often begins at long distances, secondary visualization is required for gunners to be able to track the trajectory of outgoing and incoming fire.
So far, our answer to this problem has been incomplete. We have opted for a splitscreen-style camera system which focuses the player tank center-screen until an enemy combatant ventures within combat range. At that point, the system switches abruptly to a 50/50 split of the screen, with the two tanks arranged in the order they appear in the level (in theory if the player tank was ambushed from behind, the enemy combat cam would appear on the left). Above these two split screens is the "radar cam," which visualizes the combat as a whole and allows for aiming. When the two tanks are within a designated distance threshold from each other, their cameras become merged until they break the threshold.
The old system
This methodology has been a pain point for playtesters. Given that smooth animations are not implemented for the splitscreen, players become frustrated and lose track of themselves when the camera shifts abruptly. Also, we end up wasting a bunch of precious screen space when there is a stark difference in size between combatants, because the mirrored cameras must match each other in scale. This especially punishes long tanks. Furthermore, the radar screen often provides redundant and unclear information, because it is simply a zoomed-out version of the normal camera that vertically moves to capture both tanks, which it often struggles to do when the altitude separation is sharp enough.
Our new solution is as follows:
- Do not subdivide the screen. The entire screen is, by default, dedicated to the player tank. This makes the game feel much more expansive and less locked into a window. A content-aware buffer zone will need to be created so that the camera focuses the player tank in a position where it is not obscured by UI. The buffer zone will need to adjust smoothly to account for UI changes. This is also necessary because we have moved away from UI that is blocked off from the visible camera zone of the screen, and are opting for UI that lays over an active camera.
- Make the player cam more dynamic. Always having the player tank centered in screen is unnecessary and inefficient when it comes to conveying important travel information. The new system will pan the camera in the direction of travel so that the pilot is able to see threats in the real world earlier than they otherwise would, increasing player situational awareness. The camera will also zoom out based on some incremental factor (probably speed) to enhance the effect. We will limit the amount that the camera can be zoomed out so cells in the tank are never untenably small in the frame.
- Abstract radar cam. This has already been fixed by Peter. Instead of using the full-detail assets in the radar cam, we will use bare-bones representational sprites to only convey necessary information. This allows us to make the radar cam a heck of a lot smaller while conveying important data such as where manned weapons are pointing, mortar trajectories, and the positions of active projectiles. We will still need to figure out how to get the radar to encompass both tanks at all times without stretching unnaturally (and maintaining a consistent range).
- Windowed enemy tank cam. Instead of dedicating an entire half of the screen to enemies within combat range, a much smaller window with a shrunken view of the enemy tank will slide into frame when an encounter begins. The window will grow or shrink depending on how far away the enemy is (mirroring the 3D experience of objects looking smaller or larger depending on how distant they are). When the enemy is close enough to the player, their camera will grow to match the real tank size relative to the player tank, and the frame will disappear, smoothly merging the cameras for a combined dynamic battle camera. This will involve the player camera panning and growing dynamically, using that content-aware buffer zone addressed earlier.
- Mini-cams on player cards. In a previous version of the game, I built a system that made tiny camera bubbles pop up around the main camera when players ventured offscreen. This is ugly and ineffective. Instead, when a player is wandering somewhere outside the normal tank cam area, they will be shown an ample view of their surroundings inside their player card at the bottom of the screen. This way, players always know where to look, and the offscreen visualization doesn't have to tangle with the movement of the main cameras. A perfect solution.
UI overhaul, still with the old camera system
Because this overhaul is so extensive, I will be rebuilding the camera script from scratch.
Starting Thoughts:
- To start out with the new script, I am implementing a more generic targeting system for my cameras. The previous version of the combat scene camera manipulator script references a tank controller directly and pulls data from it, which became a problem when the system needed expanded functionalities, like being able to track the remnants of a destroyed tank. With this new system, I will be having a subclass called CamTarget do all the dirty work of handling weird edge cases and unusual situations, while the CamSystem class and its manager can stick to handling camera positioning alone.
- Because our new system is not designed to be so loosey-goosey with being able to handle ANY amount of onscreen tanks, I can really lock down and simplify the handler and make it more specific. Instead of programming functionality for every camera to be able to handle theoretically infinite tanks, and for the system as a whole to be able to handle theoretically infinite cameras, I can make the Controller class itself more specific and therefore more straightforward, which is a blessing.
4/8/2025 - Tues
- Improved tank camera targeting functionality by blending old system for centering and sizing camera (which estimates on-screen volume of tank using tank size values) and unused tank bounds system which gets exact current axis-aligned bounding box of tank (good for finding effective center mass for camera but bad at finding extents because it fluctuates based on tank angle, and having the camera zoom in and out based on tank angle looks wonky). The following gif demonstrates the difference between these two systems, with the green boxes representing the actual bounds of each room, and the yellow box representing the pre-determined tank dimension centered using the actual bound calculation (see that it tries to split the difference vertically and horizontally when the tanks bounds exceed predicted values due to tilt).
- Ported radar and player camera functionality (including parallax and screenshake) from old camera manipulator, streamlined scripts by removing multi-target stuff for now
4/9/2025 - Wed
- Created framework that confines tanks within the main tank camera to a designated dynamic zone, which allows us to introduce UI that moves into the screen and adjust where the tanks are so the two never overlap
- Implemented functionality that automatically adjusts camera to fill the designated dynamic confinement zone (purple area) with any number of defined targets (including their preset buffer space), without cutting any parts off. This is a powerful tool for positioning objects flexibly in the main game camera, as it focuses on a given target itself while allowing the rest of the background to fill low-priority camera real-estate.
- Fixed radar framing so that tank is glued to middle of far left side. I had anticipated having to incorporate an elaborate solution to fit terrain into frame, but this might actually work alright with relatively few modifications.
4/10/2025 - Thurs
- Implemented opponent visualizer cam, opponent tank always fills the space given and the window can be moved around wherever using a visualizer
- Having a persistent and VERY irritating issue where the framed tank would stutter as it moved. Realized this was linked to the way I was finding the center of the target tank bounds (something I applauded myself on doing differently earlier). The problem was that the program was getting the center using a bounds.encapsulate on all of the tank's individual cells, the positions of which only update on physics steps, and thus the discrepancy between their position and the tracked pose offset position the camera was linked to (a transform for the whole tank) resulted in chop.
I spent way too much time today trying to solve this problem unsuccessfully. In lieu of a solution, and not wishing to sacrifice the more accurate camera centering model, I have kept the physics-based bounds centering system and added a smoothing factor to the camera to reduce jitter. It is not perfect, but it is better than the previous camera system's centering process because it will radically reduce the possibility for cells in long or tall tanks to leave the confinement area.
4/16/2025 - Wed
- Separated camera functionality for player cam, radar, and opponent cam into three separate classes that inherit from CamSystem. I wanted to avoid this initially because there's really only ever going to be one of each in any given scene, but structurally this makes more sense than having three instances of the same class all basically running entirely different code (separated by disgusting switch statements). This will also make implementing offscreen vis cameras easier (maybe). My initial impetus to make this change is needing some sort of unique statemachine for all three cameras, and having three separate statemachine variables in the same script or sharing one and using it differently depending on camera type would have been a bridge too far. Decoupling was a nightmare because the now-obsolete camType enum is used 36 times in the script, however the camera manager feels SO much more organized now this is very nice, even if it means I have to repeat a few minor blocks of code.
- Had a very funny weird issue where the engine would hard crash when playing for the second time. Eventually figured out it has to do with instantiating gameObjects in the constructor, which wasn't a problem before I decoupled the cameras into child classes, but I guess probably has something to do with that (maybe the way that virtual and override methods work I dunno).
- Added main camera multi-tank encapsulation. Now when an opponent tank gets close enough to the player, the opponent viewer cam deactivates and the player cam grows to encapsulate it. Next step is smooth merging...
4/18/2025 - Fri
- Very nice piece of progress: I have gotten the zoom adjustment right for the opponent tank cam merging phase. Now, as the opponent tank gets closer, the player tank camera will zoom out towards the ortho size that will be needed to fully encapsulate both tanks. This way, at the exact moment when the opponent tank comes on screen, the player camera ortho size will be the same as when it is tracking both tanks and the transition will be smooth.
- Built in functionality for the camera to track the rightmost tank during opponent tank encapsulation, so that when the opponent tank goes offscreen, it will be fully covered seamlessly by its visualizer. I may choose to blend out of this state if the two tanks get real close.
- As part of merging into shared tank camera, the opponent visualizer window needs to move up or down to accommodate where the tank will actually appear when it becomes visible on the player camera. The math to make this happen is very weird because I need to be able to compare the real world position of the opponent tank to the world position of its image in the opponent visualizer relative to the player camera.
- Met with Ryan, talked about some system parameter changes:
- I have been going crazy about how bad verticality messes with the camera view. Tall tanks and hills just ruin the ability to see or do anything on screen. We've found a solution we think we like, which is to just have the player camera cut off the top of tanks that are taller than the player tank and go outside the confinement area. I am not fully sure how good this is going to look, but I think it's a good idea.
- I am going to lay off trying to have the opponent visualizer frame PERFECTLY blend into the shared engagement camera. We can get away with so much if it just animates offscreen to reveal the enemy tank, so I don't have too much more work to do before the system is as we want it.
- We are considering having the visualizer camera shrink into a heatmap (similar to what the player has on the left side of the screen) when transitioning into merged engagement mode. I am not super sure I like this, but it would be cool and might convey information that would otherwise be lost by cutting off the top of the tank with the frame. My concern is this give somewhat inconsistent info throughout the battle, as in it doesn't make a ton of sense for that heatmap to disappear when it just turns into the normal tank camera feed once the tanks separate again.
- I have been going crazy about how bad verticality messes with the camera view. Tall tanks and hills just ruin the ability to see or do anything on screen. We've found a solution we think we like, which is to just have the player camera cut off the top of tanks that are taller than the player tank and go outside the confinement area. I am not fully sure how good this is going to look, but I think it's a good idea.
- For my last project of the night, I have built in a functionality I've been talking about for a while and knew would be easy: when the enemy tank is farther away, it is smaller inside its visualizer. This is done with a simple lerp toward a designated zoom modifier based on separation distance. I love this system because it adds a sense of 3-dimensionality to combat, meaning that far away enemies actually get smaller (in the odd 2D way you can see them). I feel like this will have a positive psychological effect, even if players don't really notice it. It just gives the world a little more depth I think, so the feeling of having an enemy right on top of you is fundamentally different from that of seeing one at a distance.
4/19/2025 - Sat
- Created a feature that any camera using the CamSystem class can implement. If initialized with this feature, a camSystem will now be set to render to a texture in the UI canvas, similar to how my old offscreen visualizer system worked. This gives us WAYY more flexibility with cameras that need it, as the raw texture can be rotated or moved offscreen (which cannot be done by simply defining the camera output rect as I was doing before). I also think it may be more performant in certain cases (it allows me to modify resolution freely which is nice).
- I have completely done away with the merged combat cam tracking the vertical position of the opponent tank. This way, no matter how big the opponent is, they don't mess up the players' view of their own tank.
4/22/2025 - Tue
- After tearing it apart on saturday, I realize there is a place for enemy-based height adjustment in the camera system and am adding it back at a limited scope. When an enemy tank is sharing the camera with the player but is merged, the increase in camera ortho size due to bound width means there is empty headroom above the player tank if we keep the camera pinned to its treads. So I have re-implemented the feature where the bottom of the camera frame follows the lowermost tank, however I don't want the camera to be able to follow the lower tank (if indeed it is the enemy) so far down that it raises the player tank out of view. By finding the world space top of the camera confiner, I can compare that to the world space top of the current tank bounding area and prevent this from occurring, while dedicating maximal screen space to keeping both tanks on screen as much as possible.
4/23/2025 - Wed
- Implemented opponent camera frame exit and entry animation so that it blends more pleasingly. There are separate animation curves for both entry and exit, and the opponent camera script keeps exact track of what animation stage it is in at all times.
- Fixed all the bugs that come up when a tank is destroyed and the camera system loses a target. The last system also had a feature where the camera would linger on exploded tanks before disengaging, so I have added that here by having the CamTarget component spawn a limited-time husk of itself upon initial destruction.
4/26/2025 - Sat
- Added functionality to CamTarget which allows system to store tank size values and smoothly blend between camera sizes when parts of tanks are destroyed in combat. This serves a similar purpose as the linger period on exploded tanks from Wednesday, that is to prevent jarring frame shifts right after big explosions or changes to tank geometry. Now, CamTarget will wait a little bit before beginning a blend between the old tank size and the new tank size, and send those pre-processed bounds to the CombatCameraController. This is an excellent use case for the whole CamTarget system, as it filters out a lot of the edge case minutiae that really clogged up the works with the last camera system script.
- I have even added a feature where the CamTarget stores a secondary set of tank size values right before beginning the blend transition, so that if they change again during that transition (a somewhat likely edge case), it will still be moving toward the same target, and simply restart the wait and transition process once the first transition is completed, maintaining camera continuity.
- Filled out functionality in TankManager so that I can spawn the odd-shaped shops Ryan has made for camera stress-testing purposes.
- Moved buffer values calculation back over to CamTarget (buffer values add space around the edges of targeted objects so that camera isn't always perfectly encapsulating them). I originally moved this system onto the CombatCameraController and made it generic to all targeted objects because I thought it would be simpler than to have a separate buffer value for every target, but I now realize that it makes more sense since CamTarget is functioning as a filter for other similar object-specific properties and buffer values are not necessarily generic (i.e. player offscreen cams will have different buffer values than tanks). This also simplifies/removes some dodgy code snippets in CombatCameraController.
- Implemented system for blending through that ugly jolt that happens after the linger state on a dead opponent ends. Before, the system would wait for a little bit after an opponent dies so that the explosion is visible, but then the camera would snap back to baseline values. Now, it figures out a timed blend state between baseline and merged values and automatically blends the two after a designated period of time, making the opponent tank death process fully seamless.
- I also added an extra check that disables the offscreen visualizer as soon as this blend transition starts, so that it has time to animate away smoothly before its dummy target is destroyed.
4/28/2025 - Mon
- We had a big integration meeting on Sunday, and now that my system is implemented with a more updated version of the game (and we've done some cursory playtesting), I have some bugs to fix. The first one I've addressed is to prevent opponent tanks from being skipped by the targeting system (due to multiple tanks being spawned at once). I have addressed this by having non-targeted tanks always scan for when the opponent camera system is available.
- Because the radar will have to be able to be enabled or disabled in scenes like the tutorial, I have given it the RenderToTexture functionality that the opponent visualizer camera uses and built in a system for it to animate on and off screen. I have also changed the border from a predetermined sprite to a dynamic and modifiable one, for added flexibility.
5/1/2025 - Thurs
- Integrated camera system into tutorial. I have built a small script for custom markers that can be placed in scene and used to conditionally activate camera features. Right now there is one which checks for when all players have entered the tank and one which activates once the tank crosses a certain threshold. Blending between my camera system and the tutorial dolly is done automatically through cinemachine with easy priority changes.
- Fixed bug where having multiple opponents in scene at once would cause the opponent camera to crash out.
- Finally re-implemented screenshake by hunting down every script calling the shake method on the old camera system and updating it to the new shake method (which works identically).
5/3/2025 - Sat:
- Fixed a bug with the tank size adjustment blend I implemented last saturday. The program was having issues because I was only blending between the bounds for the old tank size and the new tank size, and I was not taking into account the center position, which is calculated dynamically. This was a bit tricky to solve, however my solution was to store an array of corners (of room bounds) on tank load and then after the completion of each blend, and then rotate and position that array dynamically relative to the tank (on any given frame during the blend). This allows the program to know what its shape used to be before a cell destruction, so it can figure out where to center those bounds properly when blending to the new size (even if the tank is moving and tilting wildly during the blend).
Before the fix:
After the fix:
Stress test:
- Turned radar frame system into a general camera feature which can be implemented on any CamSystem subclass. Now the opponent tank visualizer can use the same code to generate a frame.
- Implemented system that scales orthographic size of opponent visualizer camera up or down (during the merge phase) so that when it merges with the main player cam, the tank in the visualizer is the same size as it appears in the main camera. This is one of the last steps on the road to truly seamless camera merging.
5/4/2025 - Sun:
- At long last, I have achieved the improbable and produced a camera system with relatively seamless camera merging. By implementing a procedure which moves the opponent visualizer camera offset as the opponent camera begins its blend process with the main camera, the enemy tank is moved into the exact ideal spot (vertical offset is already accounted for) for when the visualizer disappears. I have built in systems to account for wider or thinner tanks, which need to be moved to different sides of the visualizer for the effect to work without the tank overrunning the position of the camera window. The finishing aspect of this effect is my implementation of an offset which triggers the visualizer to disappear earlier than the merge distance would dictate. Now, the opponent visualizer camera disappears at the precise moment when its subject is overlayed over its counterpart in the main camera. This required the merge value (which controls blending of effects like zoom, camera position, and camera window position) to be based dynamically off of the distance between the opponent tank and this theoretical point in the camera (as opposed to the previous approach where it was based off of hard distance between the treadsystems of the player and opponent tanks).
- Unfortunately, the parallax system in the game right now is based on camera delta movement rather than absolute camera position, and thus the position of the background is non-deterministic and will never naturally align when I'm doing my blend. As a temporary measure, I have hijacked this parallax system so that I am able to blend the positional values of one parallax group into those of another, allowing the animation blend for the tank position in frame, frame position, and visualizer camera zoom to incorporate a smooth adjustment to the background so that it lines up perfectly when the cameras finally merge. When my workaround is working as intended, it looks like this:
Not perfect, but as far as player attention goes, this is close enough to perfect for me. I have tried to make the unusual movement of the background more palatable by mapping it onto an animationCurve which eases in the changes in background velocity. The system works great when the tanks are near world zero, however due to the way the parallax loops, it breaks down severely during encounters with tanks further along in the level. I believe I may have to rebuild the parallax system to fix this.