Lecture 6

Welcome!

  • Hello, world. This is CS50 2D, Lecture 6. Today, we’re going to take a break away from the older NES titles, even as pleasant and nostalgic as they are for me. We’re going to venture back towards more modern mobile-oriented games. In particular, which we looked at with Match 3 previously, Candy Crush being a good example of that.
  • Angry Birds is an example of another franchise that is very popular. This is an example from one of the mainline titles like Angry Birds One or Two. Originally, it wasn’t quite as large as this (this looks like it was probably done on a tablet) but it encompasses the overall gist of the game.
  • Essentially, you’re controlling this slingshot here, which has a bunch of these angry birds as the name of the game suggests, that are trying to get revenge on these pigs that have started to build forts to take over their territory. Not entirely sure of the exact lore behind the game, if any such exists. But the pigs are blocked by these obstacles here made of various materials: Wood, glass, ice, metal. You have varying types of birds that you can use to affect damage onto these structures and ultimately the pigs. If you’re able to take out the pigs with a certain number of birds or without depleting your limited birds, you can accomplish getting higher scores for that level in the form of stars, typically.
  • There are many iterations of the franchise. There’s a Star Wars one, and I’m not entirely sure how far the franchise has gone in recent days. I know they even have TV shows and the like. Very popular franchise, one of the first mobile series I ever played. Really big fan on iOS, especially in the early days of the platform.
  • This allows us to really look at physics in a unique way that we haven’t really looked at in games thus far. We’ve done everything ourselves with simplistic overall physics calculations: Velocity, manipulating positions directly, approximating collisions and bouncing and whatnot. But Box2D is this library that LÖVE 2D embeds within it. Box2D is a separate library that you can actually embed in any compiled framework or use in any situation that you want to. It’s just a set of code, a set of functions for modeling physical interactions. It’s very popular, and LÖVE 2D exposes it as a set of wrapper functions that essentially defer to the underlying Box2D functions that you might normally expect to see in something like C or C++.
  • Today, we’ll be looking at Box2D as our main focus with a bunch of various underlying parts, including bodies, fixtures, shapes, and joints. Then we’ll also be looking at (because we’re dealing with Angry Birds, which is a mobile game that has this mouse or finger touch-based input in order to drag the bird on the slingshot and then release it) how to actually use mouse movement with X and Y and track key released and key pressed and so on and so forth.
  • Playing the game, click and drag. It does detect if you are going to shoot the bird to the left on this example. As you can see, very nice! You’re able to defeat the obstacles. It does detect collisions, and then the boxes will break if they hit your alien or there’s a velocity calculation between the two objects that’s past a certain threshold. Then it restarts after a certain period of time.
  • Our goal is to approximate Angry Birds with a very simplistic version. But nonetheless, we do have an alien creature here. We’re using a Kenny open-source Creative Commons tile set that allows us to accomplish the same thing, but with a different aesthetic. We’re using an alien aesthetic as how they term it in the art pack. We have this alien here, a circle, which is our character. Then we have a square-based enemy alien there in the obstacles. These obstacles will be able to be deleted by colliding with them, and then they’ll break, thereby exposing the alien on the right, which after a certain collision, or should the obstacle collide with the alien itself, the alien will be defeated, removed from the game world, and then we will achieve victory in our play state.

The Box2D World

  • The very first thing we should begin to talk about with Box2D is this idea of the world. Box2D is ultimately just a rigid body physics simulator of sorts, where everything that exists in the world is this rigid defined body, this entity that takes a shape and can collide with other entities. There are various coefficients and multipliers for various things. You can perform impulses on things and get angular velocity and linear velocity for things and damping and so on and so forth.
  • These things allow you to do all kinds of very complicated physics, realistically modeled without having to necessarily know every single thing about the math to do it. It empowers people to, beyond that, also just create very interesting assortments of shapes that model physical, more complicated engineering projects that allow you to do very complicated things, which we will only briefly allude to. For example, you could get very creative and create things like cars and automated systems and pulleys and whatnot. They do all sorts of really sophisticated things.
  • But the place that we begin is the world, and that’s where the physics actually gets simulated. There’s this overarching container. We’ve thought about it in our own way with play states and levels and dungeons and whatnot. You can think of the world as this Box2D container for all of your bodies in the world, which we’ll look at in a moment. The world’s job is to update periodically: You pass it in dt, just like you would pass in dt to update anything. And it will actually take charge of performing all of the collision detection, including continuous collision detection for very fast moving things to make sure that they don’t clip through each other.
  • The function that you would use to create this new world would be love.physics.newWorld. You would pass in gravity. Remember that we did have gravity in our own world with Mario: It had a positive Y value that would apply to anything that was in the world, and that would affect things moving downwards unless they hit the ground. Box2D takes care of that for you, but you pass that at world time and you can set an X gravity and a Y gravity.
  • If you are doing a top-down game, for example, you might not have either of those set to anything, and you might instead be doing everything without gravity but still performing physics calculations in some 2D plane, which is totally possible with Box2D.
  • Consider from static/main.lua:

    -- new Box2D "world" which will run all of our physics calculations
    world = love.physics.newWorld(0, 300)
    

    Notice that we pass 0 for X gravity and 300 for Y gravity. LÖVE 2D’s physics space has a 30 pixel per meter approximation for gravity. If you assume 9.8 meters per second squared, 9.8 times 30 is almost 300. We just round up to 300 to apply roughly Earth-like gravity if we assume the 30 pixel per meter approach. That’s a tunable parameter just like any else.

Bodies

  • We know we have this simulator that’s going to exist, that’s going to perform all these calculations for us, all this physics that we don’t necessarily have to worry about as much, at least in terms of the nitty-gritty. But we do need to actually have things that can operate in this physical space. The first step towards that is defining a body.
  • What a body is, is essentially this abstract container that can hold these things called fixtures that apply shapes. The body’s job is to essentially take those physics calculations and then perform just basic mechanics without a shape, without any idea of mass or any particular friction or anything. It’s this invisible thing that can have physics applied to it, almost like a particle, but it can be affixed. It can have fixtures affixed to it that therefore influence its shape and therefore allow it to perform collisions with other entities.
  • The bodies are like the nucleus behind all of the things that actually interact in your world. They’re the things that you build pieces on top of, fix things to, to create behavior in your world space.
  • To create a body, you’ll call love.physics.newBody, which takes in the world itself so that the world has a reference to it, and then an X and Y, and then a type. Consider from static/main.lua:

    -- body that stores velocity and position and all fixtures
    boxBody = love.physics.newBody(world, VIRTUAL_WIDTH / 2, VIRTUAL_HEIGHT / 2, 'static')
    

    Notice that we pass in the world that we defined, the X and Y position (center of the screen), and the type. Everything in Box2D is relative to its center, unlike LÖVE 2D’s default where everything is relative to its top left corner. There is a little bit of difference in how things should be rendered and how physics should be looked at in Box2D versus the AABB stuff that we’ve looked at so far. But it’s a relatively simple shift. If anything, it’s simpler in a lot of ways thinking in terms of the center points of objects.

Body Types

  • The type is very important. There are three different types of bodies: static, dynamic, and kinematic.
  • A static body: You can think of something that literally will just not have physics essentially applied to it. It’s statically there. Its position is fixed. It doesn’t do anything to anything else, really. It can still affect things in the sense that it can still have physics applied to it and things can bounce off of it, but it will itself not be affected by other objects. And it cannot move. It is fixed. It will not change.
  • A dynamic body will actually move and perform bounces off of things and affect other things. It is, by its nature, a dynamic body in the world, the opposite of a static body.
  • A kinematic body is similar to a static body in that it can move around and influence behavior, but it does not get the usual behavior affected towards it. It cannot be influenced by other objects colliding with it. It isn’t affected by gravity. You might have something like a moving platform that is kinematic and actually affects other objects should they collide with it. But it itself is not being affected by gravity: It is not falling like a dynamic body would.
  • If we run the static example, we see a square being rendered right in the middle of the screen. It’s not doing anything, it’s not really all that different from the Pong paddle or ball that we created in week zero.
  • If we run the dynamic example, rather than the static rectangle being fixed to the middle of the screen, we start to see the equivalent of when we started applying gravity in prior examples, being applied and done for us by LÖVE 2D’s world. Consider from dynamic/main.lua:

    -- body that stores velocity and position and all fixtures (now dynamic)
    boxBody = love.physics.newBody(world, VIRTUAL_WIDTH / 2, VIRTUAL_HEIGHT / 2, 'dynamic')
    

    Notice that we’re just passing in the string 'dynamic' to the constructor for a new body instead of 'static'. We get all of that functionality (all the gravity, everything) for free.

  • For kinematic bodies, when I think of a kinematic body, I think of many obstacles in games or things moving around that seem to affect other objects, but themselves are defying the laws of physics. If you were to think about the paddles in Pong, those are kinematic bodies that can have their positions directly affected, but themselves are not affected by gravity.
  • Consider the kinematic example with three spinning rectangles from kinematic/main.lua:

    -- table holding kinematic bodies
    kinematicBodies = {}
    kinematicFixtures = {}
    kinematicShape = love.physics.newRectangleShape(20, 20)
    
    for i = 1, 3 do
        table.insert(kinematicBodies, love.physics.newBody(world,
            VIRTUAL_WIDTH / 2 - (30 * (2 - i)), VIRTUAL_HEIGHT / 2 + 45, 'kinematic'))
        table.insert(kinematicFixtures, love.physics.newFixture(kinematicBodies[i], kinematicShape))
    
        -- spin kinematic body indefinitely
        kinematicBodies[i]:setAngularVelocity(360 * DEGREES_TO_RADIANS)
    end
    

    Notice that we create three kinematic bodies and set their angular velocity to perform a full spin per second. Most of the functions in Box2D take radians as their unit for rotation, so we multiply by DEGREES_TO_RADIANS to convert from degrees.

  • In the kinematic example, we have a green dynamic rectangle that falls down, gets bounced around by the spinning blue kinematic rectangles, and eventually lands on the red static ground. The blue bodies are not falling, they’re not affected by gravity, but they do impart force onto the green body. The dynamic rectangle was affected by those rotations, affected by the vertices of those rectangles applying some force to it. Then it fell to the bottom of the screen and came to rest after friction.

Fixtures and Shapes

  • We have a body. We can move it around or know that it will apply physics or not be applied physics to in our world. But on top of that, you need to have some shape. You need to have some idea: How much friction does this body have? How does it actually affect things? What does it look like? What are its edges?
  • To do that, it needs to take a shape. To take a shape, it needs this thing called a fixture. A fixture will essentially literally be. You can envision a car which has this nucleus, and then you can imagine the chassis being the first fixture on the car and then having more fixtures for the wheels. Now you’ve got places where you can start to put wheels and build up this object with shapes. But you need places to actually put those shapes. That’s what fixtures allow you to do.
  • After you define a fixture, you can then decide to put various kinds of shapes onto that object to give it form, to allow it to actually collide with things in the way that a rectangle would or a circle would.
  • Consider from static/main.lua:

    -- shape that we will attach using a fixture to our body for collision detection
    boxShape = love.physics.newRectangleShape(10, 10)
    
    -- fixture that attaches a shape to our body
    boxFixture = love.physics.newFixture(boxBody, boxShape)
    

    Notice that we create a rectangle shape of 10 by 10 pixels, then create a fixture that attaches that shape to our body. It will know, based on whatever shape that we pass to it (if it’s a circle, if it’s some other shape) how to actually calculate all of the physics to properly affect things interacting and colliding.

  • There are various shape types in Box2D:
    • love.physics.newRectangleShape(width, height) - rectangular collision shape
    • love.physics.newCircleShape(radius) - circular collision shape
    • love.physics.newEdgeShape(x1, y1, x2, y2) - line segment for walls/ground
    • love.physics.newPolygonShape(...) - arbitrary polygon with any number of vertices
  • Edge shapes are really nice for borders. The ground in Angry Birds is also an edge shape. If you have borders in your game, they’re often very well approximated with edge shapes, they don’t really need depth, they’re just a hard barrier for things. Consider from ground/main.lua:

    -- static ground body
    groundBody = love.physics.newBody(world, 0, VIRTUAL_HEIGHT - 30, 'static')
    
    -- edge shape Box2D provides, perfect for ground
    edgeShape = love.physics.newEdgeShape(0, 0, VIRTUAL_WIDTH, 0)
    
    -- affix edge shape to our body
    groundFixture = love.physics.newFixture(groundBody, edgeShape)
    

    Notice that we create a static body for the ground and attach an edge shape to it.

  • Fixtures also have properties you can set:
    • Restitution is like bounciness. The closer to zero, the less you bounce. The closer to one, you essentially preserve your bouncing perfectly, there is no lost energy effectively.
    • Friction affects how much things slide and how much angular spin is applied on collision.
    • Density affects mass. Higher density means heavier objects.
  • Consider from ground/main.lua:

    -- make the falling box bouncy
    boxFixture:setRestitution(0.5)
    

    Notice that setting restitution to 0.5 means the box will retain half its energy on each bounce.

  • To render Box2D shapes, we can use love.graphics.polygon. Consider from static/main.lua:

    -- draw a polygon shape by getting the world points for our body
    love.graphics.polygon('fill', boxBody:getWorldPoints(boxShape:getPoints()))
    

    Notice that body:getWorldPoints(shape:getPoints()) returns all of the XY pairs that equal the polygon’s shape for drawing to the world. This handles rotation automatically.

Mouse Input

  • Because we’re dealing with Angry Birds, which has this mouse or finger touch-based input in order to drag the bird on the slingshot and then release it, we need to look at how to use mouse movement.
  • love.mousepressed(x, y, button) fires when the mouse button is pressed. It takes an X and Y (where you clicked) and the button that was pressed.
  • love.mousereleased(x, y, button) fires when the mouse button is released. This is when we want to finally release the bird and send it on its way.
  • If you press the mouse at an XY and then maintain a state of “now we’re clicking and dragging,” you can take that XY and hold onto it. This is what we do with the slingshot. As you’re clicking and holding the bird on the left and moving it around, it’s manipulating the trajectory. Once you release, we launch the bird and have it perform the collision should it hit something.
  • In our implementation, we use love.mouse.wasPressed(1) and love.mouse.wasReleased(1) (where 1 is left click) to detect these events. Consider from angry50/src/AlienLaunchMarker.lua:

    -- if we click the mouse and haven't launched, show arrow preview
    if love.mouse.wasPressed(1) and not self.launched then
        self.aiming = true
    
    -- if we release the mouse, launch an Alien
    elseif love.mouse.wasReleased(1) and self.aiming then
        self.launched = true
        -- spawn and launch alien...
    end
    

    Notice that clicking starts aiming mode, and releasing fires the alien.

Collision Callbacks

  • So far, if we pay attention to ball pit or the other examples, all of these things are moving around and they’re staying existent and they’re bouncing and doing all these things, but none of them are disappearing. That is an important detail with Angry Birds. As you’re playing and throwing the bird from one place to another, depending on how fast it’s moving, it might destroy the obstacle or the pig.
  • In Box2D, there are a set of callbacks that will fire when collisions occur. We can define these callbacks not all too dissimilar from the callbacks we can define for LÖVE’s main functions like keypressed or mousepressed, except they have different points at which they occur during a collision:
    • beginContact - when two objects start touching
    • endContact - when two objects stop touching
    • preSolve - before collision response is calculated
    • postSolve - after collision response is calculated
  • We only need beginContact in today’s example. Consider from angry50/src/Level.lua:

    -- define collision callbacks for our world; the World object expects four,
    -- one for different stages of any given collision
    function beginContact(a, b, coll)
        local types = {}
        types[a:getUserData()] = true
        types[b:getUserData()] = true
    
        -- if we collided between both the player and an obstacle...
        if types['Obstacle'] and types['Player'] then
    
            -- grab the body that belongs to the player
            local playerFixture = a:getUserData() == 'Player' and a or b
            local obstacleFixture = a:getUserData() == 'Obstacle' and a or b
    
            -- destroy the obstacle if player's combined X/Y velocity is high enough
            local velX, velY = playerFixture:getBody():getLinearVelocity()
            local sumVel = math.abs(velX) + math.abs(velY)
    
            if sumVel > 20 then
                table.insert(self.destroyedBodies, obstacleFixture:getBody())
            end
        end
    end
    
    -- register just-defined functions as collision callbacks for world
    self.world:setCallbacks(beginContact, endContact, preSolve, postSolve)
    

    Notice that beginContact receives fixtures a and b for the two colliding objects. We get their user data to determine what types they are, check the player’s velocity, and if it’s high enough (greater than 20 meters per second combined), we flag the obstacle for destruction.

  • Important: LÖVE 2D does not want you to destroy bodies during the actual callbacks as they’re being processed because of the way it internally does physics calculations. You want to do all the removals after all of the callbacks have resolved. Otherwise, you run into issues that can crash the library: Stack overflows and the like. This is documented behavior. You typically want to do all your deletions, not in your callbacks, but in some cleanup post-stage. Consider from angry50/src/Level.lua:

    -- bodies we will destroy after the world update cycle; destroying these in the
    -- actual collision callbacks can cause stack overflow and other errors
    self.destroyedBodies = {}
    
    -- destroy all bodies we calculated to destroy during the update call
    for k, body in pairs(self.destroyedBodies) do
        if not body:isDestroyed() then
            body:destroy()
        end
    end
    
    -- reset destroyed bodies to empty table for next update phase
    self.destroyedBodies = {}
    

    Notice that we collect bodies to destroy during callbacks, then destroy them all after world:update(dt) completes.

User Data

  • To differentiate between types of objects in collisions, we use user data. Fixtures can hold metadata so that your objects can do things like flagging “this thing is an enemy versus this thing is my player.”
  • fixture:setUserData(data) attaches any piece of information to a fixture.
  • fixture:getUserData() retrieves that information during collision.
  • Consider from angry50/src/Alien.lua:

    function Alien:init(world, type, x, y, userData)
        self.world = world
        self.type = type or 'square'
    
        self.body = love.physics.newBody(self.world,
            x or math.random(VIRTUAL_WIDTH), y or math.random(VIRTUAL_HEIGHT - 35),
            'dynamic')
    
        -- different shape and sprite based on type passed in
        if self.type == 'square' then
            self.shape = love.physics.newRectangleShape(35, 35)
            self.sprite = math.random(5)
        else
            self.shape = love.physics.newCircleShape(17.5)
            self.sprite = 9
        end
    
        self.fixture = love.physics.newFixture(self.body, self.shape)
        self.fixture:setUserData(userData)
    end
    

    Notice that we pass in userData to the constructor and call setUserData to attach it to the fixture. This allows us to later identify what type of object this is during collision.

  • When creating objects in the level, we pass different user data strings. Consider from angry50/src/AlienLaunchMarker.lua and angry50/src/Level.lua:

    -- spawn new alien in the world, passing in user data of player
    self.alien = Alien(self.world, 'round', self.shiftedX, self.shiftedY, 'Player')
    
    -- spawn an alien to try and destroy
    table.insert(self.aliens, Alien(self.world, 'square', VIRTUAL_WIDTH - 80, VIRTUAL_HEIGHT - TILE_SIZE - ALIEN_SIZE / 2, 'Alien'))
    

    Notice that the player gets 'Player' user data while enemies get 'Alien' user data. This lets our collision callbacks differentiate between them.

The Alien Launch Marker

  • The alien launch marker handles the slingshot UI and launching. We’re actually simulating that we have an alien on the left. We don’t actually spawn a physics body until we release the mouse click.
  • Consider from angry50/src/AlienLaunchMarker.lua:

    function AlienLaunchMarker:init(world)
        self.world = world
    
        -- starting coordinates for launcher used to calculate launch vector
        self.baseX = 90
        self.baseY = VIRTUAL_HEIGHT - 100
    
        -- shifted coordinates when clicking and dragging
        self.shiftedX = self.baseX
        self.shiftedY = self.baseY
    
        self.aiming = false
        self.launched = false
        self.alien = nil
    end
    

    Notice that baseX and baseY are where the slingshot is anchored. shiftedX and shiftedY track where the user drags the alien.

  • When the mouse is released, we spawn the alien and set its velocity. Consider from angry50/src/AlienLaunchMarker.lua:

    -- spawn new alien in the world, passing in user data of player
    self.alien = Alien(self.world, 'round', self.shiftedX, self.shiftedY, 'Player')
    
    -- apply the difference between current X,Y and base X,Y as launch vector
    self.alien.body:setLinearVelocity((self.baseX - self.shiftedX) * 10, (self.baseY - self.shiftedY) * 10)
    
    -- make the alien pretty bouncy
    self.alien.fixture:setRestitution(0.4)
    self.alien.body:setAngularDamping(1)
    

    Notice that we take the delta between where we dragged (shiftedX/Y) and the base position (baseX/Y), invert it, and multiply by 10 to create the launch velocity. If we drag down and left, we launch up and right. Angular damping of 1 means minimal air friction on rotation.

  • To show the trajectory preview, we simulate what gravity will do to that impulse over time. Consider from angry50/src/AlienLaunchMarker.lua:

    local impulseX = (self.baseX - self.shiftedX) * 10
    local impulseY = (self.baseY - self.shiftedY) * 10
    
    local trajX, trajY = self.shiftedX, self.shiftedY
    local gravX, gravY = self.world:getGravity()
    
    -- http://www.iforce2d.net/b2dtut/projected-trajectory
    for i = 1, 90 do
        -- trajectory X and Y for this iteration of the simulation
        trajX = self.shiftedX + i * 1/60 * impulseX
        trajY = self.shiftedY + i * 1/60 * impulseY + 0.5 * (i * i + i) * gravY * 1/60 * 1/60
    
        -- render every fifth calculation as a circle
        if i % 5 == 0 then
            love.graphics.circle('fill', trajX, trajY, 3)
        end
    end
    

    Notice that over 90 iterations (90 frames at 60 fps is 1.5 seconds), we calculate where the alien would be at each point. The X trajectory is simple linear motion. The Y trajectory includes gravity: 0.5 * (i * i + i) is the accumulated effect of gravity over time, essentially the triangular number formula for summing accelerated motion. We render a circle every 5 frames, giving us 18 dots showing the predicted path.

Joints

  • What joints allow us to do is fill in this other missing piece: You’re capable of much more than just simple shapes moving around. You can actually create compound objects. There are many types of joints by which we can accomplish this. You can envision most physical contraptions modelable in Box2D.
  • Weld Joint - Rigidly attaches two bodies together, like welding metal. Consider from weld/main.lua:

    headBody = love.physics.newBody(world, startX, startY, "dynamic")
    headShape = love.physics.newRectangleShape(headW, headH)
    love.physics.newFixture(headBody, headShape, 2)
    
    handleBody = love.physics.newBody(world, startX, startY + headH / 2 + handleH / 2, "dynamic")
    handleShape = love.physics.newRectangleShape(handleW, handleH)
    love.physics.newFixture(handleBody, handleShape, 1)
    
    weld = love.physics.newWeldJoint(headBody, handleBody, startX, startY + headH / 2, false)
    

    Notice that we create two separate bodies (head and handle), then weld them together at an anchor point between them. They now act as one rigid object. You can also destroy the weld at runtime with weld:destroy() to break them apart.

  • Revolute Joint - A hinge joint that allows rotation around a fixed point, like a pendulum. Consider from pendulum/main.lua:

    local anchorX, anchorY = VIRTUAL_WIDTH / 2, VIRTUAL_HEIGHT / 2 - 60
    anchorBody = love.physics.newBody(world, anchorX, anchorY, "static")
    
    local ballX, ballY = anchorX, anchorY + ROPE_LENGTH_M * METER
    ballBody  = love.physics.newBody(world, ballX, ballY, "dynamic")
    
    hingeJoint = love.physics.newRevoluteJoint(
        anchorBody, ballBody,
        anchorX, anchorY,  -- world-space hinge point
        false)             -- collideConnected
    

    Notice that the ball will swing around the anchor point like a pendulum. We can apply impulses with body:applyLinearImpulse(x, y) to give it a push.

  • Rope Joint - Similar to revolute but allows slack, like a tether ball. Consider from rope/main.lua:

    ropeJoint = love.physics.newRopeJoint(
        anchorBody, ballBody,
        anchorX, anchorY,
        ballBody:getX(), ballBody:getY(),
        maxLen,
        false)
    

    Notice that unlike a revolute joint which keeps a fixed distance, the rope joint allows the ball to get closer to the anchor, the rope can have slack. But it constrains the maximum distance.

  • Pulley Joint - Connects two boxes through ceiling pulleys. Consider from pulley/main.lua:

    local gx1, gy1 = boxA:getX(), 60
    local gx2, gy2 = boxB:getX(), 60
    pulley = love.physics.newPulleyJoint(
        boxA, boxB,
        gx1, gy1, gx2, gy2,   -- ground/ceiling anchor points
        boxA:getX(), boxA:getY(),
        boxB:getX(), boxB:getY(),
        1.0, false)           -- ratio, collideConnected
    

    Notice that the pulley connects two boxes through anchor points at the ceiling. When one goes down, the other goes up, like a real pulley system.

  • There are many more joint types in Box2D. Folks can go to LÖVE 2D’s physics documentation to see all the different joints. The sky is the limit, you can mix and match these to create and physically model so many different things.

Summing Up

  • We did a lot in terms of diving into Box2D and very interesting physics. There’s a lot more to explore beyond what we did today. There are more shapes: With polygons, you can model any shape you want. You could have very complicated geometry. Even Angry Birds has triangular pieces that can be modeled using polygons in Box2D.
  • You can have levels that are separate. We only had one level, but maybe you have a table that creates obstacle definitions that you can then instantiate and load into Box2D every time the level loads. Now you’ve got multiple aliens, a big complicated structure that you’ve defined in code that you’ve tested as stable, made of different materials.
  • For the assignment, you’ll be extending the game:
    • When you hit the spacebar after you spawn your first bird, you should spawn three birds: The normal bird keeps going, but two more spawn off going at certain angles away from that center bird, and all three should apply collision.
    • Add glass and metal to the world with differing numbers of hits to break: Glass is one hit, wood is two, metal is three.
    • When something takes multiple hits, the second hit should show a crack, and the third can show a deeper crack.
    • Incorporate at least one joint in your level that is tied to the mechanics of the game: Either an obstacle or something the player can interact with that could trigger a collision onto an enemy.
  • In this lesson, you learned how to use Box2D to create physics-based games. Specifically, you learned…
    • How the Box2D world serves as a container for all physics simulation.
    • How bodies are abstract containers that hold fixtures and shapes.
    • The difference between static, dynamic, and kinematic body types.
    • How fixtures attach shapes to bodies and control properties like restitution and friction.
    • How to handle mouse input for click-and-drag mechanics.
    • How collision callbacks let you respond to physics events.
    • How user data lets you identify object types during collisions.
    • How to calculate and render trajectory predictions.
    • How joints connect bodies to create compound objects and mechanical systems.

See you next time!