For our top-down 2D cowboy shooter, Project Spaghetti, we began to introduce a new enemy type, the alien, that behaves differently from the other enemies in two ways: it has a shield, and it fires a laser.
Our goal for the laser is to have a weapon that takes the player’s position, begins to charge, fires, then, after a brief pause, sweeps in a 90 degree arc in the direction the player has moved since the laser started to charge.
In last week’s episode of our game development show, we got started on the laser but were having trouble finding a solution that worked in GameMaker. Since then, Eric has tried a few variations out but has had no luck with any of them due to some limitations in the functions available.
1) Firing an “invisible” bullet that stops at collision, providing an endpoint for a laser “line”
This is what we worked with last episode on the assumption that, at the very least, we could get the appropriate end point for the laser, avoiding a lot of unnecessary collision checks. Unfortunately, there are two problems with this approach. The first problem is that GameMaker checks collision by whether or not sprites are overlapping (specifically, whether the collisions defined for sprites are overlapping), which means if a bullet moves too fast, it could pass entirely through a target sprite in the course of a frame, never registering a collision. In fact, the limit of the bullet speeds in the game is determined by this limit. For bullets it’s fine, because if the bullets moved any faster, the player would have trouble dodging them, but people expect lasers to be fast. If it crawls across the screen to its destination, it won’t look right, and things will only get worse when we reach our second problem: sweeping the laser. The laser needs to slice across the screen at a reasonable speed, so if it has to wait for an invisible bullet to collide every time it shifts, things will bog down fast.
2) Using GameMaker’s collision_line to fire out a ray and determine what the laser will hit
This approach showed promise, but Eric hit a wall due to the way collision_line is implemented. Again, there were two problems. The first was that the collision_line only returns the instance ID. Although we can use the instance ID to determine what kind of object is on the other end and how the laser should react, we can’t rely on it to give us where the laser graphic should stop, because the only coordinates we can pull out of the instance are for its sprite origin. Somewhat forgivable with the player or a crystal bumper (we could draw the laser under the sprite so it looks like it’s stopping in the right place, for example), but unforgivable for the border of the level. We ended up with a situation where all of the aliens were firing lasers at the same random spot in the middle of the screen – the spot that happened to be the sprite center of the level border. The second problem was that the collision_line doesn’t actually trigger a collision in the object it’s looking for, so even if we get the laser to look right, we can’t get the alien shields dropping, players dying, or the crystal splitting effect. So much for collision_line.
3) Checking every point from the start of the laser along the way to the edge of the screen for collision
Although we wanted to avoid this approach, because it seemed wasteful to check every single pixel on the way to a destination, it looks like we’ll have to use some variation of it. The original approach we discovered before involves checking pixel by pixel until encountering a collision, but we’ll try to optimize it a bit. If we know the narrowest collision widths and heights of the sprites the laser will interact with, we can at least cut the number of checks to the minimum number of pixels in a target collision. Visually, this means the laser might hit the sprite in different locations based on where the collision finally hit, but we could fix that by working backwards from a collision detection until collision is no longer detected, finding the edge of the sprite collision, while avoiding all of those unnecessary pixel checks on the way. Whether this method works for us or not is something we’ll have to discover in the next episode!