In a 2D world, whether top-down or side-scrolling, as soon as something moves outside the screen’s bounds, it’s gone. In a large universe where the player can freely move in any direction, keeping the player oriented is a challenge. It’s particularly difficult when your universe is literally the universe and the background is often just a field of stars and nebulae with no distinctive landmarks.
Local Navigation
In the player’s immediate vicinity there will likely be at most a handful of important points of interest: a central star, a couple of planets and some enemy ships. In the heat of battle the player won’t want to bring up a separate map screen to find out where they are.
The original Starcom Flash game used a minimap for this purpose:
The minimap has been much maligned lately. I think some of that criticism is exagerrated– there are situations where a minimap is the best solution.
Minimap Advantages:
- It provides on-screen directional and distance information.
- The information is spatially oriented.
Minimap Disadvantages:
- The player is either looking at the game or they are looking at the minimap. It disrupts flow for players to constantly redirect their attention between the two views. This is particularly bad in fast-paced games.
- As its name implies, it’s small. You don’t want to crowd the game action with a large map, but there a limit to how much scale you can get onto a minimap before everything shrinks to the point of invisibility.
An alternative to the minimap is a translucent overlay map.
Overlay Advantages:
- The player’s eyes stay in the game viewport.
- It’s large and can convey more information than a minimap.
Overlay Disadvantages:
- Requires the player to constantly filter information. Worse, it requires the player to constantly swap from filtering out the map and gathering information from the game, to filtering out the game and gathering information from the map.
- Players may mistakenly interpret map elements as part of the game world and vice versa.
In Starcom: Nexus, I’ve gone with a system I call “edgedar”– it’s not an original idea, but I’ve never seen it given a name. With edgedar, icons hug the edge of the player’s screen showing the direction and distance to any major point of interest or capital ship within half a sector.
Advantages of edgedar:
- Players have a clear directional sense of threats and entities.
- When a target comes into the main view it materializes right where they’re looking– they don’t have to shift their vision from a minimap or change their filter from an overlay. For an action game this is a particularly useful feature.
- Doesn’t clutter the main area of action.
Disadvantages:
- While the icons do have distance numbers, these don’t convey the same intuitive sense of spatial distances that a map does.
- Two icons in the same direction will overlap, rendering the distance unreadable. This can be remedied by hiding the distance for the further icon when two are roughly at the same angle (not implemented yet).
- Doesn’t work for the large scale– there’s too much information for the player to sort through. It needs to be limited to a handful of the most salient features.
The Bigger Picture
Edgedar tells the player about entities in their immediate vicinity, what about when they are plotting larger movements, like where to explore next?
For that, I decided to with a full-screen map. But I wanted a map system that felt directly connected to the game world and gave the players a sense of the universe’s scale. My inspiration was a short science film from 1977 called Powers of Ten.
The map screen is zoomable from an apparent game height of two million units down to 3000 units. When switching between map and normal view, the map appears to zoom all the way in, with the schematic map icons gradually being replaced by the objects they represent (the transition is slowed down in this animated gif):
As I discussed in a previous post Unity, like most 3D engines, can’t handle very large differences in scale due to floating point precision. In the above image, everything on the map is scaled down by a factor of 4000, so that one sector = one unit of distance. A dedicated map camera keeps itself 1/4000 of the height of the game camera and renders its output to a texture on the map canvas. When the map camera transitions between map view and normal view, the canvas’s alpha transitions from 1 to 0.
Technical Explanation of Edgedar
(This technical explanation is targeted primarily at other Unity developers)
Any object that can show up on Edgedar has an EdgedarTarget component. This instantiates an EdgedarIcon on a Canvas dedicated to Edgedar display. The EdgedarIcon maintains a reference to the GameObject it tracks.
The tricky bit was figuring out where on the canvas corresponds to the part of the screen edge that lies in the direction of the target. My first attempt was simply to translate the x/z coordinates of the target into the Viewport and then clamp them to 0,1. This isn’t right– it badly distorts the angle to the target.
My current solution, which has room for optimization, is as follows:
- Get the Frustum Planes for the game camera.
- Do a Raycast against each of the first four planes towards the target (we ignore the front and back planes). If it’s a hit, the point where it hit the plane is the Worldspace position we want to (potentially) show the icon. Call this the edge position. (I’m sure there’s an optimization that will let me eliminate at least two potential raycasts, but I haven’t tried to figure it out yet).
- Do a PlanesAABB test on our target’s bounds to see if it is onscreen. (Actually do a test against a new Bounds about half the size of the target because it looks better if we keep the Icon visible until the target is partially on-screen).
- If the target is off-screen:
- Use the game camera’s WorldToViewportPoint on the edge position. This gives a value from 0-1.
- Translate the Viewport position into a screen position for the canvas:
Vector2 screenPos = new Vector2(((vpPos.x * canvasRect.sizeDelta.x) - (canvasRect.sizeDelta.x * 0.5f) + xOffset), ((vpPos.y * canvasRect.sizeDelta.y) - (canvasRect.sizeDelta.y * 0.5f) + yOffset));
icon.RectTransform.anchoredPosition = screenPos;
There were numerous small details I skipped over, such as switching the distance indicator from above the icon to below when the target is above the screen, but this gives a simple overview of the technique I used.