Friday, December 12, 2014

Making a Simple 2D Physics Engine - Part 3

Hello again, guys! I know, once again, this took a while to release, based on the release of the last part, but this time it wasn't due to having a new computer, like last time: it was delayed due to pure laziness and procrastination. So sorry about that...

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

Although this is the "official" last part of this series of physics tutorials, I want to make smaller, more informal tutorials for different things in a physics engine, like slopes, player movements, triggers, etc. But, without more delay, let's move on to this part of the tutorial!

This is the part 3 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!




Drawing your object

If you're already here, that means that you've most likely set up everything you need for a physics engine, except for drawing the objects. If you're familiar with LÖVE or Lua, you've most likely figured out how to do that, but I still want to show different ways of doing this, in order to make it more efficient or more automatized. The most basic thing you can do to draw your object is by simply adding an image and positioning it wherever the object is:

10 
function myObject:init(x, y)
    self.x = x
    self.y = y
    self.image = love.graphics.newImage("myObject.png")
end

function myObject:draw()
    love.graphics.setColor(255, 255, 255, 255)
    love.graphics.draw(self.image, self.x, self.y)
end


This will make the image follow the object only, without taking into consideration the object size, the image size or the offset (if the image should be centered, for example). Even though this is ideal for most cases (you'll most likely make both the object and its texture the same size), there are some cases where you'll want the object's hitbox to be smaller than its image, or vice-versa. And, unless it is intended for you, your image will put its excesses to the bottom and right:


And, in most games, this won't look good, considering that, if your game has gravity, for example, this object will appear to be inside the ground. So you'll have to make a bit of math to position you object's image better in a given hitbox area. There is no fixed rule or ideal method in this case, since it varies a lot depending on what you want, but here's a little function that you can use:

10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
function myObject:draw()
    love.graphics.setColor(255, 255, 255, 255)
    
    local x = align("center", self.x, self.width, self.image:getWidth())
    local y = align("end", self.y, self.heigh, self.image:getHeight())
    love.graphics.draw(self.image, x, y)
end

function align(mode, pos, objSize, imgSize)
    local mode = mode or "center"
    --modes can be "start" (top or left), "center" or "end" (bottom or right)
    
    if mode == "center" then
        return pos+objSize/2-imgSize/2
    elseif mode == "end" then
        return pos+objSize-imgSize
    else --if it's "start" or not, it's the same
        return pos
    end
end


The function requires 4 variables: mode, which tells the function what alignment you want (start, center or end); pos, which is the object's position (not the image position!); objSize, which is either the width or the height of the object, depending on what axis you want to align; and imgSize, which is either the width or the height of the image (again, based on the axis you're trying to align). This function will return a number, which will be used as the position of the image. To make it clearer, here's a diagram:


For most types of game, the 8th square (counting left-to-right, top-to-bottom) would be the most appropriate, since it centers the image only in the x axis, while keeping the image over the object's hitbox (so the image doesn't gets over the ground).

Another thing you may want to do is resize the image: in case you always want the image to fit in its hitbox, this is what you'll have to do:

function myObject:draw()
    love.graphics.setColor(255, 255, 255, 255)
    
    local scaleX = self.width/self.image:getWidth()
    local scaleY = self.height/self.image:getHeight()
    love.graphics.draw(self.image, x, y, 0, scaleX, scaleY)
end


In the case of scaling, at least, there's just this method. In Löve, you can scale an image based on a percentage, thus we need to divide the object's size by the image's size (if it's equal, it'll return 1. If the object is smaller, it'll return a number smaller than 1. If bigger, a number bigger than 1). If your framework/coding language uses the width on it's own, instead of percentage, you can simply use the object's dimensions.

For objects that are drawn often or are based off a spritesheet, I highly recommend using SpriteBatch. It's not so hard to understand, makes your game much faster and you get the same effects. If your coding language/framework can't support them, it's okay, your code will work normally without them.


The next thing I'd recommend is checking if an image is on screen and only drawing it if it is. You can use canvases to have this, but my last computer couldn't support them, so I never studied them very much. Also, if you can make your game without canvases/shaders (or making them only optional), it'd be a lot better for you, since a lot of people have computers that still can't support them (like I did). Another thing that you could do, though, is checking the position of the object. Let's consider your game screen to be 800x600 (the default window size in the Löve framework). Let's also consider that the camera is following the player. So, in this example, the camera is at x:200 and y:100.

function love.load()
    camera ={}
    camera.width = 800
    camera.height = 600
    camera.x = 200
    camera.y = 100
end


After that, it's easy to spot which objects should be drawn or not. In this case, I'm considering ONLY the object's position, so if its texture is bigger than its hitbox, this may not be the solution for you. It shouldn't be hard to fix it for that case, though, but it'll take a bit more of math...

10 
11 
function physics_draw()
    for m, v in pairs(objects) do
        for i, obj1 in pairs(v) do
            if obj1.x+obj1.width > camera.x and obj1.x < camera.x+camera.width then
                if obj1.y+obj1.height > camera.y and obj1.y < camera.y+camera.height then
                    obj1:draw()
                end
            end
        end
    end
end


We're pretty much done with the drawing part, but here are some little hints for you:

  • Avoid big images, as they take too much memory, and some computers can't load big images in games. If you can, make the image as small as possible, or less detailed. If not, split the image into segments and draw they all together.
  • If you want to draw an image bigger, enlarge it in the program itself, not in the image file!
  • Load only images you're gonna use. Loading several images and not using some just take up space.
  • While dealing with repetition (several similar tiles, for example), SpriteBatches are the best option.
  • If an image can be hidden in a certain moment, do it. Images that don't appear don't need to be drawn, right?
  • Look up for images in loops. Unless they are really necessary, don't draw an image more than once.
  • In some cases, you can not draw an object if it behind another one. This is a bit hard, since most times it requires some tricky coding to get, but if you have a really big objects and entities that go behind it, it is preferable that you don't draw the entity behind.

Now that we've got drawing out of the way, let's move on to some things you can do to optimize the "physics" part of your engine.


Delete unused objects

May sound a little obvious, but many people have problems with it. Not that deleting objects is hard, but they sometimes overlook it and keep trying to get a source of lag that they're simply ignoring. Deleting objects is important because it gives you more memory to work with. Plus, it's always good to "clean up the trash", if you know what I mean.

Sometimes, especially for entities like players or enemies or allies, you have a "dead" variable, to confirm the entity is dead and should not interact or be interacted with. But this alone won't make it not count. So a good way of getting rid of entities is on the loop you already have for entities, in your physics.lua file:

10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
function physics_update(dt)
    local delete ={}
    --It is always good to have a "to delete" table
    for m, v in pairs(objects) do
        for i, obj1 in pairs(v) do
            if obj1.dead then
                if not delete[m] then
                    delete[m]={}
                    --'m', in this case, is the name of the object "class".
                    --So if you have a box in objects["box"], 'm' will be "box"
                end
                table.insert(delete[m], i)
            else
                --collision detection, gravity, speed, etc.
            end
        end
    end
    
    for m, v in pairs(delete) do
        for i = #v, 1,-1 do
            --What we're doing here is run a 'for' loop backwards.
            --That means we're checking it's items from the last to the first,
            --which makes deleting more efficient.
            
            table.remove(objects[m], v[i])
        end
    end
end


Another good thing you can do that may be helpful is deleting entities out of the map. In the case of an entity getting outside the map's range, you can always teleport it back, but if you don't care about bringing it back, simply do the same as the code above, but instead of checking for the "dead" variable, check for the entity's position; if it's outside the map, include it in the delete table.


Ignoring unnecessary collisions

Checking several collisions every frame takes a bunch of memory: the more objects you have, the more you need to check for collisions. In some cases, though, if you have a lot of objects and, even after using masks, collision worlds or ignoring static objects you're still having lag, here's a little trick for you: ignore certain collisions. For example: if the object only collides with tiles and you, for example, but you're not even close to it, why check every tile plus you? Make it static until you've got really close - it'll have the same results, but without the whole checking thing.

Another thing: if you have a tile/grid based game and don't wanna check for collisions on those hundreds of tiles around, you can only check for tiles around the player/object! There's no predefined way of doing this, since it varies for each game you make, but here's a quick way with most games:

10 
11 
12 
13 
14 
15 
16 
17 
function checkTiles(obj)
    local size = 16 --If your grid is not 16x16, change this
    
    --Top-left corner tile
    local p1x, p1y = math.floor(obj.x/size)+1, math.floor(obj.y/size)+1
    --Bottom-right corner tile
    local p2x, p2y = math.floor((obj.x+obj.width)/size)+1,
                     math.floor((obj.y+obj.height)/size)+1
    
    --If your grid starts on tiles 1-1, add the "+1". If not, remove them.
    
    for x = p1x, p2x do
        for y = p1y, p2y do
            checkCollision(obj, tile[x][y])
        end
    end
end


In this case, you can totally ignore checking collision with tiles in the physics updates, since all the necessary tile-checking is here. Also, you'll only call this function with objects that DO collide with tiles. You may also adapt this to your code, not only in the grid size, but also in the way tiles are stored. If you don't have a "tile" table, just get the most convenient way for you to associate tiles with points in the grid.



Conclusion

I know this doesn't look like much, but there isn't really much to be done in optimizing: it REALLY varies on how your game was made or how things in general work. Besides the things I've already said, here are a few tips:

  • Ignorance is powerful! The more you can ignore objects, the faster your game will run!
  • Invisibility clothes! If your object doesn't need to be drawn, why would it be?
  • Trim out the fat! If deleting something doesn't affect the game, it's because it isn't needed.
  • The size matters! If you can do smaller loops between objects, it'll save you a lot of memory!
  • Repetition is bad! I couldn't find a good pun for this, but really: if you're making the same things in different places, make them all fit in a single one! If you need to do something for every object of a certain type, include it in the main physics loop instead of creating a new one!
  • Simple enough! If something can be simplified or reduced without a significant quality loss, it'll be a lot better! Although quality is a common goal for every game, if you have to choose between graphical/rendering quality and gameplay quality, always choose gameplay. Players emerged in the game's mechanics or focused on the game's objectives will not care for details that were reduced or simplified!

I hope you guys enjoyed this tutorial, even though it was shorter and delayed! It may not look like, but I had to put a lot of thought into this part, since optimization can sometimes be tricky (in some cases, it may even cause more lag than the unoptimized version of your game). Also, it is something very, very relative: it can totally work in some games and be completely useless in others.

Also, a little hint I've read from game-makers before: procrastinate optimization! Optimization must not be your main goal! You must optimize your game only after you've finished everything and, even then, only do it if it has some significant impact: if your game runs perfectly fine with a great framerate and just a little of memory usage, why even bother optimizing?

Remember: the key for having a successful game is not having the best graphics, rendering options, sounds, etc., but being the most immersive and entertaining as possible! You don't play Minecraft or Mari0 because of their super realistic graphics, do you?

For the ones that need a little help with organization, here's our little code friend:

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 
35 
36 
37 
38 
function physics_load()
    gravity = value
    friction = value
end

function physics_update(dt)
    for each object do
        if object is dead or out of the map then
            delete object
        else
            if object is not static then
                for each 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
end


That's it for this tutorial then! Thanks a lot for reading, sorry (again) for the delay, and I see you in my next post! Keep tuned, because smaller tutorials for specific physics features may come anytime!
Also, a small program/game related to this tutorial series is also being developed. It'll let you test how certain methods work in the action (yes, multiple ways of handling physics in a single game!). As soon as it is finished I'll make a new post, but still, keep tuned!

<<PART 1    |    << PART 2

No comments:

Post a Comment