Categories
personal blog

Amaze Me Game Jam Post-Mortem for Neon Run


Table of Contents


Introduction

Back to TOC

We’re in the post end-game now.

The community votes are in and everyone’s just waiting on Yoyo’s official rankings and then the whole jam will be done and dusted. Since I didn’t enter to win, I don’t particularly care about waiting to see the “official winners”, so fuck it, we’ll do it live!

To set the scene, my entry was Neon Run, a neon-infused endless runner with online leaderboards, revolving around the concept of switching colours to pass through deadly beams with a dash of platforming and a hefty heaping of speed.


The Goal

Back to TOC

I entered Amaze Me with one goal in mind: To kick myself in the arse until I became at least semi-comfortable with shaders. I’ve avoided shaders throughout most of my time making games, always treating them as “dark magic” that might destroy me if I tried to harness them with my limited mana pool. But recently I’ve been caught dipping my toes into that whirlpool of maths and pixels and I really wanted to try to wrap my head around some more complex concepts.

The theme for the jam was announced as “neon” and it was the perfect excuse for me. I needed to learn some more shader stuff to be able to deal with lighting effectively in Alchementalist and here I was, being handed a theme that could very effectively use shader lighting. Having a “time period” (the jam was kinda funny, there was no official time period so previously made games were fine to submit, but I only had X amount of time before submissions were due) meant that I couldn’t drag my feet as I had been doing in Alchementalist and it would give me a solid chunk of time to devote to just shaders without feeling guilty that I wasn’t making progress anywhere else.

Goal acquired.


The Process

Starting Up

Back to TOC

Since I had a (relatively) simple goal, I wanted to scope effectively. This meant I for sure was not going to make the newest groundbreaking RPG for this jam, or submit a contextual art center-piece with multi-layered readings of meaning to be found. No, I was gonna make an endless runner dammit and it was going to have shaders. The main mechanic would be quick and easily implementable and I would use the extra time to figure out how to shader.

Initially I grabbed some old art I had sitting around for a test project that involved a player using enemy bullets as jump pads.

“Bullet Hop”

That’s the old prototype. I thought the graphical style would mesh well with the “neon” theme and I could get started coding pretty much straight away. So I stuck that dude into the game and started coding up the collision and movement physics.

Once I had a floor platform and some physics, it was time to start working on the endless part of the endless runner. The story behind the game was slowly coalescing in my head during the early coding. The Neon Run is a famous racetrack that runs through the skyscrapers of Old New New York. It’s a deadly race with “solid light” being used as the platforms. So I wanted the platforms to appear before you and disappear behind. I implemented that using the camera edges to set the floor spawning point and destruction point, with them playing an animation to “load in and out”.

Next I started working on implementing the colours system. It’s a pretty simple mechanic, so it wasn’t too hard to do. The colour values are stored in an array (with each colour being a macro that holds the hexadecimal format for that colour) and an additional global.colour_selected variable that holds what position in the array is being read.

The player then simply reads the colour and updates it’s image_blend to that colour. Quick and easy, just like I like it in jams.

Now it was time to work on beams. The beams themselves have two components: The base of the beam and the beam itself.

The bases can either rotate and have multiple beams or don’t rotate and have a single beam. The beams are created by the bases and get given their colour by the base when it spawns them. Here’s the code for the multi-beam base picking it’s colours for the beams it will spawn:

beams = [];
len = sprite_width/2;
for (var i=0;i<4;i++) {
	beam_col[i] = choose(RED,GREEN,BLUE);
}
if (global.difficulty > 1) {
	var _col_list = [RED,GREEN,BLUE];
	for (var i=0;i<array_length(_col_list);i++) {
		if (_col_list[i] == global.colour) {
			array_delete(_col_list,i,1);
		}
	}
	for (var i=0;i<4;i++) {
		beam_col[i] = _col_list[irandom(array_length(_col_list)-1)];
	}
}

We can see that initially, it simply chooses a random colour for each of its 4 beams, but if difficulty is greater than medium, it uses an array with all the colours, deletes the colour that matches the players current colour and picks a new colour from that array. This means that on higher difficulties, the beams being generated are guaranteed to be different to the colour the player currently is, adding a touch more difficulty.

Differences in Difficulty

Back to TOC

I tried to put in as many of these little differences between difficulty levels as possible as I knew that it would be hard to make the differences between difficulty levels meaningful in a game with simple mechanics like this.

Another place I managed to tweak the difficulty was the spawning of the actual obstacles themselves. I could’ve just gone random; made a roll between 0 and 100 and if it’s below a certain number an obstacle gets spawned (indeed there is one thing I did that with). But I wanted to make generation a little more orderly than that. This is what I ended up with:

// Floor y position change
var _roll = random(100);
if (_roll < 2) {
	level_prev = level;
	var _next_level = 0;
	if (global.difficulty <= 1) {
		_next_level = choose(CELLSIZE,-CELLSIZE);
	}
	else {
		_next_level = choose(CELLSIZE*2,CELLSIZE,-CELLSIZE,-CELLSIZE*2);
	}
	if (level+_next_level >= global.death_zone) {
		_next_level = -CELLSIZE;
	}
	if (level+_next_level <= ((360 div 2) div CELLSIZE) * CELLSIZE) {
		_next_level = CELLSIZE;
	}
	level += _next_level;
}

// Obstacle generation
if (cooldown <= 0) {
	var _roll = random(100);
	if (_roll-chance < 1) {
		_roll = random(100);
		if (no_floor > 0) {
			_roll = 0;
		}
		if (_roll <= 70) {
			instance_create_layer(next_spawn*CELLSIZE,level+32,"beams",choose(obj_beam_multi,obj_beam_single,obj_speed_boost));
			chance = 0;
			cooldown = global.obstacle_cooldown_max;
		}
		else {
			no_floor += global.current_speed div 2;
		}
	}
	else {
		chance += global.chance_inc_max;
	}
}
else {
	cooldown--;
}

// Curb floor gaps based on difficulty
if (global.difficulty <= 1) {
	no_floor = min(1,no_floor);
}

// Create floor if no floor gaps
if (no_floor <= 0) {
	var _inst = instance_create_layer(next_spawn*CELLSIZE,level,"floor",obj_floor);
	_inst.image_speed = 3*global.current_speed;
	array_push(floors,_inst);
}
// Otherwise skip floor piece and decrement no_floor
else {
	no_floor--;
}

That might be hard to grok at first, but I’ll break it down. To begin with we do a simple random roll to see if the y position (the level variable) of the track will go up or down. It’s got the same chance of doing so on all difficulties, but on higher difficulties the level can jump more than one block (meaning jumps have to be more precise).

Then there’s a few checks to make sure the level stays within certain vertical bounds on the screen to make sure it doesn’t drop too low or climb too high.

After that we come to the obstacle generation. The first check is for cooldown, this gets set differently depending on the difficulty but it basically determines how often obstacles will spawn, higher difficulties means a lower cooldown and therefore the more often it will attempt to spawn an obstacle. As long as cooldown is above 0, we never even try to spawn an obstacle, which lets us set a baseline for periods between obstacles where they are guaranteed not to spawn.

Then we roll a random number (_roll) and we subtract chance from it. chance as a variable is being used to guarantee an obstacle generates at a certain frequency. If _roll is greater or equal to 1, it means the obstacle spawn won’t happen. Whenever that occurs, we increase chance by a certain amount determined by the difficulty level. So each miss means that chance gets higher and therefore _roll-chance is going to be more likely to be lower than 1. At a certain point, _roll-chance is guaranteed to be lower than 1 because chance will be over 100, which means a spawn is guaranteed to happen. Because the amount chance increases by each missed spawn (global.chance_inc_max) is higher for higher difficulties, we can guarantee that spawns are more frequent in higher difficulties, regardless of how the random _roll turns out (but there is still variability in exactly when a spawn will happen).

After that, we’re doing a check to see what type of obstacle the spawn will be. If _roll is less than or equal to 70, we choose between a single beam, a multi-beam and a speed boost (I definitely could’ve added some more checks to be able to distinguish between these three, but I never got around to it) and then we set chance back to 0 (restarting the whole “guarantee an obstacle spawn thing”. Otherwise, we’re going to spawn a gap in the floor to force a jump. I probably should’ve moved chance = 0 into a place where it got reset whether an obstacle was spawned or a floor gap was made, but I didn’t remember to (and it does lead to some more tricky floor sections, so it makes the game more interesting, I guess).

If the gap is chosen, we add the current speed divided by 2 to a variable called no_floor. This determines how many floor sections will we skip. The faster the player is going, the more floor pieces will be skipped (bigger more precise jumps).

Finally, we reset no_floor to a max of 1 if difficulty is medium or lower and then we start trying to create floors. If no_floor is 0, a floor piece is created. Otherwise, we decrement 1 from no_floor. There’s a check I didn’t include above all this code that makes sure this whole spawning system only works when the right edge of the camera enters a new section of CELLSIZE width (16 pixels). This means that it only triggers every 16 pixels and so floors don’t stack up on top of each other or whatever.

I think the above is the most interesting places where difficulty changes things, but here’s the final places where difficulty differences are implemented:

  • The number of hearts the player gets.
  • The max speed the player can reach (higher is harder as you have less time to react).
  • How many obstacles need to be cleared before a lost heart piece gets regenerated.
  • The coyote time the player has when jumping again as they are about to land from a previous jump (lower difficulties let the player be less precise with the timing of the jump if they are still a few frames away from landing).
  • The coyote time the player has to jump as they fall off a platform (lower difficulties means more frames where jump still works as they fall off a platform).
  • The coyote time a player has when hitting obstacles as they switch to the correct colour (lower difficulties means the game waits a longer period after hitting an obstacle before declaring that obstacle as hit, if the player switches to the right colour during this time the hit doesn’t count).
  • The amount of obstacles that need to be cleared before a combo starts up.

All of these let me subtly increase difficulty in a myriad of ways which felt satisfying and required more or less twitch reflexes from the player. I think it came together quite effectively.

So now I had the basic game functioning. It took me a day or two to get to this point and I was starting to panic about the shader stuff, so I switched over to research mode and started looking into how I would do the neon glow effect.

Throwing Shade

Back to TOC

To begin with these are the sources, at least those that I remember, I found most useful (after much digging through google and stack overflow and other sites like that) in no particular order:

Of course, to begin with reading that felt like reading ancient martian, but I tried. And I tried. And I tried. As I read more and experimented, I slowly started to make sense of what was going on. There were basically three steps. Render to a surface only the bright lights from the scene (let’s call this the Bright Surface). Here’s the code for that (remember, I am new to this so all the provided code is in no way guaranteed to be performant or optimal):

//
// Simple passthrough fragment shader
//
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

void main()
{
	vec4 tex = texture2D( gm_BaseTexture, v_vTexcoord );
	vec4 col = tex * v_vColour;
	vec4 brightcol = vec4(0.0);
	float brightness = dot(col.rgb,vec3(0.5126, 0.7152, 0.5722));
	if (brightness > 1.0)
		brightcol = vec4(col.rgb,1.0);
	else
		brightcol = vec4(0.0,0.0,0.0,0.0);
    gl_FragColor = brightcol;
}

This pulls out colours above a certain threshold (using the dot product) and draws them, otherwise it simply draws a transparent black colour).

Then we create two new surfaces, render the Bright Surface onto one of them and then ping pong between those two new surfaces blurring horizontally on the ping and vertically on the pong (we’ll call these the Ping Pong Surfaces). Each time I ping or pong, I draw the previous surface to the new surface, so the blurs get combined. The blur is essentially taking points from the texture around the current pixel and adding their colour to the current pixel with some weighting involved. Here’s the code for that:

//
// Simple passthrough fragment shader
//
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform vec2 tex_size;
uniform float horizontal;
uniform float pulse;
uniform float alpha_multi;

void main()
{
	//int num = 5;
	float weight[7];
	weight[0] = 0.227027;
	weight[1] = 0.1945946;
	weight[2] = 0.1216216;
	weight[3] = 0.054054;
	weight[4] = 0.016216;
	weight[5] = 0.0016216;
	weight[6] = 0.0001256;
	
	vec2 tex = 1.0 / tex_size;
	vec4 col = texture2D( gm_BaseTexture, v_vTexcoord ) * (weight[0] * pulse);
	if (horizontal < 1.0) {
		for (int i = 1; i < 7; ++i) {
			vec4 tex1 = texture2D( gm_BaseTexture, v_vTexcoord + vec2(tex.x * float(i), 0.0));
			vec4 tex2 = texture2D( gm_BaseTexture, v_vTexcoord - vec2(tex.x * float(i), 0.0));
			col += tex1 * (weight[i] * pulse) * alpha_multi;
			col += tex2 * (weight[i] * pulse) * alpha_multi;
			col.a *= 2.;
		}
	}
	else {
		for (int i = 1; i < 7; ++i) {
			vec4 tex1 = texture2D( gm_BaseTexture, v_vTexcoord + vec2(0.0, tex.y * float(i)));
			vec4 tex2 = texture2D( gm_BaseTexture, v_vTexcoord - vec2(0.0, tex.y * float(i)));
			col += tex1 * (weight[i] * pulse) * alpha_multi;
			col += tex2 * (weight[i] * pulse) * alpha_multi;
			col.a *= 2.;
		}
	}
	
    gl_FragColor = col;
}

You can see I’m supplying a few arguments (the uniforms at the top). These are both necessary for the shader, but also give me the ability to add graphical settings to the Options menu, letting the player decide how vibrant, “pulsey”, etc, the shader is. You can also see the horizontal uniform, which gets flipped alternately as I’m flipping between the Ping Pong Surfaces and controls whether I’m blurring horizontally or vertically.

Finally, I create one more surface and using a third shader I draw the application surface, combining it with the last drawn Ping Pong Surface in the process, to this third surface using this code:

//
// Simple passthrough fragment shader
//
varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform sampler2D blur_tex;
uniform float exposure;
uniform float gamma;

void main()
{
    vec4 baseColor = texture2D(gm_BaseTexture, v_vTexcoord);
    vec4 bloomColor = texture2D(blur_tex, v_vTexcoord);
	baseColor += bloomColor; // additive blending
    // tone mapping
		vec4 result = vec4(1.0) - exp(-baseColor * exposure);
    // also gamma correct while we're at it       
    result = pow(result, vec4(1.0 / gamma));
    gl_FragColor = result;
}

Some of the code from all of the above shaders is taken from various bits around the internet, which I then edited to suit my needs. Most of them I can explain pretty well what is going on, but that last one is still a bit of black box. As far as I can tell, it’s adding the pixels from the application surface and the Ping Pong surface together, then it’s manipulating the result to increase brightness and finally it’s setting a baseline brightness for the result. I understand what exposure and gamma are but explaining why the maths is that way is beyond me.

Then I draw this third surface over the application surface in the Draw End event. Here’s the code for the actual GML side of things doing all the work with the surfaces:

var sw = camera_get_view_width(CAM);
var sh = camera_get_view_height(CAM);
var cx = camera_get_view_x(CAM);
var cy = camera_get_view_y(CAM);
var xscale = sw/1920;
var yscale = sh/1080;

if (!surface_exists(threshold_surf)) {
	threshold_surf = surface_create(sw,sh);
}

surface_set_target(threshold_surf);
	draw_clear_alpha(c_black,0);
	if (global.shaders_on_setting) {
		shader_set(shd_threshold);
	}
	draw_surface_ext(application_surface,0,0,xscale,yscale,0,c_white,1);
	shader_reset();
surface_reset_target();

if (!surface_exists(ping_pong_surf[0])) {
	ping_pong_surf[0] = surface_create(sw,sh);
}
if (!surface_exists(ping_pong_surf[1])) {
	ping_pong_surf[1] = surface_create(sw,sh);
}
	
for (var i=0;i<2;i++) {
	surface_set_target(ping_pong_surf[i]);
		draw_clear_alpha(c_black,0);
	surface_reset_target();
}

var first_iter = true;
for (var i=0;i<blur*global.blur_setting;i++) {
	var horizontal = i mod 2;
	surface_set_target(ping_pong_surf[horizontal]);
		if (global.shaders_on_setting) {
			shader_set(shd_blur);
			shader_set_uniform_f(u_tex_size,sw,sh);
			shader_set_uniform_f(u_hor,horizontal);
			shader_set_uniform_f(u_pulse,pulse);
			shader_set_uniform_f(u_alpha_multi,alpha_multi);
		}
		if (!first_iter) {
			draw_surface(ping_pong_surf[!horizontal],0,0);
		}
		else {
			draw_surface(threshold_surf,0,0);
			first_iter = false;
		}
		shader_reset();
	surface_reset_target();
}

if (!surface_exists(final_surf)) {
	final_surf = surface_create(sw,sh);
}

surface_set_target(final_surf);
	draw_clear_alpha(c_black,0);
	if (global.shaders_on_setting) {
		shader_set(shd_combine);
		var blur_tex = surface_get_texture(ping_pong_surf[horizontal]);
		texture_set_stage(sample_blur,blur_tex);
		shader_set_uniform_f(u_exp,global.exposure_setting);
		shader_set_uniform_f(u_gamma,global.gamma_setting);
	}
	draw_surface_ext(application_surface,0,0,xscale,yscale,0,c_white,1);
	shader_reset();
	if (instance_exists(obj_player)) {
		if (player_hit) {
			if (player_blink) {
				if (!instance_exists(obj_tutorial) || (obj_tutorial.show == false)) {
					shader_set(shd_red);
					with (obj_player) {
						draw_sprite_ext(sprite_index,image_index,floor(x-cx),floor(y-cy),image_xscale,image_yscale,image_angle,c_white,image_alpha);
					}
					shader_reset();
				}
			}
		}
		if (!instance_exists(obj_tutorial) || (obj_tutorial.show == false)) {
			with (obj_player) {
				draw_sprite_ext(spr_player_outline,image_index,floor(x-cx),floor(y-cy),image_xscale,image_yscale,image_angle,c_black,image_alpha);
			}
		}
	}
surface_reset_target();
draw_surface(final_surf,cx,cy);

Once again, I’m not saying this stuff is optimal, but I got it working and then I got cold feet when it came to trying to optimise stuff because I didn’t have a lot of time left and a lot of the “fixing” of stuff I was doing was trial and error, so yeah.

One note I will make is that if I were to do it again, I’d probably end up using a parent object for the things I wanted to glow and only draw to the Bright Surface from those. Since I’m literally using the application surface to pull brightness from in the first shader, it meant I couldn’t have “bright” lights that didn’t glow. As I was making more and more graphics, this definitely became a little painful and limited the amount of stuff I could draw. I was also forced to manually draw some things after the shader work had been done (such as the player outline, or the red blinking shader I made for damage), otherwise that stuff glowed as well and became unreadable with all the other glow around it.

Ok shader stuff is done. Great, things were really coming up Milhouse!

Mr. Sheen

Back to TOC

The deadline was looming in a few days, so I started moving on to polish. The first thing to go was the player sprite I was using. I wanted the Neon Run to be done with “laserbikes”, not actual running, so I made a little dude who was riding a bike and slotted him in.

Mah dude riding

This really started to coalesce the theme and I quickly got to work making some background buildings and signs to spice up the world, as well as making a dithered background (which the youtube compression algorithm hates) that looked like a nice night sky.

The game with shaders turned off
The game with shaders on in the middle of a particularly bright neon pulse

The game was finally starting to have some real atmosphere. At some point around here (these images are post-completion though, so they don’t totally represent the state of the game at the time), I started to add all the little polish points, like particles when the player is moving, the particles of the beams, different animations for dying and jumping and stuff, all the stuff that makes a game come alive.

Now is when I started work on the title screen as well. I draw a scaled up bike with my limited pixel skills, added a neon road, threw particles everywhere and added in the the buttons.

Here I went back and started working on the online leaderboards. I’ve done some work with them in Spaceslingers before, so this wasn’t completely new territory like shaders, but I’m still an amateur when it comes to implementing them. I used a few new techniques I had learnt and after a bit of trial and error I had the leaderboards working. It probably took around a day. If anyone wants, I can go into more detail in another blog post about how I implement them, but this bad boy is already becoming a behemoth of a post, so I’ll leave it alone for now.

I then moved onto the Options screen and started implementing all the different settings for the shaders, the volume controls, difficulty settings and keymapping. This took awhile and plain GUI work like this is very easily my most hated part of gamedev. But after a period of time I had the functionality I wanted from it. I probably had around a day or two left at this point and I was feeling pretty good. Mostly everything was implemented and I wasn’t feeling the pressure too much. So I spent some time looking around for music and found some nice CC0 tracks that suited. Then I spent a few hours twiddling around in SFXR and merging and “effecting” the SFXR tracks in Audacity to create the sound effects for the game. As a musician in another life, this is always another fun part of game dev for me. It’s satisfying to tweak and blend odd sounds into the noises I’m imagining in my head.

After implementing all of that, it was finally time to do the tutorials. I’m a wordy motherfucker, as you might’ve noticed from this blog post, so I always tend to overwrite explanations. The Neon Run tutorials definitely suffered from this and it’s something I need to work on. But I covered the colour system and beam dodging, the speed boosts, the platforming and finally the combo system. As usual, I had to stick a bunch of ugly checks into otherwise existing code to accomodate what the tutorial needed when it was running, but it didn’t take too long to do. And that was that, I was finally “finished” with Neon Run. I had a little bit of time to spare, so I went around adding random things to the project and tightening some stuff up, but I didn’t have enough time to do eeeeverything I wanted. Still, the game was as I imagined it, so I was pretty happy, it was time to upload.


Player Feedback

Back to TOC

I uploaded the game and waited. There’s always a funny period after uploading any sort of work to the internet. To you, it’s the most important thing in your life at the time (within reason, of course), but to everyone else, it’s just another piece of content to be ignored or glossed over as easily as you gloss over the latest tweet from Justin Bieber. However, it didn’t take too long for me to get my first few ratings, and I was pretty happy with the comments. People had some uplifting praise and some legitimate criticism, and I was happy for both. Then I thought of a cool addition to the game. I wanted to add the ability to look at the overall leaderboard from the Title screen and I wanted the player to be able to check their stats from the same place.

You see, when the player completes a run, their score gets uploaded and they can check out where they ranked. The game also keeps score of a bunch of different little things (like how many obstacles you hit, how many times you jumped, etc, just little stats). It displays all these alongside the high score. But what if the player wanted to just see these stats, or just see how they ranked without having to fail a run quickly to do it? I wanted to add that in. Submissions were still open at this time, so I booted up GMS and started work.

The Mistake

Back to TOC

Here’s where I made a critical error. I don’t update GMS when I’m in the middle of a project, but a new beta version (I’m always running as a guinea pig for the beta) had come out during the time I was working on Neon Run. I ignored it as usual and went through the game, adding in the leaderboards and stats screen, tested them and then saved the project. It was getting late and I still had a good amount of time before submissions ended, so I went to bed, resolving to compile and upload the next day.

The next day came and I booted up GMS, the “new update” screen flashed and I updated without thinking. I then compiled the game and uploaded the new version, overwriting the old one.

The ending for submissions came and went without me getting anymore comments and I was slowly diving back into Alchementalist. Then I got a new rating and comment that mentioned a game crash. I had thoroughly tested the game (alongside some family and friends) and I had never encountered a crash, so I was a bit nervous, but I figured it was just a one off thing and left a comment saying something along the lines of “that’s weird, I’ll look into it, but it runs fine for me and my testers”. So I started looking into what could be the cause of the crash. I ran the game, completed a run and the game crashed.

What the Hell…

Back to TOC

I did it again, it immediately crashed. Oh no, something was very wrong. The error code it was giving me was pretty useless, simply pointing to an array not being declared. The array were declared. I knew it was, I could see the code, and I had tested everything so many, many times. Why was it just screwing up now?!

So I started breaking down the code around the error. I had a struct containing the players data which was used to store settings and also keep track of all their stats for each different difficulty level. So the order of data nesting was Struct (overall settings) > Array (corresponding to each difficulty) > Variables (storing the specific stats for that array position). For some reason, the array in that order had decided to shit it’s pants and act as though it didn’t exist when it was being read.

I was using a global variable called global.difficulty to tell the game what difficulty level was selected and it held a value between 0 and 3. The difficulty array in the settings struct had positions from 0 to 3, with the relevant data inside. I put a breakpoint before the crash and ran the game. Since I was running in YYC, GMS told me I’d have to switch to the VM to debug as it always does, I confirmed and ran the game. No bug appeared? Wait what was going on? I ran the game again this time not in debugger mode and the game crashed…Ok something funny was happening…

What could I do to fix this? I started toying with variables. To begin with, I set an instance variable that just held the value 0 and used that to read the position in the array. It still crashed. Then I moved onto using a local variable for the same thing. The damn game worked. Don’t ask me why I changed from an instance to a local variable, I was trying a bunch of different things all over the place and that just happened to be one of them. So why would a local variable holding 0 work, while an instance variable or a global variable holding 0 (and confirmed to be holding 0 whenever I ran in debugger mode) wouldn’t work but ONLY when running in YYC? I was getting really frustrated at this point. So I set it back to the global variable, changed the build compiler back to VM and it worked fine. I switched to YYC and it crashed. Goddamn it, I think there’s a bug in GMS.

So I went through and edited all the code involving reading from the arrays and instead of directly using the global.difficulty variable, I instead set a local variable to global.difficulty and used the local variable for the array position. I ran the game in YYC and it worked…

I sent off a project to Yoyo that held only the minimum amount of code required to showcase the bug and went back to brooding.

My gosh, I had broken my game after updating and hadn’t tested it. Not only that, but I could see ratings without comments coming in during this time period, so a good amount of the ratings I got came during a time when the game crashed after every run without even getting to submit scores, and I was pretty sure it was the fault of a bug in GMS, not my code…I couldn’t have been more frustrated.

I uploaded a new build, but felt weird about it because it was after submission and I didn’t yet have confirmation that it was indeed a bug in GMS OR that uploading new builds after submission was ok. I ended up leaving my original build up alongside the new build and simply left a comment on my page explaining what was going on. Frustrated, tired and feeling pretty defeated, I went to bed.

Of course, I’d actually left a second instance of the same bug in there unfixed (I hadn’t updated the code to a local variable when the player checks their stats from the Title Screen), but I didn’t know about that till later.

As is the wont of the internet, a lot of people ending up downloading the old buggy version despite the fact I’d uploaded a new one without even realising they were doing so, which was a really frustrating experience. In hindsight, with the lax rules of the jam, I should’ve just completed removed the buggy version, but I didn’t do it until the ratings period ended because again, it felt a bit weird. I didn’t want to give myself an unfair advantage over other people who were sticking to the submission period.

I got confirmation from Yoyo that it was indeed a bug. I found the second instance of the game crashing bug by watching Evanski play and break my game in multiple ways (which was hilarious even if there was a little ego-death milling around in there). I uploaded a new version fixing it. All the while I was kicking myself for such a stupid error. Why no test? Ah well, there’s no use crying over spilt milk. There was nothing I could do about it.


Overall Feelings

Back to TOC

Despite the weirdness surrounding the rules, the GMS bug breaking my game and the feeling of helpless frustration I had experienced during the ending of the jam, I still had a lot of fun and I while I’m definitely not an expert on shaders, I now know enough to understand the general idea behind what chunks of shader code are doing and I can usually edit them or formulate some of my own well enough to be able to get what I need done. So goal achieved I guess.

Beyond that, I was (and am) proud of Neon Run. It’s a fun game built in a short period of time and I’d managed to add enough polish to really make it feel like an actual game, instead of just a quick jam entry. Of course, plenty of other jam submissions overshadowed mine but I was pretty excited for the quality of work that people had managed to put out. There’s no point in comparing yourself to others when it comes to this sort of thing; each person is at a different point in their development and you wouldn’t make fun of a high school runner for not being able to beat Usain Bolt, so why beat yourself up by comparison to others who are further along their game development journey than you are?

If you managed to make it to the very end of this gargantuan post-mortem, it’d be awesome to see what sort of high score you can get when it comes to Neon Run. You can find it here on itch.io.

Until my next word-vomit, thanks for reading =)

Advertisement

By RefresherTowel

I'm a solo indie game developer, based in Innisfail, Australia.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s