Log In  

Cart #unh-3 | 2020-03-08 | Code ▽ | Embed ▽ | No License

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.

P#73671 2020-03-04 20:23 ( Edited 2020-03-08 09:10)


wow this is actually insane, good job

P#73676 2020-03-04 23:16

For those interested in the standalone version, you can download it for free on itch.

P#73687 2020-03-05 11:47
:: gate88

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!

P#73698 2020-03-05 19:35

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
        enemy_bullet(i, start, speed, sprd, rot, col, homing)

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)
    return {
        x = enemy_x,
        y = enemy_y,
        vel_x = speed * cos(a),
        vel_y = speed * sin(a),
        spr = col

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)

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.

P#73699 2020-03-05 19:41
:: gate88

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?

P#73700 2020-03-05 19:45

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
        player_grace = 3

    if player_grace == 0 then
        player_grace = 3
        player_lives -= 1
        if (player_lives <= 0) player_dead = true
        player_hit = true

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
            bullet.y <= enemy_y + 7 and
            bullet.x <= enemy_x + 9 and
            bullet.x >= enemy_x - 12
            enemy_hp -= player_damage
            del(player_bullets, bullet)
        -- cleanup
        if (bullet.y < -8) del(player_bullets, bullet)
P#73702 2020-03-05 19:52
:: gate88

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!

P#73703 2020-03-05 20:00

> 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.

P#73704 2020-03-05 20:02
:: gate88

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.

P#73705 2020-03-05 20:09

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

P#91564 2021-05-06 07:42

Whenever I go into the options I see this screen. Do you think you could fix this error/glitch? Thank you.

P#92541 2021-05-25 12:30

[Please log in to post a comment]

Follow Lexaloffle:        
Generated 2021-06-14 14:20:37 | 0.014s | Q:27