Ludum Dare can be one of my favorite times of year. I rarely participate anymore, but on and off for almost a decade now, I've spent the weekends bettering myself. Initially, I used little more than Game Maker, which still makes a "real" project, but didn't teach me much in the way of coding. With that said, I was also 13 years old. I'll give myself a pass on that one.
This time around, I wasn't seriously participating. I can't even remember what the full theme is; something about staying alive. The competition, this weekend at least, is purely an excuse for me to try my hands at something that I've wanted to for a long time: making a bullet hell game.
Touhou is a video game series about dodging bullets. Lots of them. Like, literally hundreds at a time.
I like the games a lot, and have always wanted to try my hand at making one. And although I can't get anywhere near the level of complexity that those games have, nor the artwork or music, I can certainly replicate it on the most basic level.
To get started, I decided to look on this website itself for any resources on working with Löve 2D, and I found a lovely guide @jeansberg . It did a good job of walking me through the basics of using the library to make a shooting game. The game he makes is quite different from the one I made, with his being more of a horizontal endless enemy fight and mine being a vertical fight against a singular boss, so I only followed it up to the point that made sense, somewhere in page 3. I'd recommend checking out the series here, it's a good read, and I'm not going to retread over the basic things that I added to the game like movement or collisions; it's all there in that series. In this post, I'll be focusing on the boss, and the bullets that the boss spawns.
First of all, in touhou games, bosses have multiple phases where they attack differently, so I need to make a variable and a function to keep track of which stage of the fight I'm on.
function bossBehavior(dt)
if (bossCycle == 0) then
bossPattern1(dt)
elseif (bossCycle == 1) then
bossPattern2(dt)
elseif (bossCycle == 2) then
bossPattern3(dt)
elseif (bossCycle == 3) then
bossPattern4(dt)
elseif (bossCycle == 4) then
bossPattern5(dt)
end
updateBullets(dt)
end
Most of this is self explanatory, but if you skipped the other tutorial, dt is a variable passed through the update function which (according to the wiki) "represents the amount of time which has passed since it was last called", or essentially just represents the current frame. This will be very important for updating the gaps between when bullets are fired and their movement. Now, let's say that we're inside of bossPattern3 at the moment.
function bossPattern4(dt)
if attack4Delay then
for index = 1, 16, 1 do
bullet = {xPos = boss.xPos, yPos = boss.yPos, width = 16, height=16, 100, img = bossBullet, behavior = sixteenWayBullet, number = index + attack4Spin, bulletSpin = 0}
table.insert(bossBullets, bullet)
end
attack4Delay = false
attack4Timer = attack4TimerMax
attack4Spin = attack4Spin + 0.1
end
if attack4Timer > 0 then
attack4Timer = attack4Timer - dt
else
attack4Delay = true
end
end
Each pattern has a delay, which is how long it will take between attacks, a timer, which keeps track of whether the delay has run its full course numerically, and a boolean storing whether that numeric value is high enough to shoot again. If it finds that it's time to shoot again, it begins the for loop featured above.
In the case of this bullet pattern, it creates 16 bullets at the position of the boss. They have various properties which are mostly self explanatory, but behavior keeps track of which function will be called each frame to tell the bullet how to move. In this case, the function telling them how to move is 16 way spin bullet.
function sixteenWayBullet(dt, index, bullet)
bullet.yPos = bullet.yPos + dt * 100 * math.sin(math.abs(math.rad(bullet.number * 22.5)))
bullet.xPos = bullet.xPos - dt * 100 * math.cos(math.abs(math.rad(bullet.number * 22.5)))
if ((bullet.yPos < 0) or (bullet.yPos > love.graphics.getHeight())) then
table.remove(bossBullets, index)
end
end
Inside this function, some basic trigonometry is being used to find an angle for each bullet to shoot out an equidistant angle from one another. In other words, 22.5 * 16 is 360, so this means that the bullets will each be equally spaced and shoot out in a circle. Their speed is being calculated using a combination of some arbitrary constants, the dt variable, and some sin and consine functions, using multiples of 22.5.
The last bit removes the bullets if they leave the screen, preventing the program from lagging due to too many bullets.
However, what makes this pattern interesting is this snippet from the bossPattern4 function.
if attack4Delay then
for index = 1, 16, 1 do
bullet = {xPos = boss.xPos, yPos = boss.yPos, width = 16, height=16, 100, img = bossBullet, behavior = sixteenWayBullet, number = index + attack4Spin, bulletSpin = 0}
table.insert(bossBullets, bullet)
end
attack4Delay = false
attack4Timer = attack4TimerMax
attack4Spin = attack4Spin + 0.1
end
Notice how there's that variable attack4Spin. What it does is help increment the number attribute of bullet, which is then used to calculate the angle it will emerge at. As it gets incremented, the next bullet gets shifted just slightly, ultimately culminating in a pattern that looks like this:
It's rough as hell, but it actually looks like an incredibly basic pattern from a bullet hell game (or one component of a much cooler pattern). It also mostly uses placeholder assets from @jeansberg (once again, go check out his tutorial if any of this seems interesting, it's a great starting point).
Tomorrow, I'm going to add a little more functionality to it, such as dying when you run out of your three lives displayed in the top left, having a win state, and maybe having a visible hitbox when you focus (focusing is one of a few very small additions that I made to this which I didn't feel the need to include, the boss behavior feels more than enough).
I've never come close to winning Ludum Dare before, and I sure won't tomorrow, but the weekend truly is a gem.
Top comments (0)