CHESS IS STUPID | Tech Breakdown

Tech Breakdown

Disclaimer: I'm not a programmer at all. Somehow this stuff works, but there are probably better ways of doing it.

Object Types

So here you'll start to see my particular idiosyncrasies when it comes to GMS2 development - I like specific naming conventions and terms that make the most sense to me, starting with Archetypes (arch_*  - things that exist just to be parents of other objects):

Most objects that appear in CHESS IS STUPID! trace their roots back to the arch_actor Archetype, since I knew that I would add depth-sorting and other stuff to every instance of this.

Other Archetypes were made for volumes (which inherit a blank Draw Event since they will never be drawn), anything that should pause gameplay Step Events, etc...

Actor Archetypes share some boolean flags, offset values, etc... and also do not use standard GMS self-drawing.

Various scripts were made that could affect any Actor children, as well as more specific scripts for certain types - the shared scripts mostly affect the aforementioned flags, offsets, etc...

VFX objects in this game are also actor children, and can be spawned from other objects with the scr_actor_vfx script - which takes, as arguments, the VFX Sprite to use, as well as X and Y offsets for spawning - this can be useful for spawning VFX around an actor and have that VFX get drawn at the right depth:

Depth Sorting & Drawing

To draw actors correctly from a top-down 3/4 ish perspective, I refined a For Loop-based technique that I've used previously. In the main controller object for gameplay rooms, the StageHandler Archetype, I set up this loop and offset value (with the knowledge that every room in my game is 160x160 pixels)

In Create Event:

In Room Start Event:

In Draw Event:

In an older version of this technique, I used more complex calculations in the Draw Event involving clamps - the GMS2 profiler quickly showed me the error of my ways!

And the scr_actor_drawself script contains the actual draw_sprite instructions. This ensures all actor children are drawn roughly correctly - the max slice count can be increased for more accuracy, but at the cost of performance.

Rooks being drawn at the right fake depth:

And of course, things like actor shadows are always drawn in-between all actors and the chessboard squares.

Actors are drawn at not just their X and Y positions, but also are affected by various offsets, like a Y Offset for "jumping" - so they remain drawn at the proper depth even while their Y value appears to be smaller as the sprite moves upward towards 0 on the Y axis.

Scr_actor_drawself also takes into account offset variables from the StageHandler meant to simulate camera shake - by doing it this way, I don't need to pad my room out for real camera movement, and I was able to easily exclude certain things from the shake effect (the "void" background in stages).

If I want an event (e.g. the player taking damage) to trigger a fake camera shake, I just need to run this script to affect all actors and the chessboard itself:

Chess Piece Movement

I knew I would have to code several patterns of movements for the chess pieces and have them be:

  • Accurate to the real game
  • Aware of occupied/destroyed squares
  • Prioritised if they have a line of attack on the player

So the way I approached this was to set a global value for the constant size of the board squares, use the built-in GMS value of Direction, and spam lengthdir calculations to pick movement targets. For the actual movement, I used this fantastic script.

So, in the StageHandler, whenever a certain 'cooldown' variable is at 0 and no pieces are detected as moving, it picks a piece (finds a random/prioritised ChessPiece Archetype, which is a child of the Actor Archetype):

linked_obj_chesspiece is a StageHandler variable either referencing an object's id value (a built-in GMS value), or nothing (noone) - the region of code named "Establish Link" handles setting a particular piece as arch_sys_stagehandler.linked_obj_chesspiece if conditions are met - e.g. if it's being attacked, or has a clear line of attack on the payer, etc...

At this point the piece currently being manipulated via with() runs the scr_chesspiece_picktarget script - this checks if each piece can move in the way it should. If it can, the StageHandler's cooldown variable is reset to a >0 value and that piece gets new target XY values. If not, the cooldown value stays at 0 and the StageHandler instantly makes the next arch_chesspiece run the script until someone has a valid move to make.

Scr_chesspiece_picktarget uses switch() to check for valid movement based on a stored variable all pieces have, which just tells the switch statement what kind of piece they areExample of the Bishop's PickTarget instructions:

  • A fallback direction is picked in case the Bishop can't see the player but is still going to move
  • Count_movesquares_current decides how many squares will be covered by this movement at maximum - movement will still be halted by other pieces or missing squares
  • Here are some nested scripts that just check if the Bishop can see the player in the argument0 direction using collision_line and lengthdir
  • scr_chesspiece_targetsquareindirection will set the Bishop's X and Y target values to the X and Y values of an existing chessboard square 1 iteration of the global offset distance away in the specified direction. Argument1 is also here as a True or False, which is just whether or not to trigger the white flash effect you see when a piece moves - this is always off when the same script is used for checking to stop/continue movement across the board


The adjustment of target values is cancelled if another piece is already on the target square, and the activation flash effect only triggers when the target values differ from the piece's current X and Y coords.

When a piece has different target XY values compared to current XY coords, or is in the air (like a Knight) it executes the movement script and lets the StageHandler know a chesspiece is moving. Note that the actual movement script is only executed after the activation flash effect is done, so the player has time to react after an activated piece has flashed white:

Just like the picktarget script, scr_chesspiece_movement uses a switch statement to control what happens to each type of piece:

Example of Rook movement, because it runs a script for the rumble effect every other frame or so and that's cool. It checks, as a backup, for a difference between current XY and target XY, runs any unique stored scripts that should activate on first movement, uses the Approach code posted above to move to its target without overshooting it, and tries to continue on in its current direction unless stopped (via the checks for occupied or missing squares in scr_chesspiece_targetsquareindirection)


Even though this game has a total of 4 cutscenes, and 2 of those are the intro, I wanted a nice way to handle them. So, of course, I made an Archetype - arch_sys_cutscene.

This archetype counts down a cooldown value in Step, similar to the StageHandler counting down to pick a chesspiece to move, and displays from arrays of sprites and text, based on the current 'phase' value:

When the cooldown value does reach 0 and the phase variable iterates to the next, this also triggers the built-in GMS Alarm of the new phase - which makes it easy for me to add bespoke instructions for more or less every phase (the only limitation of being lazy like this, being GameMaker's 11 Alarm limit):

Much like how I have the StageHandler handle all the Draw Event stuff for the gameplay rooms, the drawing of Cutscenes and other UI stuff is handled in a UIHandler Archetype - so I can't have cutscenes be completely bespoke and wacky, but the upside is it became super easy to add or modify them with barely coding a thing, and they're all very consistent.

(The UIHandler also handles drawing the subtle fullscreen dithering effect you see in-game)



Log in with to leave a comment.


Neat! Key concept of programming, usually if it works it works.

Out of curiosity how long was total dev time?

Total time was about a week, I think? Minus time taken up by a fulltime job of course. I've started keeping bits of useful code in good old .txt files to reuse to speed things up