Vladimir Fedyushkin and Nicolai Danielsen are working on Jet Lancer! Nicolai has written the following blog on how they use 2D and 3D tricks within GameMaker to achieve the amazing look and feel of the game.
Jet Lancer is an action-packed game where you control a fighter jet that is as fragile as your enemies. This makes mobility the most important key to your survival. It would be near impossible to keep such a high intensity in a 3D game and still make it feasible to pull off all these stunning manoeuvres, so we went with good old fashioned 2D. The great thing about 2D is that you have a complete overview of what’s going on and never have to worry about enemies sneaking up from behind. That said, we really wanted to make it feel like there is an expansive and living world behind the screen, so we used some tricks to fake just that.
Before we get into the meat of this blog post, here is the teaser trailer for reference:
2D TO 3D
The main gameplay of Jet Lancer takes place solely on a flat plane (sorry for the pun)… but that doesn’t mean the backgrounds have to be flat! We use a very simple method to create that sense of depth. The backgrounds are standard sprites that we stretch with a handy free script called
draw_sprite_pos_fixed that you can find on the Marketplace here. The reason for not using the built-in function
draw_sprite_pos is that it will create diagonal seams in the drawn texture, but luckily the Marketplace asset allows us to simulate perspective without that problem.
With the script ready, all we need is to calculate the positions for the 4 corners of the background. This is actually simpler than it might seem at first glance and can be done by offsetting the coordinates relative to a focus point, which will usually be tied to the camera.
This is only a simplified version of the code used in Jet Lancer and you could easily spend time tweaking this to get it just right, but I think it still gives a very good result for such little work.
Admittedly it’s not physically accurate 3D. That doesn’t really matter though, because it looks close enough that no one raises an eyebrow. At least no one has called us out on it so far…
3D TO 2D
Maybe the above technique is already familiar to you, but there are still many other methods of combining 2D and 3D in convenient ways. In Jet Lancer we also have an overworld where you can travel to the various mission locations. This is your moment of peace in the game and as such, we decided it would be appropriate to make the map an explorable 3D model. All combat is contained in the missions themselves, so we don’t need to worry about quick readability as much.
If you have ever worked in 3D before, you’ll know how much of a pain it can be to achieve smooth collisions between models. Luckily you don’t always have to do it the complicated way. The overworld is functionally still a 2D plane that you control your ship on, which means we also only need a 2D collision solution. If you have used heightmaps before, you might see where I’m going. A heightmap is essentially an image where each pixel has information about the elevation of the terrain at that specific point. They are traditionally used for creating a 3D terrain mesh, but they can also be very useful tools after the terrain is built.
In order to generate the heightmap, we project a depth path of 3d mesh onto a flat orthographic image.
To have more control over collision, we also have two different 1-bit black/white texture maps to indicate whenever the player is over the water surface, on land or approaching impassable parts of the terrain, such as mountains and other massive landmarks.
After all three maps are ready, we combine them all into one texture using Red, Green and Blue channels to store information for heightmap, land/ocean level collision and impassable objects. This helps to save a lot of memory, which is quite important, as generally, we want our heightmap to as high-res as possible in order to achieve high collision precision. And having 3 separate textures for storing purely black/white pixel information is a total waste of space.
When everything is assembled and ready to go, we proceed to implement the heightmap in-game:
The upper left corner of the GIF above shows the debug view for the overworld heightmap. Each color channel stores certain information about the world. All we need to do is sample the heightmap at the player’s location to know if we are colliding with a mountain or moving out to sea. This sampling can be achieved by using a surface, but for better performance, we decided to convert the surface to a buffer first using another free asset from the Marketplace called
buffer_getpixel, which you can get here.
First, we draw our heightmap onto a surface and convert it into a buffer in the create event. It will look something like this:
Pretty simple right? Then we can easily sample the buffer to get our world information at will.
Now we know exactly what the player is standing on and where we need to move the ship in the z-axis to align with the model. Even impassable objects can be accounted for. You’ll need to write some logic to handle all these cases, but the general idea is exactly like what we use in Jet Lancer.
In conclusion, it can be super helpful to switch up your mindset between 2D and 3D sometimes. Many techniques can be reused in ways that they might not have been intended for and sometimes that makes some difficult problems much easier to solve. Never over-engineer something when a simpler method will do just fine.
I also think it’s fair to say that GMS is capable of much more than you’d expect. Sure, it’s specialized for 2D games, but with a bit of creativity, you can make it do anything you can imagine. Even fancy loadout screens for customizing your jet!