Dev Log Week 3: Beams, Mirrors, and Good Enough

Last week, I touched on how I had to make the player able to walk on the light beams they created, but I didn’t discuss the beam’s main mechanic – reflecting off mirrors and interacting with other objects it pointed at. I’ll be expanding on more specific objects as I talk about level design in the future, but we’ll focus on the mirror and some higher-level beam concepts for now.

The mirror is simple in concept, at least. Point light at it, and it reflects. But how do you know exactly where to stop the light beam when it’s hitting the mirror? What about if it’s hitting a mirror at all? Thanks to the tile-based collision I wrote instead of using default Gamemaker code, I also had to determine if the beam was hitting a tile-based wall myself. As I like to think about it, there’s a correct way and an easy way to do it, and I’ve drawn them out in this image below to help explain.

(As a side note, I use pen-and-paper a lot more than you’d expect for doing something as digital as video game design. To me, at least, things feel much more fluid and easy to change on paper or whiteboard than on the harsh glow of a screen. I should post some of my pages of notes. They make me look like a madman.)

The most precise and efficient way to check if a line has hit a tile is to check the transition points along the grid lines where changes are possible, as you see on the left. This has several advantages – no redundant checks are made, and the final point that hits a tile will be exactly on the border of the wall. It’s primary disadvantage is being hard to code, and after a few days of trying my hardest to get it right, I gave up and moved on to the easy version on the right. On the right, you simply check after a set distance along the line, and once you get a point that’s within a solid tile, you stop. Depending on how big the gap between checks is, this is either inefficient (taking hundreds of checks where 10 would suffice) or imprecise (going too far into blocks or possibly going through a corner that should have blocked it).

Ultimately, a bit of imprecision was okay for me to accept. For one, I actually wanted the line to go a bit into the tiles if I could help it. I was making the beam appear on screen with a simple draw_line_width() function, and that function ended the lines abruptly, at 90 degree angles. If the beam ended exactly at the line, then I would still need to push it farther in anyway to make it look right to the player. Secondly, I didn’t really care if the beam cut a corner occasionally. This is something I like to call a “beneficial bug” – it’s something that’s not intended, but it’s existence actually makes the game a bit less frustrating than if it was more harshly correct. It’s not hard to imagine the player trying to line up an awkwardly angled beam and getting frustrated at it being cut off on the sides.

(Left: the issue with skipping corners, illustrated and exaggerated a bit. I check more frequently than depicted. Right: How the beam would look if it was placed exactly on the edge of the wall, since it’s collision line is in the center. Ultimately, the pixel art style would mask the issue well enough, but it’s something I would have noticed.)

During this process, I decided to avoid looking for mirrors entirely. It’s highly likely that they could fit in-between each checking point, and Gamemaker gives a simple way to see if an object is along a line you already have – a function called collision_line(). The issue with collision_line(), though, is that it won’t tell you what the line hit FIRST. Maybe there were two mirrors in a line – how do you know which one you hit? You don’t. Where is the mirror in the level? No idea. I was able to solve this issue, though, with a common algorithm called Binary Search.

In a binary search, you effectively start in the middle, and become more precise as you move on, taking out a half of the line with each check. For example, I would start by checking if there was a mirror along the first half of the line. Let’s say I don’t find one. That means there are no mirrors along that first half, so I only need to check the second half. I’ll start in the middle of the second half (so the 3/4 point). Oh, this time I found a mirror! Cool. But I only know it’s somewhere between the 1/2 and 3/4 points. I need to keep looking, so let’s look at the halfway point between those, at 5/8. This continues until we find the pixel where the mirror should be stopping the beam, and can stop it there.

(My bad attempt at drawing out Binary Search)

Whew, that was a lot. Join me next week as I talk about my lighting system, and the week after that where I mention sound design, probably.

Leave a Reply

Your email address will not be published. Required fields are marked *