Log In  

Cart #ultra_simple_platformer-0 | 2021-03-08 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
12

Demo:

Same base engine used in Bullet Ball:

This platformer base code is what my game Bullet Ball is built on. It is inspired by Super Mario Bros for the NES. This demo contains code for Super Mario style variable jumping, AABB collision (1 way platforms or full collision), and a very simple animation function.

Like many newbies, I struggled with platforming when I was learning to program and always found that my code was bloated with many unnecessary variables. I still see many newbies struggling with this. I'm also seeing many demos with verbose code and needless multiple variables.

Programming is difficult and optimizing even more so, but as a rule of thumb, you always want to have efficient, optimized code that's clear to read and understand. By efficiency, you want to use as little computer power as you can. Even though with today's ultra blazing fast processors, it seems not necessary, it's a good practice, because it also helps you simplify. It takes years to get there, but you learn.

The magical element in my code is the use of lookup tables for the use of movement and position for collision detection. Super Mario Bros used lookup tables as using mathematical equations (especially multiplication and division) would bog down the program on the NES. The NES ran at 1.79MHz with very limited ram. NES programmers had to optimize hardcore to get their games to run the way they wanted them too. SMB is a true marvel. In my own movement code, I only increment, decrement and use modulus (same as division, but the result is the remainder) for friction.

So, a quick look at the lookup tables and an explanation for them. Following that in the hidden tag is commented code. It is aimed at newbies with little experience coding. The code in the demo above is condensed down to 87 lines (sans the 2 dividing dashed lines) and can be condensed a bit more if you take out the block for 1-way platforms.

This is the speed look up table. This is used for both horizontal running and vertical jump movement.

speed={-4,-2,-1,-1,0,1,1,2,4}

By using a counter variable that can increment or decrement, I can then add the result to my x or y position and voila.

For example, if you wanted the player to just run right at top speed:

hv=9 --hv is horizontal velocity
x+=speed[hv]

So by simply changing the hv variable, I can make the player not only move left or right, but also immitate acceleration and drag. Notice how the sets of numbers count up on both sides of 0. Look at the movement function in the code below to see how this all works.

For gravity, using the speed table is super handy. If vv (vertical velocity) is less than 5, my player will move up (jumping), but go past 5 and the player starts to fall. So in my movement code, I have:

if(vv<9) vv+=1

This line runs no matter what. When holding down my jump button, vv is set to 1 and will be reset at 1 every tick until either my jump boost (see code below) runs out or I let go before it runs out, so that the player will move up. When I let go or my jump boost has hit max, then the line above is free to count up vv. This causes an arc movement and then the player falls down and will continue to move down unless stopped by a platform. Gravity.

My 2nd look up table is simply a table of multiples of 8:

for i=-1,16 do
    tile[i]=i*8
end

This table helps in detecting local collision around the player. Refer to the code below to see how it works.

Feel free to use the code as a base for your projects. Be careful if you tinker with the values within the speed table. Collision can be easily thrown off and you might end up inside or going through blocks. I'm not easily available, but I'll answer any questions best I can, when I can. I'm also of the "there are no stupid questions" policy. Ask anything.

You can also look at the code in Bullet Ball to see more platformer functions and to see how my code works with the entire map space. At the moment BB's code is uncommented, but I am putting together a post-mortem post that will contain commented code.

FULL COMMENTED CODE:

-->8
--player variables
x=60
y=88
xdir=false 

--movement variables
hv=5 --horizontal velocity
vv=5 --vertical velocity
friction=0
surface=2 --works as a counter with friction.. the higher the number, lower the friction
boost=0 --variable jump height counter
speed={-4,-2,-1,-1,0,1,1,2,4} --this is the speed look up table. it can be used for left/right and vertical movement.

--map variables
tile={} 
for i=-1,16 do
    tile[i]=i*8 --look up table with multiples of 8 (8*-1 to 8*16). this is used in the collision function for horizontal and vertical coordinates.
end
--animation variables
anim=false 
set=2 --the first sprite of the current set of frames to animate.
start=0 --this works as a counter with duration.
duration=3 --length of current set of frames
frame=1 --starting frame

--test timer variables
timer=0
timerlimit=20 

-->8
function _update()
    --timer=(timer+1)%timerlimit
    --if timer==0 then
        input() 
        move() 
        animate() 
    --end
end

function input()
    if btn(0) then --if pressing "left", face left, count down horizontal velocity
        xdir=true 
        if(hv>1) hv-=1 
    elseif btn(1) then --"right", face right, count up horizontal velocity
        xdir=false 
        if(hv<9) hv+=1 
     else 
        friction=(friction+1)%surface --when friction%surface is 0, then if moving horizontally, count up or down until hv is 5
        if friction == 0 then
            if(hv<5) hv+=1
            if(hv>5) hv-=1
        end
    end
    if btn(5) then
        if boost < 6 then --variable height jump
            boost+=1 
            vv=1 --so long as boost is less than 6, keep vertical velocity at 1. speed[1]=-4, so the player will continue moving up 4 pixels
        end
    else
        boost=6 --if let go in mid air, set to 6 to prevent double jumping. resets to 0 when on ground
    end
end

function move() --here is where the speed look up table is used for both horizontal and vertical speeds
    x+=speed[hv] 
    y+=speed[vv]
    if(vv<9) vv+=1 --unless vv is set by another function, it will otherwise count up till 9. here is where the jump arc happens and then constant gravity.
    collision()
end

function collision()
    anim=false --at the beginning, anim is false - this way, unless player is touching ground, player will display jump sprite.
    surface=4 --setting at 4 at the beginning allows for more natural movement in air. on ground is set to 2.
    if(x>=120) x=120 --set right side screen boundary
    if(x<=0) x=0 --set left side screen boundary
    local n1=flr(x/8) --n1 and n2 get the coordinates of the tile that the top left pixel of the player is currently inhabiting.
    local n2=flr(y/8)
    for i=n1-1,n1+1 do --now we check all the tiles around the player. in this code, this is extended 2 tiles below the player.
        for j=n2-1,n2+2 do
            if fget(mget(i,j),0) then

                --uncomment this block if you want one way platform collision------
                --[[if x < p[i]+7 and x+7 > p[i] and y <= p[j-1] and y+8 >= p[j-1] and vv>5 then 
                    -- if y < p[j] and y+8 >= p[j] then
                        -- if (not btn(5)) boost = 0
                        -- y=p[j-1]
                        -- vv=5
                    -- end
                end]]
                -------------------------------------------------------------------                         
                --this block of code allows for full aabb collision---------------- 
                --top of tile
                if x < tile[i]+7 and x+7 > tile[i] and y < tile[j] and y+9 >= tile[j] and vv>5 then --note that the 'tile' look up table is used here.
                    if (not btn(5)) boost = 0 
                    y=tile[j-1] --set player position on ground
                    vv=5 --stop vertical movement
                    anim=true --now on ground, anim is true
                    surface=2 --add more friction to movement now that on ground
                end
                --bottom of tile
                if x < tile[i]+7 and x+7 > tile[i] and y-1 <= tile[j]+7 and y+7 > tile[j]+7 then
                    if(vv<9) vv+=1 --incrementing vv here doubles time for movement to go from up to down.
                    boost=6 --set to 6 to cancel jump
                    y=tile[j+1]
                end
                --right side of tile
                if x-1 <= tile[i]+7 and x+7 > tile[i]+7 and y < tile[j]+7 and y+7 > tile[j] then 
                    hv=5
                    x=tile[i+1]
                end
                --left side of tile
                if x < tile[i] and x+8 >= tile[i] and y < tile[j]+7 and y+7 > tile[j] then 
                    hv=5
                    x=tile[i-1]
                end
                --------------------------------------------------------------------
            end
        end
    end
end

-->8
function animate()
    if anim then 
        if(hv~=5) then
            start=(start+1)%duration --when anim is set to true and hv is not 5, then run the 'running' animation
            frame=start+set
        end
        if(hv==5) frame=0 --if hv is 5, then set frame to 'stand' frame
    else
        frame=4 --otherwise set to jump frame
    end
end

function _draw()
    cls()
    map(0,0,0,0,16,16)
    spr(1+frame,x,y,1,1,xdir) 
end

P#88071 2021-03-08 21:01 ( Edited 2021-03-09 04:03)

indeed solid basis.
comments:

  • vertical speed is too floaty imho, suggest a dedicated vertical speed table
  • too many single letter globals, this is very error prone in a larger codebase
  • use functions with parameters to handle collision, again to allow multiple entities without relying on globals
  • platform collision is a bit awkward as it doesn’t take velocity nor player size into account (something that will trip beginners)
    o
P#88710 2021-03-09 07:38 ( Edited 2021-03-09 07:44)

@LawrenceJunior thank you. :)

@freds72 Thanks for your feedback: I'll address each point you brought up:

1.Yes a vertical speed table can be implemented, but collision detection might need to be altered a bit to keep from falling through or into a block. Usually when the player falls down, the position is always multiples of 4 along tile axis'. This works perfect with the current collision detection.

2.For the purpose of a small demo, x and y for example, being separate single letter variables isn't going to hurt. For newbies reading this response, you can make a player object like this:

player={x=60,y=88,xdir=false}

Then if you're updating x in the code, you would instead use

player.x+=speed[hv]

I could have done this in the demo code, but for learning sake, I try to keep it simple and easy to read and understand.

3.I do this in Bullet Ball. Again, in the demo, just for simplicity sake.

You do have me thinking that perhaps I should try a Youtube tutorial or tutorial series. Then I could build a whole project, show potential pitfalls and what to do to avoid them.

4.I'm a bit confused by the last comment. Do you mean that it doesn't feel like there is friction when coming in contact with a platform? I have a little friction function that can be altered by lowering the "surface" value. I have it currently set to 2 and it allows a small bit of sliding on the platform - like Mario when running at full speed. There's 2 things that could be done there: when coming into contact with a tile - set surface to 1 (but I found that it felt more sticky). OR if moving slowly, surface can be lower, but if moving at full speed, surface can be knocked up 1. Not sure how much that would help though. When I designed this particular platformer base, it was meant for fast action movement.

The bounding box for the player is the same size as tiles (since, the original characters are almost square):
[0x0]
[0x0]
[0x0]
[0x0]

The Mario character is smaller and I might just update the code to give him a smaller bounding box.

Thank you again. :)

P#88729 2021-03-09 17:08 ( Edited 2021-03-09 17:13)

i like this alot. i'm curious to what would be involved in using a custom/smaller bounding box. I'm struggling with this in general, not just when looking at this code. :(

P#89024 2021-03-15 21:52 ( Edited 2021-03-15 21:53)
3

Hi @tjbynum, so I whipped up a quick image for ya. The following works for axis aligned bounding boxes:

I'm just explaining the X-axis here, but this is how Y works as well. It can get confusing when the X,Y coordinate of any sprite in Pico-8 is the top-left pixel. This is the way the map works as well. The map starts at 0,0 at the upper left corner.

When you give your character a bounding box, it will depend on how you position it. In the case of my chonky Mario character, the standard bounding box starts at X and goes to X + 7 (7 more pixels to the right totaling 8 pixels). So I don't have to position it, it's already centered. In the next example I shrunk the bounding box down to 6 wide and 7 high. To center it, I go by the X coordinate again, but add 1 on the left side and take away 1 on the right. So now it starts at X+1 and ends at X+6. The top of the sprite is also shrunk down 1 pixel, so the top starts at Y+1.

Let's say you have a small character that's only 4 pixels wide. You could center by X+2 to X+5 or you could draw the character to the left side of the 8x8 sprite space and set the bounding box at X to X+3.

If you look at the lines of code above, chonky mario can run back and forth on the block, and as long as the edges of the bounding boxes touch, he'll stay on top of the block. I hope this helps. :)

P#89085 2021-03-17 09:28 ( Edited 2021-03-17 15:24)

@Snow_ thank you so much for this. let me mull it over a bit and see if i can get something working in the game i've been working on. again, thank you!

P#89118 2021-03-17 19:09
1

@tjbynum You're very welcome. Another trick you can do (can also be done digitally in paint.net for instance), is to cut out pieces of graph paper where each grid square represents a pixel. Draw your sprite on one and the other one will be the object or block you want to figure out your collision for. Then on your sprite, draw your bounding box on the grid lines. Then you can position them around and see where lines and corners intersect. Game design almost always involves you making yourself tools that help you figure things out. Sometimes the tools are very personalized. I've known designers that will even code their own map makers, so that it's more intuitive for them.

One thing to remember with collision, if you're just doing AABB collision, it's best that the blocks/tiles/objects you collide with use their full size bounding box. So in the case of the platformer demo above, the platform blocks are all 8x8. If you shrink the bounding box for blocks/tiles, you might end up producing passable gaps, if they are lined up in rows or columns. If you're doing an RPG and you have trees that you want to be able to pass between, then you can give them a tiny bounding box (or none if you do old school overworld).

I wish you the best with your project. And remember, 90% of your coding will be just problem solving, so don't let it get you down. Struggling is part of learning.

P#89126 2021-03-17 20:30

Thanks for making and sharing this! It’s nice to have something with all the basics working, and it’s small enough to be adapted (say if I wanted to avoid global variables or rename the functions, easy to do myself).

For a future game I have looked at another platformer lib (by eniko, https://www.lexaloffle.com/bbs/?tid=35821) which has a nice feature: slopes! Maybe you’re interested in adding support for these. Or maybe not and it will be a challenge for me!

P#89127 2021-03-17 21:05 ( Edited 2021-03-17 21:07)
1

@Snow_ thanks again, i think i'm getting the hang of it slowly but surely. i was having issues with my sprite "bumping" his head, but i think the help you've given will set me on the right path. thanks again for sharing with the rest of us.

@merwok yep, i've seen that and i like it. this is also really slick. https://github.com/jamesedge/pico8-physics

P#89131 2021-03-17 22:05
1

Well you know, I've been wanting to do slopes. I could approach the method the same way I'm using look up tables - using a sub-flag system, detect which block my sprite is coming in contact with. Then adjusting the hv and vv values to make it seem like the sprite is colliding with the slope (no matter at which point on the slope). The other method would be to try and come up with a compact polygon collision detection function.

I'm intrigued. I'll test out these methods. In the meantime, @merwok I do encourage you to try your own solution as well.

I think I'll be adding more demos to this thread for sure to make it a more complete beginner's platformer base. I had forgot to add wall jumping to the demo above (which is super easy in my code), so I'll add that to a second demo.

If I do make a demo with slopes, I'm only going as far as 3 or 4 angles per left or right direction. I'm not going to do curves (like Sonic levels), as that's more advanced and not beginner friendly. I'd say, I'll go as far as the mechanics of Super Mario Bros 3.

P#89156 2021-03-18 07:50

awesome, looking forward to more!

P#89188 2021-03-18 21:21

@tjbynum that physics lib has very impressive demos!

@Snow_ right, I’d be very happy with just one angle at 22.5° (so that two tiles bring you one tile up, see that other thread if I’m not clear). but now that you’ve said 3 or 4 angles I see the potential for variety in level design and nicer transitions, and it could be interesting to write that code!

P#89248 2021-03-20 03:55 ( Edited 2021-03-20 04:04)

[Please log in to post a comment]

Follow Lexaloffle:          
Generated 2022-12-02 05:30:52 | 0.064s | Q:29