Jacob Bell's Blog

Forced Perspective in Magic Coffee Shop

23 January 2022

A lot of 2D games use an orthographic or isometric art style, but I went for a more 3D perspective in the hand-drawn background of Magic Coffee Shop. I was inspired by Playstation 1 Final Fantasy games, which pre-rendered their 3D environments as 2D images so they could have lots of detail on the low-powered hardware. They separated the environment into several images so 3D characters could move in front of or behind them. By applying the same camera projection when pre-rendering the background and real-time rendering the characters, the two blend together nicely.

I didn't make the background in Magic Coffee Shop using a 3D modeling program; it's all hand drawn in a forced perspective that makes no mathematical sense. Making the characters move in front of or behind objects and get smaller as they move away from the camera requires some customized tricks.

Problems with Perspective Depth

Since 2D has no depth axis, you need a clever way to scale down characters as they move away from the camera. Most of the time moving up along the y-axis in the scene corresponds to moving away from the camera. A simple formula tuned to the scene can scale characters based on their y-positions.

This method has a problem with multiple height planes.

At these two positions, the character is the same distance from the camera and should be rendered at the same size, but he has very different y-positions.

Problems Determining Z-Index

In the absence of a proper depth axis, Godot engine refers to the draw order of sprites as the z-index. A sprite will appear in front of another if its z-index is larger. If the sprite's origin is placed where it touches the floor, we can calculate a good z-index using its y-position.

This works well for axis-aligned sprites, but has problems for diagonal ones.

One solution is to split the railing into several sub-sprites, but this could still have problems for narrow objects.

Depth Regions

There are only 3 significantly different areas in Magic Coffee Shop: the ground floor, staircase, and upstairs. I defined a polygon for each to represent the missing depth information needed for rendering.

I gave each region a minimum and maximum scale value that corresponds to the highest and lowest y-positions of the polygon's points. The polygon is hit tested with each dynamic object's origin, and if they're overlapping, scales the object by interpolating between the minimum and maximum scale based on the object's y-position. Now upstairs and downstairs can have individual scaling factors.

The two bigger regions are connected by a staircase. The minimum and maximum scaling factors of the staircase region must carefully line up with the upstairs and downstairs regions or a sprite's scale will visibly jump when crossing the boundaries.

The regions also change the z-index of sprites, according to sprite.zIndex = region.zIndex + sprite.position.y (positive y goes down in Godot engine). The ground floor region's z-index is set to 1000, the staircase region's is 0, and the upstairs region's is -1000. Now a sprite always appears in front of the railing on the ground floor, and always appears behind it when upstairs.