How to create a Platformer

Okay. Here’s a big chunk. Not sure if it’ll be of any help without explanation, but if you’d like to check it out, go ahead and feel free to ask questions:

hero.frameLoop = function(event) local touching = touching -- compute potential horizontal motion for next frame --print(editor.state) if hero.dead == false then if touching == "left" then hero.xVel = hero.xVel - hero.xAccel elseif touching == "right" then hero.xVel = hero.xVel + hero.xAccel elseif touching == false then hero.xVel = hero.xVel \* hero.friction end end if hero.xVel \> hero.xMaxVel then hero.xVel = hero.xMaxVel elseif hero.xVel \< -hero.xMaxVel then hero.xVel = -hero.xMaxVel end hero.xPosNext = hero.xPos + hero.xVel if hero.platform ~= false then -- add platform xOffset to hero.xPosNext hero.xPosNext = hero.xPosNext + platformList[hero.platform].xOffset end -- compute potential vertical motion for next frame if hero.jumpState == "falling" then hero.yVel = hero.yVel + hero.yAccel jumpTouch = false elseif hero.jumpState == "grounded" then if jumpTouch == true then jumpTouch = false hero.jump() end end if hero.yVel \> hero.yMaxVel then hero.yVel = hero.yMaxVel elseif hero.yVel \< -hero.yMaxVel then hero.yVel = -hero.yMaxVel end -- set yPosNext if hero.platform == false then -- move hero with velocity hero.yPosNext = hero.yPos + hero.yVel else -- move along with platform hero.yPosNext = platformList[hero.platform].image.y end -- reevaluate next position for hero based on collisions! local levelMap = levelMap -- first correct the vertical movement, based on current X and next Y position !!! local bottomRow = math.floor((hero.yPosNext)/94) -- first row = row 0 which is good for row multiplier 0\*mapwidth for Arraypos's local topRow = math.floor((hero.yPosNext-252)/94) -- first row = row 0 which is good for row multiplier 0\*mapwidth for Arraypos's local leftColumn = math.floor((hero.xPos-62)/126) -- first column = column 0 so add 1 for Arraypos's local rightColumn = math.floor((hero.xPos+62)/126) -- first column = column 0 so add 1 for Arraypos's if (hero.jumpState == "falling") or (hero.platform ~= false) then if hero.yVel \> 0 then -- moving down -- check if bottom points are in a platform if hero.platform == false then for i = 1, #platformList do local vDistance = hero.yPosNext - platformList[i].image.y local hDistance = math.abs(hero.xPosNext - platformList[i].image.x) if (vDistance \> 0) and (vDistance \< 64) then -- 64 for platform height if platformList[i].type == 3 then if (hDistance \< 236) then hero.jumpState = "grounded" hero.platform = i hero.yPosNext = platformList[i].image.y if platformList[i].dropform == true then platformList[i]:startTimer() end end elseif platformList[i].type == 2 then if (hDistance \< 156) then hero.jumpState = "grounded" hero.platform = i hero.yPosNext = platformList[i].image.y if platformList[i].dropform == true then platformList[i]:startTimer() end end end end -- if end -- for platformList end -- repeat check for bottom points in a dropform!!! local botLeftTile = levelMap.physics[leftColumn+1+bottomRow\*levelMap.width] local botRightTile = levelMap.physics[rightColumn+1+bottomRow\*levelMap.width] if (botLeftTile ~= 1) or (botRightTile ~= 1) then -- one or more bottom corners will be on a solid tile audio.play(landSound) hero.yVel = 0 hero.yPos = (math.floor(hero.yPosNext/94)\*94)-1 hero.jumpState = "grounded" hero.platform = false else hero.yPos = hero.yPosNext end elseif hero.yVel \< 0 then -- moving up local topLeftTile = levelMap.physics[leftColumn+1+topRow\*levelMap.width] local topRightTile = levelMap.physics[rightColumn+1+topRow\*levelMap.width] if (topLeftTile == 2) or (topRightTile == 2) then -- one or more top corners will be on a solid tile hero.yVel = 0 -- WRONG!!! hero.yPos = (math.floor(hero.yPosNext/94)\*94)+64 else hero.yPos = hero.yPosNext end elseif hero.yVel == 0 then -- no vertical movement hero.yPos = hero.yPosNext end -- if (hero.jumpState == "falling" or platform is true) else -- if hero == "grounded" end -- then correct the horizontal movement, based on corrected Y and next X position !!! bottomRow = math.floor((hero.yPos)/94) -- first row = row 0 which is good for row multiplier 0\*mapwidth for Arraypos's local midRow = math.floor((hero.yPos-126)/94) -- first row = row 0 which is good for row multiplier 0\*mapwidth for Arraypos's topRow = math.floor((hero.yPos-252)/94) -- first row = row 0 which is good for row multiplier 0\*mapwidth for Arraypos's leftColumn = math.floor((hero.xPosNext-62)/126) -- first column = column 0 so add 1 for Arraypos's rightColumn = math.floor((hero.xPosNext+62)/126) -- first column = column 0 so add 1 for Arraypos's local xSpeedWithPlatform = hero.xVel if hero.platform ~= false then xSpeedWithPlatform = hero.xVel + platformList[hero.platform].xOffset end if xSpeedWithPlatform \> 0 then -- moving right local topRightTile = levelMap.physics[rightColumn+1+topRow\*levelMap.width] local midRightTile = levelMap.physics[rightColumn+1+midRow\*levelMap.width] local botRightTile = levelMap.physics[rightColumn+1+bottomRow\*levelMap.width] if (topRightTile == 2) or (midRightTile == 2) or (botRightTile == 2) then -- one or more right corners will be on a solid tile -- check wether on platform and only bottom point colliding if (hero.platform ~= false) and (topRightTile ~= 2) and (midRightTile ~= 2) then -- hitting only bottom point to get off platform! hero.xPos = hero.xPosNext else hero.xVel = 0 hero.xPos = (math.floor(hero.xPosNext/126)\*126)+63 end else hero.xPos = hero.xPosNext end elseif xSpeedWithPlatform \< 0 then -- moving left local topLeftTile = levelMap.physics[leftColumn+1+topRow\*levelMap.width] local midLeftTile = levelMap.physics[leftColumn+1+midRow\*levelMap.width] local botLeftTile = levelMap.physics[leftColumn+1+bottomRow\*levelMap.width] if (topLeftTile == 2) or (midLeftTile == 2) or (botLeftTile == 2) then -- one or more top corners will be on a solid tile -- check wether on platform and only bottom point colliding if (hero.platform ~= false) and (topLeftTile ~= 2) and (midLeftTile ~= 2) then -- hitting only bottom point to get off platform! hero.xPos = hero.xPosNext else hero.xVel = 0 hero.xPos = (math.floor(hero.xPosNext/126)\*126)+64 end else hero.xPos = hero.xPosNext end elseif hero.xVel == 0 then -- no horizontal movement hero.xPos = hero.xPosNext end -- gate check here! for i = 1, #gateList do local gate = gateList[i] if gate.state == "closed" then -- start checking stuff -- if gate is OPEN we do nothing at all! :-) -- CHECK wether hero is IN gate rect if math.abs(gate.group.x - hero.xPos) \< 128 then -- in horizontal area so check V overlap if (gate.group.y - hero.yPos) \< 192 and (gate.group.y - hero.yPos) \> - 444 then -- Y also within gate if hero.holdingKey == false then -- hero is blocked because gate is closed and hero is not holding a key -- BLOCK HERO!!! if hero.xVel \< 0 then -- moving left hero.xVel = 0 hero.xPos = gate.group.x + 128 elseif hero.xVel \> 0 then -- moving right hero.xVel = 0 hero.xPos = gate.group.x - 128 end else -- HERO is HOLDING A KEY! -- So check if it is the RIGHT KEY index for this gate if keyList[hero.holdingKey].group.index == gate.group.index then -- hero opens door and moves through gate.open(gate) -- is equal to gate:open() keyList[hero.holdingKey].group.y = -1000 keyList[hero.holdingKey].state = "used" hero.holdingKey = false else -- hero is holding a key with the wrong index -- BLOCK HERO!!! if hero.xVel \< 0 then -- moving left hero.xVel = 0 hero.xPos = gate.group.x + 128 elseif hero.xVel \> 0 then -- moving right hero.xVel = 0 hero.xPos = gate.group.x - 128 end end -- check if holding the right key end -- hero holding key or not check ends end -- if hero is in vertical rect check ends end -- if hero is in horizontal rect check ends end -- if gate.state == CLOSED or OPEN check ends end -- for loop over all gates ENDS -- finally, check to see if hero is still standing on solid tiles or on finish -- if not on solid or cloud, set jumpState to "falling" -- if on finish tile, level is completed! bottomRow = math.floor(((hero.yPos)/94)+1) -- first row = row 0 which is good for row multiplier 0\*mapwidth for Arraypos's leftColumn = math.floor((hero.xPos-62)/126) -- first column = column 0 so add 1 for Arraypos's rightColumn = math.floor((hero.xPos+62)/126) -- first column = column 0 so add 1 for Arraypos's local botLeftTile = levelMap.physics[leftColumn+1+bottomRow\*levelMap.width] local botRightTile = levelMap.physics[rightColumn+1+bottomRow\*levelMap.width] if hero.platform ~= false then -- hero is standing on a platform local hDistance = math.abs(hero.xPosNext - platformList[hero.platform].image.x) if platformList[hero.platform].type == 3 then if hDistance \> 236 then hero.platform = false hero.jumpState = "falling" hero.yVel = -10 end elseif platformList[hero.platform].type == 2 then if hDistance \> 156 then hero.platform = false hero.jumpState = "falling" hero.yVel = -10 end end else -- hero is not on a platform if (botLeftTile == 1) and (botRightTile == 1) and (hero.jumpState == "grounded") then -- both bottom corners will be on an air tile hero.yVel = -10 hero.jumpState = "falling" elseif (botLeftTile == 8) and (botRightTile == 8) and (hero.jumpState == "grounded") then if hero.finished == false then --print("finished!") audio.stop(1) audio.play(victorySound) -- write unlocked and score into levelTable data and SAVE! -- set current level unlocked from PLAYABLE to PLAYED levelTable.data[levelMap.number][3]=2 -- set next level unlocked from LOCKED to PLAYABLE levelTable.data[levelMap.number+1][3]=1 -- set scores (temp to Gold Silver Bronze!) levelTable.data[levelMap.number][7]=3 levelTable.data[levelMap.number][8]=2 levelTable.data[levelMap.number][9]=1 -- then SAVE the levelTable levelTable.save() hero.finished = true UI.closeShutterLevelFinished() end elseif (botLeftTile == 4) or (botRightTile == 4) and (hero.jumpState == "grounded") then -- hero is on death tile with one or two corners if hero.dead == false then hero.die() end else -- hero is still standing on solid, cloud, ice or jump with one or both bottom corners end end

The true essence of the code is that you do the following for each frame (so in an enterFrame loop) for your player character, and possibly other characters:

xSpeed = xSpeed + xAcceleration -- when going left xSpeed = xSpeed - xAcceleration -- when going right xSpeed = xSpeed \* xFriction -- when slowing doing, with xFriction at .9 or .8 xPositionNext = xPositionCurrent + xSpeed

The same goes for the vertical speed, but it’s a bit different because you check for jumping state etc…

After you’ve calculated the next X and Y position, you do a collision check for these “next” positions by checking if any of the four corners of your player are in a solid tile in the “next” position. If none is in a solid tile, your can set your player to this “next” position, and it becomes the current position. If there is a collision you need to correct your next position (and player speed), and then set this corrected next position to the current player position.

That’s really the whole basic concept!

I spent 15 minutes going over that doc, and once you ignore all the Flash bits, you are left with a very simplistic and occasionally factually incorrect set of sections once things get beyond the basic ‘here’s rectangles, let’s collide with other rectangles’.

Here’s the more important obvious errors it contains:

  • Player rectangle can’t be bigger than tiles.

  • Platforms only move in a single axis.

  • Slopes can only be fully diagonal, other (freeform) shapes aren’t possible.

There’s also other things which don’t apply to Corona (I mean apart from the general coding differences), merging clips in the section on ladders, for example, as well as many areas where Corona would be vastly better (you don’t need to move every tile when scrolling for example, you just stick everything into a group and move that, using animations to change the frames).

Given the emphasis on Flash-based structure, I’d sadly say that for us *at best* the document is useful merely as a theoretical framework, but much of that strength is lost because of the non-native English writer and errors :frowning:

The most interesting parts are those less often covered in tile-based games - isometric and pathfinding, but these are of limited value to most people.

Woah I have a lot of catching up to do, the Corona email only sent me to the first new post and I, like an idiot, failed to see there were more as they’d gone to page 2. Forgive me if my post above is a little out of context as a result!

OK it seems to me we are talking about two different approaches to tile-based games.

Thomas6 and jerejigga are covering a basic, beginner-style one (no offense to either of you intended, I’m not referring to your knowledge levels, merely the complexity of the game itself), while Caleb and myself are involved in a more complicated type of engine.

Again though, let’s not forget we can split a tile-based game into 2 areas - the drawing (which MTE and Dusk both handle), and the collision (I’ve not seen anything to suggest MTE can handle complicated collisions, but Dusk is definitely moving in the right direction, you just need patience with Caleb!).

So for now, for a simple platformer or zelda-type game there’s no reason you couldn’t use Dusk to draw, and that document to create a simple collision engine (unlike Caleb, I don’t like calling these a physics engine, dunno why not!). And it is likely more than good enough for a simple game.

I do feel though that gamers today expect a little more functionality and polish than what that document can provide. However, we all start somewhere, and I wouldn’t advocate waiting until you have the perfect code to release your first game, so I’d say that the simpler code would actually be ideal for a first couple of games, and I’ll try to bear that in mind as this thread develops.

However, if Caleb generously releases his code later on with his full physics*, it would almost immediately make many things redundant :slight_smile:

* Or indeed, if I do, but I’m not really sure whether I’ll do that, although I might look into releasing protected libraries as plugins or something.

Bottom line is, it’s all good!

Hi Rakoonic,

No offense taken. My doc is more about a Mario-level of complexity, that’s true. However, it’s pretty easy to expand things, to be honest. As it stands, the physics implementation is limited, but that’s also the beauty of working with it.

p.s. Making players bigger than tiles is easy: you just need to check collision on more than the four corner points.

No offense taken here either. And yes, I agree, we are talking about at least two different things. To your last points Rakoonic, that is basically my plan. I need to start somewhere and it makes logical sense to start simple. I will build on that and eventually create something sophisticated and truly amazing. One of things I originally asked for in this forum topic was for a how-to guide. What I am getting instead its lots of theories and concepts. In the long run, that is exactly what I need and far more valuable than a how-to guide! That said, even though the PDF that thomas6 shared might have issues, it’s still very thought-provoking. Earlier, I shared a link to a YouTube video series that someone else pointed me to. I have been working through that series and it definitely has flaws. It’s still very valuable to me though because it’s forcing me to think through the approach. Ideally, I would have a YouTube video series or PDF or web page series that provides a walkthrough of creating a Platformer using Corona SDK, from scratch, using all the best practices! Until that comes into being though, this what I have. :slight_smile:

Just a note: for MetroidVania-type platformers my simple physics should be plenty powerful to use as a base.

Amazing thread guys. I especially loved your demo Rakoonic, very impressive indeed. Also agree about not liking the event model.

If I can be so cheeky, we were thinking of doing some optimisations to our tile maps for when the player zooms out. At that point you can have around 30x30 tiles of 256x256 on screen at the same time. Corona handles it admirably but I don’t believe any MIP mapping is being done by Corona. Does anyone have any suggestions of how best to do it? My best guess at the moment is just to have 3 maps identically overlaid, all the same layout, using 3 sets of tiles (256, 128, or 64 squared) and for us to switch between them depending on the zoom. Hopefully we’ll get to it in the next month but any other suggestions are very welcome. Thanks!

@ spideri I figure the ideas in this thread need some renewed attention given the new canvas texture feature. One use case might be to use the latter to build a mipmap and then expand upon Caleb P’s technique to actually implement it. (Then again, I suppose this would exacerbate the dearth of inputs.) Are these meant as speed optimizations? Quality? Both?

An alternate approach I’m meaning to explore is to paint image sheet fill-based 2x2 rects into a texture to encode the corner texture coordinates, rather than indices, into a canvas, then look those up in-shader and do whatever. (I’m thinking animated cross-frame effects on sprites, for instance, but it might have implications for tile maps, too.) It remains to be seen if this can be done sanely in light of texture filtering.

@spideri - if the only actual difference between maps are the tile-sizes, you could probably get away with just swapping the tileset used at an appropriate distance. I guess it depends entirely on how quick the zoom is as to whether it is noticeable or not.

@starcrunch - stop coming up with interesting ideas I want to explore! But realistically, I’m not sure what benefits it would bring. One thing that you need to be careful of with shaders is pixelisation - this may or may not be a good thing depending on what you want, but unless you really want to slow things down in a shader, you are only going to ever have a single discrete pixel lookup which means no sub-pixel rendering.

I think for animated tiles I can see where you are going with your idea, but to be honest I’d probably rather just set up some special animation frames. I can’t think off-hand how to set this up in Tiled though (but I’m using their tile animation creation which works with discrete tiles just fine, so there *may* be a way to hack this - likely I’d set up the animation as a tile property (IE tile 345 has an anim property that defines how it changes over time, then at level load it uses this info to create the extra frames needed). It would be interesting to be able to paint tiles in differing resolutions all at once though, although frankly again, pixelisation could be a problem there.

@StarCrunch - Thanks for the links, we will check them out in more detail when we start doing some tests. We’re after quality and speed improvements: quality because when you’re zoomed all the way out the bilinear filtering doesn’t cut it and we get pixels flickering when it scrolls due to the super high source resolution, and speed because I’m sure it could be a lot faster with lower res textures when zoomed out. Cross-fading between the texture sets could be interesting as you pass the border between them (e.g. low to medium). Kinda like tri-linear filtering… but not. Still, it might help mitigate any visual jumps as it switches between sets.

@rakoonic - We run at 60 (where possible) so dropping frames when it swaps would not be ideal.

Thanks again guys, I think we just need to do some tests.

I should point out that many tile-engine-based games will fall foul of the horrible gap visual problem (or, if you’ve set up the tile image differently without gaps between tiles, tile-bleed). I’ve been attempting to use the new canvas texture feature to get around this, and while there’s still a few issues (an inexplicable delay in corona updating graphics which I’ll hopefully be able to prod Vlad into looking at once I’ve simplified my test case, some fighting with texture mag/min filters, and overriding content scaling without actually checking if  the files exist) it is working.

What this means is you don’t need to manually expand the borders of each tile graphics, which is a tedious and soul-crushing task, as it can be done automatically.

If you look here: https://www.dropbox.com/s/ir48kkh7an1si2u/RagnarRock.apk?dl=0

What you’ll see is some pixelation (that’s the mag/min problem from above), but what you won’t see are gaps between the tiles, despite the base 2x image normally showing them (controls aren’t 100%, this is just my platformer testbed - down in front of an open door to go through).

@rakoonic I have seen the gap issue in both MTE and Dusk! I was hoping that was just a Windows simulator issue! Are you saying that I should expect that to happen on devices too? How in the world could I create a Tiled based game if I can expect this horrible issue to occur?

Hi all,

I’ve had fairly big success sometimes by just scaling my tiles to 101 or 102 percent. Do check on different devices however, because sometimes one devices (e.g. iPhone) works best at one setting and another (e.g. iPad) works best at another setting - potentially fixed with clever device detection.

Not a magic fix mind you, but works well for some projects.

Yup, that’s the simplest solution, and I won’t knock it for its simplicity, but I’m after a more solid, pixel-perfect solution. If only I’d known the torture it would bring me while searching!

@jerejigga - there is a way to avoid it (I mean, apart from thomas6’s suggestion above).

Although the problem happens slightly when shrunk, this is most noticable when you zoom a tile image using “linear” filtering, so I’ll concentrate on explaining why that happens.

Linear filtering uses a nice blend between pixel colours. Say you have 2 pixels in the source image next to each other. White and black. Now, when you show that image zoomed, you’ll actually see a smooth fade from white to black. The GPU is linearly blending between pixels, so they don’t look all pixelated and silly up close (unless that is what you want!).

Now, with that in mind, what happens when you try to draw the edge of a tile sprite? Well, it depends on how you set up your source tile set image.

If each source tile touches the tiles next to it, when you try to draw your view, each sprite will have a border where the tile you want’s edge pixels blend into those of the tile next to it. Often not good.

The next possibility (and what I recommend) is that individual tile graphics don’t touch either other in the source tile set image. You leave ideally a 2 pixel or greater gap between them all, both horizontally and vertically. However, this now means that when you draw a tile sprite, the edge pixels are now blending from the actual graphic, into pure transparent. This is where the gaps come from.

So, the proper solution (and this is a known thing when working on this type of game), is to do your tile set graphics, then expand the edge pixels of each tile graphic by 1 - IE for each tile you duplicate its left edge one pixel to its left, its right edge one pixel to its right, and the same for up and down etc. This means edge pixels will be blended with a copy of themselves, so they’ll look 99% correct - hurrah!

However, this is a pain if you are still editing the tile set graphics. It is a tedious and frustrating task at the best of times. If you don’t do it though, your graphics in-game while testing look rubbish.

So, what I’ve been struggling to do, and with some degree of success, is using the new canvas object to automatically expand the borders.

Essentially what I do is load the tile set graphic into a canvas. Then I make it an image sheet, making sprite frames from the edge pixels of each tile that I need to expand. Then I :draw() a lot of sprites into the self-same canvas using these frames to offset the tile edges, finishing with an :invalidate(). Then I create another imagesheet from the canvas, which is the normal one I’d use for my tile sets as if I’d done none of this expansion stuff.

So what are my problems? Firstly you *must* do all this edge expansion stuff with the texture min and mag filters set to “nearest” or it just goes wrong. That alone isn’t bad, but it means that any use of the tile set canvas later on will use these filters, which is likely *not* what you want to happen.

Secondly, because you actually want the corner pixels expanded, I’m doing my edge expansion in 2 sets. Firstly horizontal, then vertical. The vertical ones will use the already horizontally expanded source, so they now have these corner things. The problem is that for some reason I can’t just update and invalidate() the canvas horizontally 1 frame after initialising it, then doing the same for vertical adjustments in the following frame after that. It *should* work, and indeed in the simulator it does. But on my device, I need to leave a delay of roughly 700 milliseconds between updates. This isn’t a huge deal, but it means something is going on I’m not aware of, and means you can’t start showing the level until approximately 1.5 seconds after you set it up. Not something I’d want in a publically released library :confused:

Hi Rakoonic,

Very good points. Might I suggest not to forget that you can create a Photoshop action to do the necessary duplicating of the edges - and I even believe that TexturePacker has an option for this, but I’m not sure.

@thomas6 You are correct about TexturePacker. It does have an option called “extrude” that behaves as you describe.

@thomas6 and @rakoonic I am glad to hear that there are workarounds for the gap issue! Presently I have been playing with tilesets created by other people so it seems I am stuck using the 101-102% zoom as suggested by thomas6. Rakoonic, once you get your solution working properly, I hope that you will be sharing it. :slight_smile:

I spent several days on trying to programatically fix the gaps between tiles, instead of requiring photoshop action etc.

I failed, but I did discover some interesting bugs which will be passed to the relevant people :slight_smile:

On the other hand, the splendiferous Vlad from Corona did pass me this gem which has flown completely under the radar until now:

display.setDefault( “isImageSheetSampledInsideFrame”, true )

This will likely fix most of your tile gap problems :smiley: Set it before you load any tile images and / or make sprite sheets from tiles (and don’t forget to reset it afterwards).

Now, it does still have some issues - in particular if you are running your app on a much higher res screen than you generally cater for, or if you zoom the tiles significantly, but those are specific cases (and in the case of high res screens, you should be running x2 or x3 assets to cater for it. For zoomed up pixel art, you’ll still need to manually extrude edges).

But, it helps a ton generally.

Try it, love it, thank Vlad!

Here’s a couple of links so you can see if it works for you or not:

APK: https://www.dropbox.com/s/ir48kkh7an1si2u/RagnarRock.apk?dl=0

OSX Build (no onscreen controls, use cursor keys to move): https://www.dropbox.com/s/jl2ynkuq43r61nm/RagnarRock.zip?dl=0

PS, let me know if it runs at 60fps nicely, and what you think of the slightly semi-trans near layer of parallax (I made it semi-trans because I got annoyed at it blocking the player entirely).