DT Mark is the creator of TAWBS, a minimalistic, trial-and-error, top-down-shooter. The game features a great visual style that displays in slick UI and menus which get broken down and detailed in this development blog.
TAWBS is a game about a square shooting other squares, making its way through intricate levels.
Its visual keyword is “minimalism”, so it was only natural to design its pause menu solely around abstract shapes. This blog’s purpose is to show a behind the scenes peek at how this menu came to be, from both a design and technical perspective.
Before even thinking about code, the design started with many paper sketches. Everything, ranging from the layout of the screens to the movement of each element during a screen transition, was planned out and written down months before starting to even write one line of code. This helps knowing where to start and also how.
I decided to put the entirety of the menu inside one object, instead of splitting it into several objects that I would have to manage whenever pausing or unpausing. This workflow obliges a thorough way of storing data on how everything on the screen moves. While GMS2 does have lightweight objects on its roadmap, I had to resort (as a GMS1 user) to various data structures.
Each graphical element on the screen has its own data structure and set of variables in which I store its attributes or properties. Let’s take the example of the spikeball in the top right corner.
Before knowing what variables to define and store, I need to know how my spikeball will behave, so I can know how to draw it. Here, the spike ball rotates, moves, and has snappy and wiggly spikes. Based on that, I know I will begin by drawing a circle, at an x and y position, around which I can draw triangles, or more precisely a large primitive. I draw those triangles by looping through the circle angularly, a couple of degrees at a time. Every second iteration, I draw a point at the radius of the circle. The rest of the time, I draw a point at a bigger radius, which represents the top of each spike. This extra radius is therefore what I will store in a
ds_grid, whose length will be the number of spike points.
Now, each spike is supposed to both burst out in different directions and slightly wiggle. In order to do that, I need to define for each of them a target point to which they will constantly lerp to at all times. But that point will randomly change every couple frames or so, which creates this erratic effect. That point’s position/radius will also get stored in that grid.
As for the slight wiggle, I just add a sine value to the point position, which is defined by 3 different properties: amplitude (what’s the maximum value it can reach when wiggling), speed (how fast it wiggles), and offset (a number desynchronizing a wiggle from the other wiggles). Typically, I just randomly generate those numbers then put them in an array, and use that as a property in my mainline grid.
Finally, to rotate the spikeball, I just add a value to all of the angles, which is stored in a separate variable. And these are all the parameters I need to define as instance variables since they will be modified throughout the object. Constant parameters that simply describe the spikeball, such as its radius, colour, or the amplitude of the spikes, are rather put as local variables right next to the code drawing the spikeball.
And this is how I operate for pretty much every graphical element of the menu. Buttons, for example, are each stored in a grid. They’re defined by their label, whether they’re focused on, whether they’re selected, the angle of their vertical line, their scale, their x position, the y position of the top left corner, and an array storing how the lines connecting them to the main bar randomly move.
Things like the exact position of each button are handled by the drawing code exclusively. But on top of that, I also need to store for each button an array holding the information regarding these random red squares that appear on their borders when they’re focused on. In order to do that, I define an array with 2 cells, one for the top border and one for the bottom border. Each cell holds another array, which is a sequence of square sizes. Typically it’s always 2 squares on one side and 3 on the other, but whether there are 2 squares on the top or on the bottom is decided randomly - in other words, the order between the 2 cells is random. The square sizes are picked randomly from a pool of presets. Finally, I also need to draw the sequence of squares at a random position, but once the position for one border is set, the other border will automatically be negative. And when a border is negative, it’s drawn from the other end of the button.
The same procedure is applied with the particles that come out of the button or the ones in the background - the reason why I don’t use Game Maker’s built-in system is because I need more flexibility with the movement of the particles - like them going up and down when pressing buttons, or getting flushed down in a tornado. The idea is just to store the attributes of the particles in a grid, loop through that grid to update them and manipulate their attributes every step however you wish, then draw them based on their attributes one by one.
A good way to add liveliness to a menu is to make things slowly move on-screen even when nothing is happening. Through the method I just described, I could build a good amount of animated graphical elements that fill in the screen to make it less empty and boring.
However, those static background elements are just half of the job. A good menu also needs feedback for when buttons are selected, pressed, and menu pages change. I focused a lot on the latter.
Indeed, every time an option is selected, the button corresponding to it flies out as if carried out by the wind.
Doing this through code only would’ve been very difficult. Each button is drawn to a small surface corresponding to it. In order to animate it, I would need to bend it with shaders, blur it in the right direction, and define the complex movement just through custom paths or math formulas. Instead, I decided to animate each button’s animation externally, in Adobe After Effects, then export it as a sprite to be displayed in Game Maker. To do that, I took a screenshot of the pause menu right before the button was pressed, masked out the button, and messed around with it. Note that I could’ve also exported the button surface to a file directly through Game Maker instead of screenshotting and masking.
The problem was that exporting each animation in full resolution would take up some memory space in the game files, even if there weren’t a lot of frames. To avoid that, and reduce the animation file sizes by up to 35% (which admittedly isn’t much when talking in sizes of less than 1 Mb), I cropped the animations to their bare minimum. I deleted their position keyframes (well, not exactly since this would mess up the bending and blurring effects I had applied on it) and tried to make the entire animation fit inside a box - that’s the cropping part. Alternatively, I copied the original position keyframe data from After Effects and pasted it into a spreadsheet. With that, I generated the declaration code of an array within the spreadsheet itself, an array that would hold the x and y coordinates of the button for each frame of the animation. I then copy-pasted that in Game Maker. After importing the cropped animation and making sure the origin of the sprite was the same as the origin of the flying button in After Effects, all I had to do was declare a variable that would serve as a custom image index, and then draw the sprite at the appropriate positions and at the correct image index whenever needed.
All in all, this hacky workaround of going through After Effects to create an animation sequence could’ve been simplified with a dedicated sequence framework, which incidentally happens to be coming very soon to GMS2 this year.
While the buttons are always randomly generated, the exported animation is always based off of the same button that I screenshotted. However, it is unlikely you noticed that just by looking at the animation since everything moves so fast. This illustrates the principle that animating is not about perfect movements, but it’s about the illusion of perfect movements. Being okay with imperfections and knowing to judge when they’re acceptable is an important part of it.
The shape animations revealing the game after quitting the menu are rather complex since they’re associated with the smooth movement of the button. These were made externally through After Effects then imported as sprites, except this time, I couldn’t crop them to reduce file size. I decided to export them in a much lower resolution and then draw them 4 times bigger. Again, while it does technically look blurry (notice the borders of the mask below), it’s not a problem at such speeds - another example of a compromise that’s worth it.
I didn’t have this luxury for longer animations through, such as these masks inverting the screen. The blur would’ve been too noticeable. I also needed to take extra care with the quality of the animation. While the other ones were sometimes animated roughly frame by frame, these ones had to be very smooth. I resorted to using bending effects on giant rotating rectangles, or twirly ones. This doesn’t mean I didn’t do any frame by frame animation at all. For example, the effect simulating a tornado was too hollow, so I had to fill in the voids with an arbitrary shape frame by frame. You can’t see the borders of that shape though, so it doesn’t matter.
I could then use all of these shapes as a mask for my surfaces. There’s an official blog explaining this in more detail, but the principle is just to draw the mask on an empty surface, then tell Game Maker not to modify anything about the alpha of the surface (so if a pixel transparent it will stay transparent), and then draw the surface you want masked inside the shape. I do this with the entirety of the menu when transitioning into the game, or with an inverted copy of the menu in the level select menu.
The same is applied with the red circles from the background and the gradient of diamonds in the top right corner, which reveal a red-tinted, blurred screenshot of the current level (basically a copy of the application surface on another surface).
There can only be one circle at a time, so only one set of variables is needed to describe it: its x and y positions, its size, its maximum size, and the size of the hole in the middle, ranging from 0 to 1. A counter continually increments and those properties are reset every X frames, X being a variable (
time_until_next) that changes randomly every time. The amount of time (duration) it takes for a circle to pop is inversely proportional to its size (it takes longer for a small circle to pop, so you can see its movements well), which is why there’s no need to define another instance variable for that - a local one is enough. Once I have that, I can express the size of the circle and the size of its hole using easing functions, using a nifty script Pixelated Pope wrote.
This showed an example of how I like to link variables between them in order to spare myself from the hassle of having to define them and treat them separately. Oftentimes I try to see how I can express one variable based on another one easily. Most times, it simply involves using
map_range() is a custom script that takes a value from a set interval, and interpolates it relatively to another interval.
For example, when I want the angle of a level preview slot to vary from 0 to 180 between the beginning of a transition and the middle of it, I can just use
map_range. When the transition is at frame number 12, the angle will be 0, and when it will reach its middle point, frame 50, it will have completed the rotation (180). The angle is interpolated for any frame in between. Of course, these calculations are only applied during the transition, so I multiply the whole thing by a boolean, (transition == tLEVELSELECT), and add it to the expression I usually use to define the angle of the save slot otherwise. If the transition isn’t being played out, the boolean will be false (therefore equivalent to 0), and cancel the whole angle turn out. This allows me to script how the variable changes during an entire complex sequence, all in one line. But sometimes, things can get tricky, and it’s necessary to use ternary operators if you want to keep the entire thing in only one expression. But there’s a point where this one-liner madness can lead to the neglection of code readability, as seen below.
But oftentimes, linear interpolations simply aren’t enough to represent what I need to. When it comes to the level selection slots turning around an ellipsis, I needed something resembling a gaussian curve to express their size and opacity depending on their distance to the centre. Using online graphs helped me out a lot trying to determine the right function through experimentation. You can still see how I tried to model those parameters with linear functions in my paper notes, then realized it didn’t look like I wanted it to.
Speaking of those level selection slots, their fake 3D rotation consists in making them follow the path of an ellipsis, defined by its width and height. So their position is in fact an angle. To turn that angle into X and Y coordinates, I had to look up on Wikipedia the polar equation of an ellipsis (which is unnecessarily frightening), linking that angle to the distance to the centre of the ellipsis. With that, I could calculate the X and Y using basic sine and cosine maths.
And as seen in the animation above, to make the effect really work, I needed to draw the save slots at different origins, to space them out. That required yet another variable that lerped either to the left or right of the slot, based on the sign of its distance to the centre.
The 3D effect on the side panels is also a little 2D hack. Each parallel line is drawn at the position of the borderline, minus a certain variable that follows this formula, ensuring the lines move slower around the border and faster when going off-screen. Their thickness also increases the further they move away.
The static perpendicular lines are drawn as trapezoids whose width varies from the initial one to the maximal one, following the same calculations as for the parallel lines. Their angle is directly calculated using
map_range based on their distance to the center.
And that about sums it up! These were the main principles that explain how I made my pause menu. It’s a lot of naïve technical art, and there are surely smarter and more optimized ways to achieve these results with shaders or custom textured primitives. My approach involved breaking a lot of vertex batches, after all!
Breaking it down hopefully gave some insight on how one could go about designing UI and defining visual entities parametrically. If you need any advice, have a suggestion to offer, or just want to say hi, feel free to hit me up on twitter @dtmark_. That’s also where you can follow the game project if you wish so.