A Touhou fangame demake! As far as I know this is the first Touhou demake on the BBS, so I'm claiming that trophy :)
Use X for shoot / accept and Z for bomb / cancel and arrow keys for movement.
This demake has the following features:
- 2 playable characters: Reimu Hakurai and Marisa Kirisame.
- 4 enemies with a total of 16 spellcards.
- 5 unique music tracks, which can be listened to in the Music Room.
- A rudimentary score system.
- A practice system, where each boss can be practiced against.
And the following mechanics:
- Slow, your character slows down whilst shooting. How much you slow down depends on your selected character (33% speed for Reimu, 50% for Marisa).
- Bomb, you start with 3 bombs which can be used to clear the screen. For each enemy you defeat, you get one bomb.
- Lives, when you get hit, you'll lose a life. When you lose all lives, you game over. Lives cannot be replenished, so don't forget to use your bombs! The amount of lives depends on the difficulty chosen.
There's probably plenty of bugs in there (I haven't actually tested highscore scores, it probably possible to overflow it), given how I spent roughly a year developing it (on-and-off). The code is a mess and uses so many global variables. But, it is finished and that's what counts :)
I will probably release the project files in the future, but I want to write a proper explanation when I do.
For those interested in the standalone version, you can download it for free on itch.
Cool implementation and pretty good performance for so many bullets!
In browser I get little hitches when tons of bullets are being created (and likely destroyed off screen), which I assume is Lua garbage collection running. Are you using any kind of object pool for bullets to reuse objects? If not, that might help with the hitching. It works pretty well regardless, and I'm very interested how you're allocating and updating all those bullets!
The bullet code is relatively simple, actually.
This snippet manages the bullet creation, which just inserts a new bullet into a table where I store all enemy bullets.
for i = 0, n do add( enemy_bullets, enemy_bullet(i, start, speed, sprd, rot, col, homing) ) end |
Where n is a parameter which indicates how many bullets to spawn.
The enemy_bullet() function creates an object with pretty basic properties.
function enemy_bullet(i, start, speed, sprd, rot, col, homing) local a = start + (sprd * i) + (rot * enemy_itr) if homing then a = start + atan2(player_x - enemy_x, player_y - enemy_y) + (sprd * i) end return { x = enemy_x, y = enemy_y, vel_x = speed * cos(a), vel_y = speed * sin(a), spr = col } end |
And the update script which manages their position and deletion:
function bullets_enemy() for bullet in all(enemy_bullets) do bullet.x += bullet.vel_x bullet.y += bullet.vel_y -- check out of bounds if (bullet.x <= -7 or bullet.x >= 127) del(enemy_bullets, bullet) if (bullet.y <= -7 or bullet.y >= 127) del(enemy_bullets, bullet) end end |
As you can see it's relatively "simple" code, but some patterns do spawn a lot of bullets, so that might be why it lags sometimes.
For my game Heat Death, I detected player to bullet/enemy collisions with pixel screen tests, because drawing + a few pixel tests is a much faster than math in pico8. I assume you're doing something similar here?
Player collision and enemy collision use two different functions.
The player collision is the simplest, as that one scales the hardest (there's many bullets after all). I tried the naive way, which is checking all bullet positions against the player, but that didn't work for obvious reasons :)
Instead I opted for pget(), which did the job correctly, with exception that it sometimes would randomly produce false-positives. In order to tackle that, I added a grace counter, which works quite well:
function player_collision() local c = pget(player_x + 2 + player_spr, player_y + 4) if c ~= 11 then player_grace -= 1 else player_grace = 3 end if player_grace == 0 then player_grace = 3 player_lives -= 1 if (player_lives <= 0) player_dead = true player_hit = true end end |
Enemy collision is handled by the bullet the player fires and uses the naive method. Because the player only fires upwards and at a constant speed, there's a fixed limit how many bullets the player can spawn. I also don't have to check the upper border of the enemy, as the player can't shoot downwards. I did have to check the sides, in case the enemy moved to the bullet.
function bullets_player() for bullet in all (player_bullets) do bullet.x += 2 * cos(0.25) bullet.y += 2 * sin(0.25) -- collision if bullet.y <= enemy_y + 7 and bullet.x <= enemy_x + 9 and bullet.x >= enemy_x - 12 then enemy_hp -= player_damage del(player_bullets, bullet) audio_sfx(5) end -- cleanup if (bullet.y < -8) del(player_bullets, bullet) end end |
Yeah, very similar to my game; I use pget checks for enemies colliding with players.
But since player bullets need to get destroyed when colliding with enemies, I use sort of a fake (sparse) screen that I draw to and keep track of the owner of each pixel for player bullet to enemy collisions, that way the bullets can be destroyed on collision. This fake screen is recalculated every time a collision happens, because bullets could mask other bullets since it's only one owner per pixel.
I do think if instead of deleting bullets you moved them to a separate queue, and then checked the queue and reused them instead of creating new bullet objects all the time, that might alleviate some of the garbage collection pressure in your game. It probably will add complexity and tokens though, so it's a tradeoff (like everything in pico8).
I haven't tried the standalone game, but I'd bet it probably performs fine. So totally not necessary, just something to think about for the next game!
> I do think if instead of deleting bullets you moved them to a separate queue, and then checked the queue and reused them instead of creating new bullet objects all the time, that might alleviate some of the garbage collection pressure in your game.
That's actually a great suggestion, I never even thought about doing something like that! Currently the game only uses roughly 50% of the tokens, so I definitely have enough tokens to try that out in the next game.
There can be performance implications on how the queue is implemented too (if you're constantly resizing or moving things in an array, for example, that might be a significant CPU load) so it might take some finesse to get right. Might need to play around with different queue sizes too to find the sweet spot.
ooh good game! my biggest complaint is the UI: if you're holding the button to shoot, the "try again" / "game over" screens stay up for just a couple frames at most. then you fire through the name entry screen before even realizing what's happened!
i've fixed this in a local copy of the game
and then went and extended the maximum "grace time" from 3 to 6 so i can actually have a chance to progress lol
Whenever I go into the options I see this screen. Do you think you could fix this error/glitch? Thank you.
Awesome game, but i suck at touhou, so i cant get past meiling
Edit: 1 year of practice, and now I can get to patchouli and beat Mokou! Still can’t beat patchouli though.
[Please log in to post a comment]