Saturday, October 25, 2014

Making a Simple 2D Physics Engine - Part 2

Hello again! So, right after publishing the part 1 of this three-part tutorial series, it became extremely well-received and known by people (mostly the LÖVE Forums community and the Stabyourself Forums community). In less than a week, it already became the most visualized post of my whole blog! This was great, and I'm really, really, thankful to all of your kindness. It makes me really glad that I'm helping you all! I was going to take a little more of time until the part 2 release, but seeing that people enjoyed these tutorials, I'm already working on this!

As for how long it took me to release the part 2, I'm really, really sorry: I've got a new computer, so I had to transfer the tutorial's files AND install a bunch of programs. But don't worry: this won't happen to part 3...

If you'd like to see more about the LÖVE Forums' post for this tutorial, just CLICK HERE!

Now, without further ado, let's get to the little details of a 2D physics engine: gravity, friction, speed, masks, collision worlds, dynamic status and other little object-specified or global variables and concepts! Since they're mostly simple to explain, I'm putting them all in this single tutorial, instead of making a single tutorial for the topic, like in part 1. Ready?

This is the part 2 of a 3-part tutorial.
Part 1:  Collision detection/handling;
> Part 2: Gravity, friction, speed, masks and other global/local concepts;
Part 3: Drawing objects and optimizing your engine.

So click on "Read more" to go to the tutorial!



So, now that we've got objects positioning, dimensions and collisions mostly done, we should get something else that is essential to objects: movement. Even though we can create movements for objects pretty easily (as shown in an example from the first tutorial), but when it gets to multiple, independently-moving objects, things can get a bit (or a lot) messy. To make up with that, we can set up a very simple way of handling objects' speeds. Remember how in the last tutorial we set up a class for an object, so we could handle functions and variables very easily? Well, I didn't got deeper into that because it wasn't exactly part of the collision-handling system, but more of variable-setting and optimization.


Setting up an object's speed

Setting up functions in objects makes it so you don't need to calculate similar variables for every object, nor make it in a hard-to-understand way. In the last tutorial, I showcased my two favorite methods of easy object creation: by function call and by classes. So let's get to how we'd set the speed variables (and any other variables related to the object) inside objects like the ones shown in the last tutorial:

10 
11 
12 
13 
14 
15 
16 
function newObject(x, y, width, height)
    local self = {}
    
    self.x = x
    self.y = y
    self.width = width
    self.height = height
    self.image = myTexture
    self.friction = .98
    
    --Setting up the speed's default values
    self.speedx = 0
    self.speedy = 0
    
    return self
end


Based on the last tutorial's methods of object creation, it should still be easy to understand how to add these variables to your objects. Unless you're making an object that is already created in motion, all objects should start with their speeds as 0 and 0. The reason why we do this is that, this way, we already define the speed variable and can later set them without having to create them again.

Setting up the speed is easy. Now we just have to make it have an influence over the object!

First of all: we want to make the object move based on it's speed, so we have to understand what each speed does to the object:


Of course, you can (and you WILL) combine both speed axis to make the object move diagonally. But the speed itself won't make the object move: when we're drawing it, we're using the x and y variables, right? So, how to convert speed into position?

The best way to do it is by using dt again. Remember it from the last tutorial? It's what we used for the physics.update function. In resume: if you add dt to a variable with the value of 0 in every update call, after one second this variable will be 1, in two seconds, 2, etc. This variable is used to keep track of time, and isn't it what speed is about? Distance moved in a certain amount of time? So, we just have to move the object for a distance N (which is the value described in the object's speed) for a single second. So basically you're just setting the distance for the object to travel in a single second, or unities per second. It may sound complicated, but it's very, very simple!

function object:update(dt)
    self.x = self.x + dt*self.speedX
    self.y = self.y + dt*self.speedY
end


See? That's all we need! You can set this on the general physics call, instead of doing it per object. This way you make your code more organized and you have less repetitive code to do!

So, on your physics_update function, just set the object's position right after the collision! This is not a rule, you can still set this before, you can experiment with it and see which options suits your needs better!

10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
function physics_update(dt)
    for m, v in pairs(objects) do
        for i, obj1 in pairs(v) do
            --This is the collision checking code!
            for n, w in pairs(objects) do
                for j, obj2 in pairs(w) do
                    if not (m == n and i == j) then
                        local sideBySide = false
                        
                        if checkCollision(obj1, obj2, sideBySide) then
                            -- Detect collision direction
                        end
                    end
                end
            end
            
            --And now, since we're still checking each object, we can put the
            --speed code here instead of making an object loop twice!
            obj1.x = obj1.x + dt*obj1.speedx
            obj1.y = obj1.y + dt*obj1.speedy
        end
    end
end


You're mostly done with the speed, but there are some extra things you can do to improve it:

1. Setting up a maximum speed

This is something good to set up because later, when we set things like gravity, for example, speeds can grow up without control and, even though it wouldn't "break" anything within your game, it'd make objects pass straight through walls or simply go so fast that you can barely control/stop them. So having a limit speed helps you have a bit more control over your object. Plus, it doesn't become overly noticeable, so having a speed limit won't make your objects act unnaturally. Here's the simplest way of doing it:

maxspeed = obj1.maxspeed or 100
--Just an example. You can always set up custom maximum speeds within each object.

obj1.speedx = math.min( math.max(obj1.speedx, -maxspeed), maxspeed)
obj1.speedy = math.min( math.max(obj1.speedy, -maxspeed), maxspeed)


Just to clear up things: I'm doing "math.max" between the object's speed and the negative maximum speed (which would be the maximum speed for going backwards). When doing "math.max", we're getting the biggest value between the speed and the negative maximum speed, so if the speed is lower than the negative maximum speed it'll automatically take the negative maximum speed. As for "math.min", we're checking which one is smaller: the object's speed and the maximum speed. So, if the object's speed is bigger than the maximum speed, it'll be automatically set up to the maximum speed.

In resume: it'll "filter" the object speed, so it is always smaller than the maximum speed.

There's a flaw with this method, though, which is the fact that, if you're moving in the maximum speed both horizontally and vertically, your diagonal speed will be higher than the maximum speed. But a friendly user from the Löve Forums, Ivan, came up with a quick and clever solution for this:

local d = math.sqrt(obj1.speedx*obj1.speedx + obj1.speedy*obj1.speedy)
if d > maxspeed then
    local n = 1/d * maxspeed
    obj1.speedx, obj1.speedy = obj1.speedx * n, obj1.speedy * n
end



2. Stable delta time

This snippet does not only applies to setting up the object's speed, but for any update call you may have in your code. But we'll discuss this deeper in the next tutorial. Basically: set the delta time to have a maximum value (say, 1/60, which would be like one frame of a 60fps animation).

This is great because if your game somehow gets lag (or simply goes slower for some reason), your object will not "teleport" between two points. Why this happens, you may ask: when your framerate is smooth and your computer fast, your delta time will be either 1/60 or less (considering your games run on 60fps, and not 30fps). BUT, if you get lag, it'll be changed to a bigger values (sometimes even half a second, which is a lot in terms of framerate: it'd be 2 frames per second). The game does this to compensate the loss on your framerate, so you'd still get the same values as you'd have without the lag. But this makes your game flicker around, and if one object is moving smoothly, it'll make a huge "jump" in the middle of it's path. To avoid that, we can simply make the delta time have a maximum value, much like in the example above with speeds:

function physics_update(dt)
    dt = math.min(dt, 1/60)
    
    --Set up speed of the object
end


This will make the game seem slower in case of lag, but you'll be just making it smoother. The fault is still in the lag...


3. Resetting speed on collision

This is not something required (if you have maximum speed), but even with that it's VERY recommended that you reset your speed after a collision. This way, if your object have to move over a surface, it won't mess up anything. Or, if your object (the player, for example) have to jump, it doesn't affects it. Resetting speed on collision is so simple that you just need this:

10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
function physics_update(dt)
    for m, v in pairs(objects) do
        for i, obj1 in pairs(v) do
            
            for n, w in pairs(objects) do
                for j, obj2 in pairs(w) do
                    
                    if not (m == n and i == j) then
                        local sideBySide = false
                        local collisionside = checkCollision(obj1, obj2, sideBySide)
                        if collisionside then
                            if collisionside == "bottom"
                                or collisionside == "top" then
                                obj1.speedy = 0
                            elseif collisionside == "left"
                                or collisionside == "right" then
                                obj1.speedx = 0
                            end
                        end
                    end
                    
                end
            end
            
        end
    end
end


And, if you think that you'll need to reset the second colliding object speed, you can just copy the two speed resetting line, paste it right below each and replace "obj1" with "obj2".


Setting up gravity

Now that we've got the speed done, we can jump to it's close friend: gravity. Doing gravity is really easy when you have the speed variables set up: all you need to do is add up to the object's speed Y, so, even if your object is moving up, it'll slow down and fall a little later. The good thing about gravity is that you can set it as a global variable, so you don't need to calculate it for every objects. BUT, if your game requires, you're still able to set it independently. Let's take a look at the best way of doing it:

10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
function physics_load()
    gravity = 800
    --This is one of the best values for gravity in
    --2D games, but you can always change it
end

function physics_update(dt)
    for m, v in pairs(objects) do
        for i, obj1 in pairs(v) do
            --Check collision
            
            if obj1.gravity then --In case you need custom gravity for your object
                obj1.speedy = obj1.speedy + dt*obj1.gravity
            else --Use default world's gravity
                obj1.speedy = obj1.speedy + dt*gravity
            end
            
            --Convert speed to position
        end
    end
end



Setting up friction

If you thought setting up gravity was hard, you may be perplex about how simple it was. That's the charm of having speed variables: they make things easier! And YES, it applies to friction too!

You can have two types of friction, to guarantee that your object will not move endlessly through your game (unless it is set up in space).

The first one: air friction.

Setting air friction is as simple (if not simpler) as gravity:

10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
function physics_load()
    friction = .98
    --In this method, we're gonna be multiplying the object's speed
    --by the friction, so it HAS to be a number between 0 and 1.
    --Numbers bigger than 0.9 are better, because otherwise the object
    --will stop too quickly instead of progressively...
end

function physics_update(dt)
    for m, v in pairs(objects) do
        for i, obj1 in pairs(v) do
        --Check collision
        --Add gravity
        
        if obj1.airfriction then --For custom air friction
            obj1.speedx = obj1.speedx * friction
            obj1.speedy = obj1.speedy * friction
        else --Stick with world's default air friction
            obj1.speedx = obj1.speedx * friction
            obj1.speedy = obj1.speedy * friction
        end
        
        --Convert speed to position
        end
    end
end


And yes: if you ever need, you can also have custom air friction per object. This is great if you have huge objects, so they can fall/move slightly slower, looking more realistic!


Setting up surface friction

Setting up surface friction is also very easy, but it requires a bit more of specification: unless you want to have the same friction effect for every surface, you'll have to specify what collisions have certain frictions and what collisions have others. You can set it either in the physics_update function or individually per object. In the next tutorial we'll talk about custom side collision functions per object, so you'll be able to set that there too. For surface friction, you'll have to calculate it inside the objects loops, so you can be sure that the objects collided to later set the friction effects.

10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
function physics_load()
    surfacefriction = .95
    --Same "rules" as with air friction
end

function physics_update(dt)
    for m, v in pairs(objects) do
        for i, obj1 in pairs(v) do
            
            for n, w in pairs(objects) do
                for j, obj2 in pairs(w) do
                    
                    if not (m == n and i == j) then
                        local sideBySide = false
                        local collisionside = checkCollision(obj1, obj2, sideBySide)
                        if collisionside then
                            if obj2.friction then --For custom surface friction per object
                                obj1.speedx = obj1.speedx * obj2.friction
                                obj1.speedy = obj1.speedy * obj2.friction
                            else --Use default world's surface friction
                                obj1.speedx = obj1.speedx * surfacefriction
                                obj1.speedy = obj1.speedy * surfacefriction
                            end
                        end
                    end
                    
                end
            end
            
        end
    end
end


The cool thing about surface friction is that it is VERY flexible. For example: if you want your player to move through water, you'll most likely want it to fall a bit slower than in air, but even more slower when moving horizontally. So you can have something like this:

10 
11 
12 
13 
14 
15 
function physics_update(dt)
    for m, v in pairs(objects) do
        for i, obj1 in pairs(v) do
            --Detect collision
            
            if insideWater() then
                frictionHor = .9
                frictionVer = .95
                
                obj1.speedx = obj1.speedx * frictionHor
                obj1.speedy = obj1.speedy * frictionVer
            end
        end
    end
end



Collision masks

Differently from what people may think, collision masks are very easy to set up. You're probably thinking about how things related to physics are far more simple than we ever thought, and that's usually the case for most details about it. Collision masks are NOT required, but if you have a more advanced game where you need certain objects to collide ONLY with a said group of objects, or another object to collide with everything BUT some objects, then you'll want to add collision masks.

There are several ways of doing this, you're free to look up after them on the internet (or by looking at some game's source; I really recommend looking at Mari0's code, I learned a lot of things from there). The way I'm going to show this is the way *I* set up this, and I personally think it is much easier to understand, set up and, mostly, read. That's why organizing your code is important: if someone need to look up after something in a certain part of your code (maybe even you!), it'll be better if you can read it more easily. I've seen some collision masks examples almost impossible to understand, so I came up with my own method:



You can have my method or come up with your own. Most masks I've seen only take into consideration objects to filter, never to ignore. So if you want a certain object to collide with everything BUT object N, you'll have to list all the objects and leave object N off.

Another thing about masks that I've seen is that they're mostly not "readable": they often use "codes", or numeric values to represent objects, so you always have to check the object's id for every mask.

My method not only is easier to read/reproduce, but is also very simple to put into action: since in this case we're using bidimensional tables (which means the object will have a name - for example, "box" - and an id - for example, 3), we can set up the objects names and filter them without making any id conversion!

Just one thing, though: for this function (and some others ahead), I'll be using a function that's not present in the Lua source, but that I made myself. It's really small and simple, and it's really useful, so we can avoid doing "for" loops every time we need to find an entry within a table. I got inspiration from a similar function I saw in Mari0's source code, but I adapted it a bit:

function table.contains(t, e)
    for i, v in pairs(t) do
        if v == e then
            return i
        end
    end
    return false
end


And now to our mask-detecting function:

10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
function checkMask(obj1, obj2, obj1name, obj2name)
    local collided = false
    
    if obj2.mask then --Make sure the object HAVE a collision mask
        if table.contains(obj2.mask, obj1name) then
            collided = true
        else
            collided = false
        end
        if obj2.mask[1] == "ignore" then
            collided = not collided
        end
    end
    
    --We check the object 1 mask AFTER, so it has priority
    if obj1.mask then
        if table.contains(obj1.mask, obj2name) then
            collided = true
        else
            collided = false
        end
        if obj1.mask[1] == "ignore" then
            collided = not collided
        end
    end
    
    return collided
end


From that, we can very easily add this to our physics_update function!

10 
11 
12 
13 
14 
15 
16 
17 
function physics_update(dt)
    for m, v in pairs(objects) do
        for i, obj1 in pairs(v) do
            
            for n, w in pairs(objects) do
                for j, obj2 in pairs(w) do
                    
                    if checkMask(obj1, obj2, m, n) and not (i == j and m == n) then
                        --Check collision
                    end
                    
                end
            end
            
        end
    end
end


And that's all you'll need for masks. Wasn't hard to understand, was it?


Collision worlds

Collision worlds are a part of physics in games that is rarely used. Unless you have a big game with several things happening at the same time or if your game involves multiple layers that shouldn't interact with each other, you will not need this. But, since I'm here to show you what you can do, I decided I should also explain collision worlds.

Collision worlds are, on it's root, wider masks. They're used for when you have the same types of objects splitted in several layers, dimensions or worlds. This way, you can have the same objects colliding as they would (using masks) in two different spaces, in a way that you don't need to make several copies of objects to get new masks and that they don't interfere with each other unless you force it to happen.

The way I came up with it was also pretty simple: you "create" worlds by simply adding them to a certain object: you can have strings, numbers or even a table, for having objects that affect two or more worlds at the same time.

Those are types of worlds you can have:

self.world = 1
self.world = 54
self.world = "front layer"
self.world = "back layer"
self.world = {23, "front layer"}
self.world = {"front layer", "back layer", 1, 23, 54}


But, even though they're not often used, collision worlds are not hard to understand: just think of them as a "large mask" that contains several of our already in-use masks. They're very easy to set up (just a single variable per object) and they're also very simple to be detected!

10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
function detectWorlds(obj1, obj2)
    local w1 = obj1.world or false
    local w2 = obj2.world or false
    
    local collided = false
    if type(w1) == "table" then
        if type(w2) == "table" then
            for i, v in pairs(w1) do
                if table.contains(w2, v) then
                    collided = true
                    break
                end
            end
        else
            if table.contains(w1, w2) then
                collided = true
            end
        end
    else
        if type(w2) == "table" then
            if table.contains(w2, w1) then
                collided = true
            end
        elseif w1 == w2 then
            collided = true
        end
    end
    
    return collided
end


It may look a bit complicated, but it isn't: if it weren't by checking similarities between tables (in case you want an object to affect two or more worlds), this code would be even smaller and simpler! I'm just giving you the keys, so you can mold it in your game however you feel like! Now you just need to add this to the object elimination process, like you did with the masks!


Static objects

Making static objects is easier than you might think. Just like most object-related behaviors, all you'll need is a simple, single variable. After you added it, you just need to check, in your collision loop code, if the object you're looking at is static. If it is, you can ignore it while applying gravity, friction, speed, etc. Furthermore, if you already want to make your game run faster, you can simply ignore them completely! That's right, to make static objects work, just make the game pretend they're not there. This way, you won't calculate speed/friction/gravity by default, but you'll also not check collision detection. That's okay in this case, because, since a collision happens between 2 objects, if one doesn't answers it, the other will!

10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
function physics_update(dt)
    for m, v in pairs(objects) do
        for i, obj1 in pairs(v) do
            
            if not obj1.static then
                
                for n, w in pairs(objects) do
                    for j, obj2 in pairs(w) do
                        
                        --Check collision, set speed, apply gravity, etc.
                        
                    end
                end
                
            end
            
        end
    end
end



Conclusion

Once again, if you want to organize yourself a bit more, here's what your code should look like, more or less:

10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
function physics_load()
    gravity = value
    friction = value
end

function physics_update(dt)
    for each object do
        if object is not static then
            for print object do
                
                if objects are on the same mask and on the same world
                    and are not the same object then
                    
                    if objects are colliding then
                        
                        detect collision side
                        push objects away from each other
                        apply surface friction
                        reset objects' directional speed
                        
                    end
                    
                end
                
            end
            
            apply gravity
            apply air friction
            
            filter maximum speed value
            convert speed to position
        end
    end
end



This was all there was for this tutorial, guys! I'm sorry that it took so long for me to release it, but, since I got a new computer, I had to set things up, transfer files from the old computer to this one (including the tutorial files), install programs again (including Notepad++ and Paint.NET, which I use in my tutorials) and a lot of other little tweaks. But don't worry: the next tutorial will come way faster!

Thank you all for reading and thank you so much for your feedback and compliments on the last tutorial, I really appreciate it! I hope I could help you! If you've got any doubt, suggestion, comment, criticism or question, please let me know in the comments of this post or in the LÖVE forums (link at the top of this post). And I see you guys in my next post!

<< PART 1    |    PART 3 >>

No comments:

Post a Comment