Before we start, please play this music in the background
I’m Oscar Brittain. I made a game called Desert Child. Desert Child is a hoverbike racing RPG out on Steam, Nintendo Switch, Xbox One, and PS4 on December 11th. Check out the trailer so we’re on the same page.
I’ve been asked how I did all the faux-3D scaling and parallax effects in the game, and I wish I could tell people that I’m one of those some GML tech demo wizards, but it’s really just simple maths, badly implemented. The following is my explanation of some techniques I used, which I offer as inspiration for people more talented than myself.
When I make a game, I always start with specific scenes or scenarios in mind that I want the game to be able to create for people. In the case of Desert Child, one of the scenes was from Cowboy Bebop: The Movie.
I knew that recreating the look of this montage would be unnecessarily difficult if the game had any kind of player-controlled camera, so I started “researching” (see: Playing) games with pre-rendered backgrounds and static cameras as inspiration. Mid-90s 3D games were a good touchstone, with a mix of games using them for cinematic effect, or to fake higher-fidelity graphics.
I decided that player movement should be along a single, defined path to avoid having too much collision detection to worry about. I was worried this might detract from player’s agency, but no one has ever complained.
The reason I decided on this, was that if the player had full control over their character in each scene, that would either require them to use a gamepad or resort to zig-zagging across some scenes, as keyboards aren’t great at directional subtlety.
Additionally, I knew I wanted scenes to be shot from different angles, meaning that, if the player had full control of the character, I would have to draw the character’s walking and idle animations from at least 9 different perspectives. I did not want to do that.
So, the code. Each scene in the game is split into a background and foreground object that each run their own Draw Event with parallax code each Step, which looks something like this:
Each part of the background is split into different layers that all scroll at slightly different speeds to give an illusion of perspective. The foreground object basically does the same thing but inverted.
So that's simple. Two objects, with the player character, sandwiched between them. Something slightly more involved was how I did the 3D perspective and model in the Piazza.
I had wanted to have a room in the game like the cathedral from Symphony of the Night, but I could never think of a good reason for it, so it ended up being just the entrance to the Bean Market in the game.
If you’ve ever looked at doing top-down 3D in GameMaker, this will be familiar to you. This is basically the draw_sprite_pos() technique of redefining the 4 corners of a sprite each step and then tying that to the camera’s x position.
For the sides of the buildings, I have two X positions of the sprite I’m drawing set to a static value; this is where the background of the scene is. Then the other X positions are set to the parallax coordinates that move relative to the camera’s X position. So basically, I’m drawing a sprite that is tied between two parallax layers, and it stretches and deforms as they move.
Hopefully, that makes some sense.
The other weird effect in the scene is the water fountain in the center of it.
This has a parallax effect identical to the other rooms, but it’s also got a 3D rotation animation that plays. This is a bit weird, but I’ve done it a few times in the game to achieve the look of a 3D model rotating in relation to the camera.
So, the fountain is a sprite with 150 images of the 3D model slowly rotating. The image_index is just linked to the camera’s X position but then divided by 13. This means that as the camera’s X position goes up or down, the fountain’s image index will also go up or down, albeit 13x slower. The +67 is just to ensure the fountain’s rotation starts at the right image.
The scaling of the player character was a lot more involved than I thought it would be, as I’d assumed, I’d just be linking its image scale to its Y position. I ended up having one of those hot shower epiphanies after struggling with a bunch of potential solutions, but I think I’ll use this technique again, so I guess it was worth it.
Each room has two objects: objCharScaleAnchor and objCharScaleAnchorMax. These are character anchors. Their position is static, and they define both the vanishing point in the scene (objCharAnchorMin) and the opposite of the vanishing point (just a place where the character hits its max image scale.)
These objects literally do nothing, as they’re just referenced by other objects, but are just easier to move around a scene, and can be dropped into any room and that room essentially then has faux 3D enabled. The player object then moves toward or away from either of these points as the player controls them, removing any annoying directional issues, or the need for tank controls.
The player object then has a little scaling algorithm that checks its position relative to the Max anchor and the Min anchor, then works out what it’s image_scale should be, and what it’s movement speed should be. Naturally, things get smaller and slower the closer they are to the vanishing point, simulating perspective.
If the background has a consistent perspective, I can tweak the scale and position of these objects to get the scaling look just right. It’s not perfect, and for very long perspective shots, I had to do some manual fudging to get it to work, but as I said in the intro, these are ideas that I’m sure someone else can improve upon greatly.
This is the same system I used for the scaling of objects during races, too.
I guess the big takeaway from this whole endeavor was that there is merit in looking at ways other than true 3D to simulate depth and perspective. When I switched from GameMaker Studio to GameMaker Studio 2, I was thankful I hadn’t cheated and used the obsolete D3D functions, and I had created a bespoke solution for my exact needs.
In conclusion, go and buy my game.