These types of games have several challanges - drawing the level is only one of them. MTE and Dusk will happily draw them, and in reality drawing it isn't hard (although drawing it *quickly* is more challenging and fun. MTE and Dusk aren't IMO particularly optimised, but then optimisation is something I really enjoy tinkering with so I'm likely more demanding than others).
So much for drawing
Other parts include things like game logic, enemies, pick-ups etc, but to be honest all of these are relatively trivial issues once you have the next (and in reality, the overall biggest) issue sorted well - collision detection.
It is in this area that I think far too many people (particularly in Corona, for reasons I will detail later) attempt to solve using the inbuilt physics engine. This, to me, is a huge mistake. Here's some of my reasoning:
- Physics engines are inherently unstable (they often take a few seconds to approach stability).
- Physics engines are usually non-deterministic (which means re-running a simulation several times from an identical start state which result in different outcomes).
- Physics engines, while easy to set-up crudely, require a lot of time tweaking to approach what you want.
- They are essentially a black-box - you don't get fine or total control (this may not be an issue for most people, but I *hate* this!).
- Physics engines do not scale well to a (potentially) infinitely large tiled-world (which is likely why you want to use a tile-based engine in the first place).
I will add that I in general *hate* physics engines, so take the above with a pinch of salt
So, what is the solution? Write your own collision detection. This is almost certainly why many people go down the physics engine route. It can appear daunting, and since Corona is marketed as enabling easy creation, the idea that new coders would come in and then be told *not* to use what appears to be a helpful engine is naturally slightly counter-productive. However, I'm reaching out now to people what want to create something more professional, so I'll continue in this nature.
Why would you write your own collision engine (note I didn't say physics engine, for now)?
- Total control over all collisions
- Once it is set up, you don't need to worry about it again (with physics, every time you add something new, it requires more tweaking).
- The nature of tile-based engines means it is extremely well suited to home-brewed collision code.
- It *can* be faster than physics engines (depends on your coding abilities and complexity, naturally).
So, how would you go about it? There's varying degrees of complexity, so I'll start from the simplest and work my way up.
However, there are some choices to be made right from the start, namely what type of collisions do you intend to deal with?
A physics engine automatically deals with solids against solids (whether 2d polygons or 3d models).
In tile-based engines this is possible, but isn't necessarily the fastest nor the most satisfying solution.
Think about your player. Here are some of the more common possibilities:
- Player rectangle against level tiles
- Player point against level tiles
- Player raycasted against level tiles
Rectangles VS level tiles (or other rectangles) collisions is actually trivial in isolation, especially in a platformer where we almost certainly would use just AABB (axis-aligned bounding boxes). This means your rectangles won't rotate. This simple limitation makes this *very* simple to code. It is, however, more complicated in that it results in more collisions to process and is slower as a result.
Points VS level tiles (or other rectangles) is the simplest there is. However, it is often impossible to reduce larger objects like players and enemies down to a single point, meaning the collision results won't be very satisfactory.
Raycasts VS level tiles etc is what I use (a technique shamelessly stolen from Sonic the Hedgehog!). It is an extension of points VS level tiles, and allows for mimicking volumes. I personally am only allowing for AA rays (axis-aligned) for simplicity and accuracy, btw. It can get complicated with large volumes, so isn't recommended for everything.
It should also be noted that these engines generally don't handle arbitrary directions of movement. They would break each movement down into individual movements in the X and Y axis. This is more accurate and vastly simpler.
OK, so here's 2 approached of differing complexity.
1) The basics. Tiles are either solid or not.
If your tiles are (say) 20 x 20 pixels, then this means any time a point or rectangle crosses a 20 pixel boundary (x % 20 == 0) either horizontally or vertically, you need to check for the new tile you are entering. Likely using a lookup table (eg tiles using frame 20 have a list of properties including solid or not), you'd check if all the newly entered tiles are solid, and if so, move whatever entered it back by precisely enough pixels so that it no longer is colliding. If you are doing rectangle VS rectangle collisions, don't forget to check the leading edge, so if moving right, check the right edge of the moving rectangle against the tiles, if moving down, check the bottom edge etc. So you might have something like (this is an example only for horizontal movement *to the right* - for movement to the left you'd swap the signs on some of the maths):
local x, y, dX = player.x, player.y, player.dX
local halfWidth = player.halfWidth
x = x + dX + halfWidth -- You check the right edge of the player, plus the amount you want to move
local tileX, tileY = math.floor( x / 20 ), math.floor( y / 20 ) -- Assuming tiles of 20 by 20 pixels
local offsetX = x % 20 -- This is how far into the given tile you have gone horizontally
local tileToCheck = map[ tileX + 1 ][ tileY + 1 ]
local thisTileProperties = allTilesProperties[ tileToCheck ]
if thisTileProperties.isSolid == true then
x = x - offsetX -- There was a collision so we move you backwards to the edge
player.x = x - halfWidth -- Set the player x to this value (removing the half width from earlier)
2) Slopes etc.
This gets to be more fun. You need an additional data structure that represents the shape of the tile. I, for simplicity, simply have a linear table defining the height of the ground (or ceiling as well if you need that - I allow a tile to be either ceiling or floor, but not the same at the time). In this case the collision needs to look up which pixel you are referencing, for which you would use the offsetX value. In my case it'd be something like:
local thisTileProperties = allTilesProperties[ tileToCheck ]
local floorHeight = thisTileProperties.floorHeights[ offsetX + 1 ]
Now, you can respond to this by moving backwards out of the tile like in the first bit of code, but this wouldn't factor in if the floor is lower generally than the point checked. This is why I use raycasting rather than a simple 'is this point colliding?'.
If you start from X, and you move dX pixels right, I'd check *every* pixel from X + 1 to X + dX, so you find exactly where the collision is - pixel-perfect.
So how do you turn the above into a collision engine that handles psuedo-solids like sonic the hedgehog? Here's how:
The player X and Y are basically the center of the object. We also have the player's half width and half height. This enables us to find the edges. So, here's the fun part
1) Cast a ray horizontally in the direction of movement, and resolve that collision.
2) If jumping, cast a ray vertically up to see if your head hit anything - resolve that collsion.
3) If falling or simply not jumping up, cast a ray vertically down either by the rate of falling, or by your gravity strength + plus an amount equal to how well you want your player to move over uneven terrain. If there is a collision, resolve it and mark your player as standing on the ground. If there is no collision, then mark your player as falling if not already, and react accordingly.
That's pretty much it. I use a slightly more complicated method using two vertical down rays to enable me to make the player object respond to uneven terrain (his his feet follow it more accurately, and to find if you are balancing on the edge of a tile, for example).
Note you can mix and match. I do my more complicated check *only* for the player - enemies get a simpler check (only 1 horizontal and vertical ray each), to keep speed up.
OK, so why do I talk seperately of physics engines and collision engines? Because in general, people use physics engines for collision. Touching pickups / enemies and things like jumping with gravity are trivial problems to solve, but collision is not (although not as hard as you may fear!) So - the core issue for me is creating a solid collision engine, and you write your own simple physics over the top of that, and that really isn't as big a deal as you'd think, because it essentially boils down to how you move your player.
If you want acceleration in player speed, maintain an acceleration property and increase that every frame the player wants to move in the same direction, reducing it when not pressed. Add this to the speed, instead of doing something like speed = speed + 5.
If you want jumping / falling, you need a property like isOnGround, gravity, fallingSpeed (negative for up, positive for down most likely) and fallingAcceleration. Let's say you are standing on the ground and want to jump, you'd first check if isOnGround == true, and then set fallingAcceleration to whatever negative value you want, and then add this to fallingSpeed, which you then add to the player's Y. Don't forget to mark isOnGround to false.
Then in subsequent frames, change fallingAcceleration by adding the gravity variable, so you curve in the air and begin to fall. As long as you remember to check below the falling player and set isOnGround when a collision occurs, all is good.
The two above cases will cover 99% of all player collisions with the level - just about everything can be broken down into either a horizontal or vertical movement, or both (one at a time).
It really doesn't require more.
OK so for now that's all I'll write, maybe later I can comment on things like enemies and pickups etc. If interested, let me know what you'd like to read about