Log In  

Hey guys, I'm stuck on trying to make an endless runner with procedurally generated platforms.
I was originally going to try and use the map sheet to make different 'rooms' and then try to cycle through them and draw the 'next room' randomly, but decided to abandon that idea in favor of just procedurally generating the platforms. (someone please let me know if that's a better way to do this type of game)

If you try the game you'll see the issue - the tiles are generated all over (I was going off the LazyDevs roguelike tutorial and trying to modify it for my purpose), they aren't jumpable despite having the right collision flags, and for some inexplicable reason they fall as if they had gravity, which I just can't figure out.

I've been tearing my hair out and really need assistance. Any help at all, or a better method would be really appreciated! I basically want to generate the runner platforms per 8 map screens, and ideally be able to generate powerups, holes in the floor, eventually enemies/traps etc.

Thank you for your time.

Cart #kabopowabi-0 | 2021-12-29 | Code ▽ | Embed ▽ | No License
1

P#103744 2021-12-29 05:42

1

There's two major problems with your cart from what I can see, and those problems cause more problems throughout.

  1. You really shouldn't be making changes that are relevant to gameplay during a draw function unless you absolutely have to. For the map_gen() function especially, you should be calling that during _update() or _init() so that the number of times it gets called will be consistent and also so it will take effect at the correct time. I would also recommend not calling it on sections that the player can already see, but that's more out of preference.
  2. You need to pay more attention to which parts of the map you're using for each part of the game. map_gen() is setting tiles in the rectangle with y values of 16 to 48, but your gameplay takes places in 0 to 15. That's why the collisions aren't working. The reason the platforms are falling sometimes is because they're being placed in the portion for the stars.

Regarding other ways to generate platforms, I think something more based on the player abilities would be better. Judging by eye, it looks like the starting jump range is 6 tiles and the starting jump height is 4. You could probably add or subtract 2 tiles or so going u or down a row, with 3 being the base distance (since the jump is symmetrical). From there you could just generate 1 platform at a time, moving the position over each time using a random position among the ones that would fit the above characteristics. For variety you could also occasionally generate decorative platforms that are irrelevant or optional platforms that may or may not be better choices for next platform. A bit of testing or math could also find the amount the jump range increases as the range increases.

Regardless of method, I would recommend putting in some code that will let you adjust the rate at which the platforms are generated so that you can tweak it.

P#103747 2021-12-29 07:55

Hi @kimiyoribaka, thank you so much for your response. I've been working on it and came to the same conclusions re map_gen() in the init() function. How do you mean calling it on sections the player can already see? As in, it should already be generated for the current screen?

As for 2 - thank you, I finally got that part figured and adjusted accordingly.

This is what the cartridge looks like now. I'm curious as to how you would generate one platform at a time - currently I'm just running map_gen() 30 times in my init() function, but that seems inefficient? I'd like for it to generate a new 30 platforms everytime the scroll goes past the 8th screen, so it effectively can scroll forever. Also, how would you go about generating decorative or other types of platforms, use different sprites etc? Really appreciate your input.

Cart #sofudipiho-0 | 2021-12-29 | Code ▽ | Embed ▽ | No License

P#103750 2021-12-29 08:08 ( Edited 2021-12-29 08:09)

Yes, I did mean the current screen should already be generated. Generating the map in the _init() function already solves that.

Going more in-depth on the one-at-a-time idea, here's a way to do it (the first that comes to mind). You make variables platx and platy, then set them to the first x position where platforms that the player should be encouraged to jump on would be and y position of the floor. Using those, you then randomly roll an x and y position that is within comfortable jumping range of the player if the player were at platx and platy. If the values rolled are out of the gameplay range, roll again. Next, Make a platform at the x and y that was just rolled, then set platx and platy to the position at the end of the platform that was just generated. You can then repeat the process as many times as needed until the end of the stage.

By "decorative" I meant platforms that can't be reached by the player. The idea would be to make the trail of platforms look more natural and less like it's been generated. If you go with that idea, it could just mean sometimes adding a 2nd platform with a position that's completely random rather than being within the constraints of where the player could jump.

P#103753 2021-12-29 08:37

@kimiyoribaka interesting, I like that idea. Would definitely bring some challenge to the game, I'll give it a shot.

Any advice on how to keep generating the level after resetting the player to screen one again? I'm currently basically resetting the position to start after hitting the rightmost edge. I'd like for the game to just continue infinitely until death.

P#103754 2021-12-29 08:42

There's a couple ways. I think you should still try to use the map for the platforms, simply because you've already got the system set up for that. However, you will need some way to reuse the map with different platforms. Here's what I suggest: choose a positions around 2/3 or 3/4 of the way through. Then when the player reaches that point, copy 2nd half of the map onto the section where the first half of the map was and move the player and any other object that isn't part of the map 64*8 pixels to the left (including the camera, and platx and platy). If done carefully, that should give the illusion that the player is in the same spot they were before, since everything else the player can see will be identical. Then you can clear out the platforms from the second half of the map and replace them with newly generated ones.

This technique is used in modern games too btw. That's actually how Half-life 2 and some areas of Portal 2 appear seamless despite the world being way too big to fit in memory all at once.

P#103755 2021-12-29 09:05

@kimiyoribaka okay, cool. That makes sense to me intuitively but I'll have to figure out how to make it work haha. Thank you so much for your help!

P#103756 2021-12-29 09:40

hey @kimiyoribaka I had some a few more questions if you have the time -

Cart #zgugoziti-0 | 2021-12-30 | Code ▽ | Embed ▽ | No License

I'm still working on the map generation using the midpoint method, hopefully that'll be the final touch on this.

I can't seem to figure out why my collisions don't work on the up-down enemy. I have 2 enemies, up-down and side-side (currently static), and the side-side guy kills ya but the up-down guy doesn't, despite them both having the same flags and all.

Secondly I've generated the side-side guys in the map_gen() so that they can be placed on the platforms, but as a result I can't animate their x-positions this way to make them move side to side. Do you have any advice on how to do this properly?

I want to add coins for extra score and small speed boost, as well as maybe having 3 lives before the final death. Any advice for these mechanics would be appreciated as well.

Thanks!

P#103828 2021-12-30 17:49 ( Edited 2021-12-30 17:56)

I'm also not sure how to copy the 2nd half of the map back to the 1st half.. as well as then call map_gen() for just the second half of the map again, if you can provide any pointers

Here's what I have so far:

Cart #moyaziyeta-0 | 2021-12-30 | Code ▽ | Embed ▽ | No License

P#103838 2021-12-30 20:05 ( Edited 2021-12-30 20:06)

The reason the up-down enemy doesn't kill the player is because your code doesn't check for a collision with it. The enemy_collide() function wouldn't do anything, because it's setting some local variables and not using them. Beyond that, your map_collide() function only checks for collisions with static objects that have been embedded in the map. Your up-down enemy appears to be implemented by created a table of its values and drawing that.

To clarify, since I can't tell if you would already know this based on your posts, mget() doesn't check flags for things that appear on screen. It only checks the flags in the current map data.

Having looked through your code, I think what you need for both types of enemies is more generic handling. You already have the up-down enemy in a table, so you already have a working representation of the enemy as an object. What you can do to expand that is to instead use that table as a prototype to make a list of enemies (as a table) instead. Here's what that would look like:

    enemy_list = {}
    for i=1,10 do
      local enemy={
          sp=16,
   -- this or something similar so they don't end up in the same spot
          x=120 + i * 300 - flr(150*rnd()),
          y=30,
          w=8,
          h=8,
          dx=1,
          dy=1,
          dir=.02
      }
      enemy_list[i] = enemy
    }

If you then add a parameter "enemy" to your existing functions for dealing with the up-down enemy, you hand them individual enemies from the list. If done in the _update() function, here's how that could look:

        for i=1,#enemy_list do
          enemy_update(enemy_list[i])
          enemy_animate(enemy_list[i])
        end

(there appears to be functions in pico-8 to make this exact method faster, but I haven't tried them)

Then the last bit to make enemies work the way you've described them would be a table-based collision function. Your existing function map_collide() does a good job at checking the space that the object is about to move into. It'd be a good idea to use the same code up until the part that translates from pixels to tiles. For the test of whether the space overlaps with an object in a table, a simple axis-aligned bounding box test can be used. Using the names x3, y3, w2, h2 for the second object's values, that would look like this:

  -- the same code as map_collide for x1,y1,x2, and y2
  -- then some code for getting x3,y3,w2, and h2 from the enemy 
  -- to check against
  local x4 = x3 + w2 - 1
  local y4 = y3 + h2 - 1
  if (not (x1 > x4 or y1 > y4 or x2 < x3 or y2 < y3)) then
    return true
  end

For why that would work, mboffin posted a cart that visually demonstrates it here: https://mboffin.itch.io/pico8-overlap.

As for the coins, you could use the above method for those too, but if they're not moving it might be overkill. If you just want coins that stay still but animate, then I would instead recommend keeping track of where the coins ended up using a table, then changing each one based on their current sprite. That way you could still use map_gen as normal for them.

For having 3 lives, that depends on what you want to happen on death. If the player's speed just stop and they get to be invincible for a bit, then that'd be as simple as setting the player's speed and checking if the player has moved enough before checking any further interactions with enemies. If death means restarting, then placing all the initial setup in a function that can be redone would help. That way you could effectively restart the game but have the score and start stay the same.

For copying the map, you can use mget and mset in a pair of loops if you want to use the straight-forward way:

  for _x=32,32+46 do
    for _y=4,11 do
      mset(_x,_y,mget(x+47,y))
    end
  end

(47 is half the range you currently have tiles being set at, rounded down)

If that feels too inefficient, I believe this would be equivalent using memcpy instead:

  for _y=4,11 do
    memcpy(0x2000 + 32 + _y * 128, 0x2000 + 32 + 47 + _y * 128, 47)
  end

I'm not confident about that, though. Also, if you choose to use the method of storing objects in a list of tables as described above, you would need to go through the tables, preferably backwards, and check which objects are still relevant. If still relevant, their x-positions would need to be changed. If not, they would need to be deleted (which is the reason to go through the table backwards).

For generating tiles on just the second half of the map, you could just set a global variable to indicate that the initial generation was done, then have place_tiles() check it to see what the range for placement should be.

P#103841 2021-12-30 21:24

Cart #yonijoyuwi-0 | 2021-12-30 | Code ▽ | Embed ▽ | No License

@kimiyoribaka thanks a bunch, your help has been invaluable.

I've paused on the map generation as I have to submit this game tomorrow, but I will revisit it. For now I've just let it repopulate the tiles per 8 screens.

I've tried to implement the enemy collision as you've described but I keep running into errors. Specifically "attempt to call global enemy_collide a nil value".
I made the enemy1 and 2 objects and got them moving on the screen, all good, then I made an enemy_collide() that takes player obj, aim, flag, and enemy obj data but I don't think I'm doing it right.

P#103855 2021-12-30 23:41

A nil value means it doesn't exist at the moment. There's 3 reasons that could occur:

  1. the code reached the call before the function is defined (which won't happen using the structure that you're using)
  2. the function was set to nil
  3. a typo

In this case it appears to be #3. You put ememy_collide, and it's hard to tell (took me a while to notice too) due to the font.

P#103861 2021-12-31 00:20

Cart #pisebusiya-0 | 2021-12-31 | Code ▽ | Embed ▽ | No License


Lol I've been agonizing over the past hour and turns out the issue the whole time was emeny_collide() instead of enemy, jfc. I think I've got it working in it's basic state now! Please let me know if you have any more ideas/thoughts! I got the pots working as 'coins' now but I'm still trying to figure out how to make them a little more animated/disappear after being hit

P#103862 2021-12-31 00:21

Also, as I was messing around trying to figure it out I ended up putting the enemy_collide() calls in enemy_update(), is that a good way to go about it? Previously I had it in the draw function, which seems like maybe a less efficient way to do it.

P#103863 2021-12-31 00:22

As I said before, nothing that affects gameplay should be in a draw function. The reason is that depending on the platform, pico-8 may need to change how often things are drawn, which can cause unintended differences in player experience.

As for where exactly to put it, remember that what works is more important what's ideal. The actual most efficient spot or method for anything would probably require benchmark testing to figure out, and could be the result of nuances that aren't documented.

For the pots, animating them is just changing the sprite. I would advise putting their positions on the map in a table during generation, then just going through the table using mget to check the current animation sprite and mset to change to the next one. If you use this method, though, make sure the code doesn't change map positions that don't have a pot anymore. Alternatively you could also just check all the map positions that are currently visible and change any that have the pot flag.

For making them disappear, I think the easiest way to code would be to have the map_collide() function store the map positions that actually got checked so that if a pot is hit, you can have the 4 possible map positions be checked against the flag for pots. If any still have that flag, you can just use mset() on that position to change it to empty (sprite 0 usually). There's more elegant ways like having a variable return value, but they require knowing the nuances of lua. Alternatively, you could also figure out the map positions again based on the player position.

P#103865 2021-12-31 00:43

Is your jump measurable?
Like, a left jump, neutral jump, right jump kind of way where you can predict in tiles what will happen, relatively?

I think there's a fair bit of map design you can work with these metrics in mind... and even if you have precise mid-air control, it's still just a minor deviation from these.

P#103869 2021-12-31 01:25

Cart #juwotedusi-0 | 2021-12-31 | Code ▽ | Embed ▽ | No License


[edit: just updated it again so enemies dont appear until 10s in]

super grateful to those who have responded and helped out.

Here's where I'm at with it for now - working around the constraints of my abilities I added a flappybird-esque jump and it feels pretty fun. I'm definitely going to keep working on it though and figure out how to store the generated objects in tables, and then on to real map procedural gen.

@TonyTheTGR I was trying to figure out how to put in a double jump, and then make the actual momentum controllable as otherwise the game would put you in impossible situations, but then I accidentally did the flappybird thing and I feel like it compensates for the issue for now.

P#103871 2021-12-31 01:41 ( Edited 2021-12-31 01:44)

I've been watching your progress on this, @ssarkar - your improved coding each time. Looking good ! Keep it up ! :)

Here's a gold star for your encouragement.

P#103872 2021-12-31 01:41 ( Edited 2021-12-31 01:41)

@dw817 :D i <3 pico, it's so much fun

P#103875 2021-12-31 01:45

@kimiyoribaka hey, I've continued to work on the game off and on, and I'm basically happy with it except for the map generation, endless running (the most important part lol). You were so helpful before, I could really use your help to finish the project. I really appreciate your time and effort.

Here's where I've gotten to:

Cart #zusasatoso-0 | 2023-01-09 | Code ▽ | Embed ▽ | No License

I turned off enemies for testing purposes.
Currently, I call map_gen() in my init() to generate a few platforms. The way I had it before, every time the player reaches the end of the map (8 screens), they get teleported back to screen 1, and I call map_gen() again a few times. So the platforms would always be stacking on each other instead of generating a new level.

I read what you said about not calling map_gen() in the update loop, so I commented it out of my update_game() function. But now I'm a bit at a loss of what to do. I tried a few different methods to get the map to regenerate - I took c{} out of the local scope of map_gen() and tried to use del(c,i) in map_update() to try and remove the previous blocks. I feel like I don't really understand how to use mget() or mset().

I'm also not sure how to continue generating the level if I only call map_gen() once at initialization. Am I supposed to store the platforms and their positions? Or is it store the platforms and draw new positions? I'm just not sure.

Compounding the issue is that I still don't understand how to do what you were saying, namely copy the second half of the map and paste it to the first, move the player back to the first half, and then generate the second half of the map.

Any help would be appreciated. This is the last hurdle for me in finishing this game!

P#123987 2023-01-09 00:52

I just re-read this thread and I'm not sure when I said to avoid map generation in the update loop. That makes it awkward to explain that advice, as I don't know why I would have said that. I did advise you to not do map generation in the _draw function, but that's just because it can make the timing inconsistent. If that's not a problem right now, you may be better off leaving the map generation where it is. Given that you're about to finish the game, the amount of effort is a good thing to take into account.

Looking at your current version, it seems like all you really need to do is clear out the current tiles and adjust how many are being added. This is made simple by how you added them to being with. Your code currently can only add tiles on rows 4, 7 and 11 on the map, so you can just get rid of them by setting them to 0. Calling this code right before generating the new level would work:

for _y=4,11,3 do
  for _x=1,127 do
    mset(_x, _y, 0)
  end
end

Other than that, I think the transition when rescrolling could be smoother. To start with, if you add this line of code after drawing the level:

    map(0,0,map_x-map_end,0,map_end,16)

it makes the ground not look like it's disappearing. You've got a couple other visuals that would need to be included too, but I'm not sure I understand them well enough at the moment to give advice on them.

I have other things to do at the moment, but I hope this helps for now.

P#123993 2023-01-09 02:40

@kimiyoribaka genuinely thank you so much for your help. Here is the latest version of the game. I have yet to add proper menuing for start and death screens, so that's next on the list for me.

https://www.lexaloffle.com/bbs/?tid=45916

If you have any more suggestions on how to improve it, please let me know. Particularly if there might be a way to make the rescroll better, since there's still a frameskip. I also played a bunch of your carts and they're really impressive.

P#124063 2023-01-10 19:38

@ssarkar
A single frameskip at 30 fps is generally not something a player would notice unless you did something that makes it obvious. I looked at your code again. You're clearing the screen 14 times when doing rescroll, and the first time is the only one that happens before the frame is drawn. If you get rid of the cls() within map_gen(), that will at least stop the screen from going black, which is what makes it obvious. After that, the change still slightly visible because things aren't aligning correctly. In the case of the player character, you could improve the change in position by subtracting map_end from the current x position rather than setting it directly. That would make it so the player's sub-tile position stays consistent. I don't know how you would prevent the pillars from also visible shifting though, since I'm only so good at reading other people's code.

P#124065 2023-01-10 20:31

@ssarkar
you might want to change the collison for the floor to one solid line instead of bumpy collision. if you press and hold jump to fly and just let it bounce on the roof, you can go backwards and get stuck above the level.

P#124256 2023-01-13 23:34

@AntiBrain is there a way to keep the ceiling’s current look but put in transparent tiles in a line to have that?

P#124259 2023-01-14 01:01

Also I’m fairly positive that’s no longer possible in my latest build but I’d still like to fix it since the bumpy ceiling collision doesn’t look as smooth.

P#124260 2023-01-14 01:03

hey @kimiyoribaka - I have to thank you again, your help was so valuable in finishing this game. I am basically all the way there, going to work on the title menu now to finish it off.

The last piece of advice I'd ask for is on collecting the gems in the level - since they are generated using mset(), I am using fset() to change the flags of the one you collide with and then using mset() on the player's position to black out the gem and make it disappear. However this is imprecise, and leads to gems sometimes being picked up multiple times or not disappearing at all. If you have any insight on how to fix this, I'd appreciate it.

Otherwise, any other advice on how to improve the game would also be appreciated.

https://www.lexaloffle.com/bbs/?tid=45916

P#124275 2023-01-14 06:17

fset() is the wrong tool. I can see why you'd try it, but I don't think there's any case where fset() would be a good idea for changing one instance of something.

You already have the correct code for getting rid of the gems. mset() works fine and is the only part needed for the gem to disappear. The issue is just which tile to make disappear. Your collision check works, but it doesn't return which tile. There's two ways to fix this: 1. change the collision check to return an indication of which tile it detected (which would still equate to true because of how lua conditions work) but still return false if no collisions were found. 2. have your gem collecting function check all the possible spots (4 in this case).

Here's code that does option 2 and also erases the correct tile:

        if mget(player.x/8+1,player.y/8)==104 then
          mset(player.x/8+1,player.y/8,0)
        elseif mget(player.x/8+1,player.y/8+1)==104 then
          mset(player.x/8,player.y/8+1,0)
        elseif mget(player.x/8+1,player.y/8+1)==104 then
          mset(player.x/8+1,player.y/8+1,0)
        elseif mget(player.x/8,player.y/8)==104 then
          mset(player.x/8,player.y/8,0)
        end

Also, I read the posts above this one, about the ceiling problem. It's not a perfect solution, but I just messed a bit with a copy of your cartridge, and it would work to change the reaction to hitting the ceiling so that the player's dy value becomes 0 and the player's y position goes down by 8. That would prevent the player from bouncing backwards by preventing the ceiling from still being detected, while also still knocking the player down in a moderately believable fashion.

P#124279 2023-01-14 07:30

[Please log in to post a comment]