A nicer love.physics api

Issue #1125 open
Bart van Strien
created an issue

As highlighted in issue #1018, the current physics API isn't exactly nice. It's not the worst, but Fixtures especially are pains to deal with. I understand why Fixtures can be nice to have, but in most of the code I've written or read, one Shape becomes one Fixture, is attached to one Body.

Ideally we'd have some api that kind of matches the love "Hello world" feel, it's just a few lines to do the simple things, but if you want to, you can get a more advanced api. In this case, if you want a circle that has collisions, you'd probably want to be able to quickly create a Body, a Circle shape and the corresponding Fixture, then attach that Fixture to the Body, ideally using only one or two functions.

Perhaps a nice starting point, though I have only glanced at it, is the library @adn adn linked in issue #1018: https://github.com/adonaac/hxdx

Comments (19)

  1. David Serrano

    Maybe something like a love.physics-light and then regular love.physics. Of course it probably should not be called love.physics-light. The light version would be a layer over the physics api to make it easier. But I think it would be important to define what is easier. I feel as though the biggest hurdle with love.physics is that there is so many methods that a body, fixture or shape have. So it makes doing even the most simplest thing a chore to get right. I don't think there would be a good way to make dealing with all those methods and settings easier. Now if by easier you mean object creation then I think that can be accomplished relatively easily.

  2. Alex Szpakowski

    I understand why Fixtures can be nice to have, but in most of the code I've written or read, one Shape becomes one Fixture, is attached to one Body.

    An small addition to LÖVE could be to have shortcut methods in Body, e.g.:

    fixture = Body:newChainShape(looping, points)
    fixture = Body:newCircleShape(x, y, radius)
    fixture = Body:newEdgeShape(x1, y1, x2, y2)
    fixture = Body:newPolygonShape(points)
    fixture = Body:newRectangleShape(width, height)
    

    Although those method names are kind of misleading, since they return Fixtures rather than Shapes.

  3. Alex Szpakowski

    You also wouldn't be able to explicitly set the density during Fixture creation with those (although there is Fixture:setDensity).

    It would probably also be good in general to move love.physics.newFixture to a Body method, since it'd be less verbose and still do the same thing. i.e.:

    fixture = love.physics.newFixture(body, shape [, density])
    fixture = body:newFixture(shape [, density])
    
  4. Bruce Hill

    One aspect of the current API that I find pretty user-unfriendly is collision groups/categories/masks. I think my ideal collision API would be something like:

    world:addCollisionCategories("terrain","player","enemy","playerProjectile","enemyProjectile")
    local projectile = {"playerProjectile","enemyProjectile"}
    world:ignoreCollisionsBetween(projectile, projectile)
    world:ignoreCollisionsBetween({"playerProjectile"},{"player"})
    world:ignoreCollisionsBetween({"enemyProjectile"},{"enemy"})
    ...
    player.fixture:setCollisionCategories({"player"})
    ...
    environmentalHazard.fixture:setCollisionCategories(projectile) -- This would collide with both players and enemies
    

    If this system were used, it would also be nice to have a collision callback API that worked something like this:

    -- This sets callbacks that are called once, immediately before the physics engine handles two bodies making contact:
    player.fixture:setCollisionCallbacks({
        -- Mapping from collision category -> callback function
        enemy = function(self, other, contact)
            self:getUserData():takeDamage(other:getUserData().damage)
        end,
        terrain = function(self, other, contact)
            local nx, ny = contact:getNormal()
            if ny < 0 then contact:setEnabled(false) end -- one-way platforms
        end,
    })
    -- Similarly, this sets callbacks that are called once, just after two fixtures lose contact
    player.fixture:setEndCollisionCallbacks({})
    ...
    -- There would be no equivalent to the current callbacks that get called once per frame, instead, you could just query what the current contacts are
    function love.update(dt)
        world:update(dt)
        -- Get a list of all contacts with an optional list of categories
        local terrainContacts = player.fixture:getContacts({"terrain"})
        player.onGround = false
        for terrainFixture,contact in pairs(terrainContacts) do
            local ix,iy = contact:getImpulse()
            if iy < 0 then
                player.onGround = true
                break
            end
        end
        ...
    end
    

    (Collision callbacks would be called in an unspecified order.)

  5. adn adn

    I've been using love.physics for a while now so I feel like I have a good grasp of its pain points. Most of what I'm gonna say can be seen in action in my hxdx library. So let me start with what other people already said:

    Ideally we'd have some api that kind of matches the love "Hello world" feel, it's just a few lines to do the simple things, but if you want to, you can get a more advanced api. In this case, if you want a circle that has collisions, you'd probably want to be able to quickly create a Body, a Circle shape and the corresponding Fixture, then attach that Fixture to the Body, ideally using only one or two functions.

    I think you can go even further and introduce the concept of Colliders. These would be new object types that are built on top of the existing objects box2d provides. Like you said, usually what most people do ends up being one body + one shape + one fixture. A collider would just be all this done automatically. In hxdx Colliders are just that and you can create a new one via something like:

    collider = world:createCircleCollider(x, y, radius, opts)
    

    You can then access the body, fixture and shape via collider.body, collider.fixture, collider.shape. .fixture and .shape are aliases for the 'main' fixture/shape pair, since because you can have multiple fixture/shape pairs they have to be in a list, but since most people will never do that it makes sense to present them as .fixture and .shape instead of something like .fixtures_list['main'] and .shapes_list['main']. If you want to have multiple pairs though you can add new shape fixture pairs to the list and operate like you would normally via some function like collider:addShape(shape_name, ...) and then it adds the shape + fixture automatically. I don't remember if I added this function on my own library.

    The point is, a Collider can expose normal "advanced" functionality but also make things easier for new users. You could go a step further than I did with hxdx and instead of having the main way of users to interact with physics object be via collider.body/fixture, you can just add the most used functions in a body/fixture, like applyLinearImpulse or applyForce or setRestitution to the Collider object and make it so that for the most common use cases people don't even have to know about bodies, fixtures or shapes. Like:

    collider:applyLinearImpulse(0, 1000)
    collider:setGravityScale(1.5)
    collider:setBullet(true)
    collider:setRestitution(0.5)
    

    Most people only want to use physics to apply forces to objects and to make collision easy, so handling those use cases via the Collider object can dramatically decrease the number of things people have to care about. And if they wanna go deeper then sure, they can still access the body, fixture and shape all they want. This would create a nice tutorial-like feel to the API where it has multiple layers to it that people can dive into at their own pace instead of being showered with tons of concepts at once.


    One aspect of the current API that I find pretty user-unfriendly is collision groups/categories/masks. I think my ideal collision API would be something like:

    I agree with your solution and it's pretty much the same one that I arrived. Dealing with categories/masks is really unintuitive. The solution I arrived at is similar to yours (https://github.com/adonaac/hxdx#add-collision-classes, https://github.com/adonaac/hxdx/tree/master/docs#addcollisionclasscollision_class_name-collision_class) it just has a few differences in how things are specified but the idea is the same. The code for doing this is not trivial but not super hard. There was a good thread about it a while ago here https://love2d.org/forums/viewtopic.php?f=4&t=75441. It's not trivial because there's a maximum number of categories you can use (16) so you wanna make sure you do it in the optimal way.


    If this system were used, it would also be nice to have a collision callback API that worked something like this:

    I personally think callbacks are bad so I disagree with this. However this all comes down to personal preferences in the end I guess. The way I solved collision detection and callbacks was like this https://github.com/adonaac/hxdx/tree/master/docs#enterother_collision_class_name. You just have an enter and exit function that will return true on the frame those events happen and from there you can do whatever.

    preSolve and postSolve functions have to still be callbacks though because they happen in the middle of some box2d operation being resolved, so they can't be queued for later like I did with enter and exit events. One of the reasons I think callbacks are bad is exactly because of this. preSolve and postSolve will never be able to not be callbacks because they're completely tied internally to how box2d works so this will leak forever and force every API on top of it to use callbacks too. It's much better to not repeat the mistake where possible and NOT make things callbacks unless absolutely necessary.

    Anyway, the way to easily deal with collisions is to use the earlier concept of collision categories to check for enter/exit collision events. In other engines I think this is typically called a collision tag. You'd have something like:

    if collider:enter('SomeOtherTag') then
    
    end
    

    And then inside that if you'll do whatever you want to handle that collision. One of the things missing is being able to just check if an object is on top of another or not, like, if collider:collidingWIth('SomeOtherTag'). This is something I didn't do in my library in this exact way, but that can be done via query functions https://github.com/adonaac/hxdx/tree/master/docs#querycircleareax-y-r-collision_class_names. The extra work here would be to check which shape the collider is and then use the appropriate query function, like:

    function Collider:collidingWith(other_tags)
        if self.shape_type == 'Circle' then
            return world:queryCircleArea(self.x, self.y, self.r, other_tags)
        elseif self.shape_type == 'Rectangle' then
            return world:queryRectangleArea(self.x, self.y, self.w, self.h, other_tags)
        ...
    end
    

    The last important thing that no one mentioned is the idea of binding the parent object to the physics object. Currently this can be done via fixture:setUserData(). This binding is extremely useful because it's through it that when a collision event happens you can get the other object that the current one collided with and do things with it, like deal damage to it or whatever. Assuming the idea of Colliders, one common pattern I found is something like this:

    function Enemy:new(...)
        self.collider = world:createCircleCollider(...)
        self.collider:setGameObject(self)
    end
    

    And then whenever in a collision event you want to get the other game object you can do something like:

    function Player:update(dt)
        local colliding_with_enemy, enemy_collider = self.collider:enter('Enemy')
        if colliding_with_enemy then
            self:dealDamage(-10)
            local enemy_object = enemy_collider:getGameObject()
            enemy_object:knockback(100)
       end
    end
    
  6. Bruce Hill

    I think you can go even further and introduce the concept of Colliders. These would be new object types that are built on top of the existing objects box2d provides. Like you said, usually what most people do ends up being one body + one shape + one fixture. A collider would just be all this done automatically.

    Yeah, I agree that the Collider (not sure about the name, maybe "PhysicsObject" instead?) model simplifies things well. Almost all of my use cases are 1-userData/body/fixture/shape.


    The solution I arrived at is similar to yours (https://github.com/adonaac/hxdx#add-collision-classes, https://github.com/adonaac/hxdx/tree/master/docs#addcollisionclasscollision_class_name-collision_class) it just has a few differences in how things are specified but the idea is the same.

    It's similar, but I can't tell from looking at your implementation how it will behave in this example:

    world:addCollisionClass('Player', {ignores={'Enemy'}})
    world:addCollisionClass('Enemy', {ignores={}})
    

    Would players and enemies collide? The above code is ambiguous. That's why in my API example, I made ignoring collisions be symmetric, so this is how you would unambiguously express that you want players and enemies to pass through each other (argument order does not matter):

    world:ignoreCollisionsBetween({'Player'}, {'Enemy'})
    

    I personally think callbacks are bad so I disagree with this. However this all comes down to personal preferences in the end I guess.

    It's not just a matter of personal preference, it's also a matter of performance. Box2D has really efficient collision detection algorithms and using callbacks allows you to only run your code when Box2D determines that a collision happens, instead of performing a check on every object, every frame. Switching the API to use collidingWith() instead of callbacks would force people to use the less performant option.

    Personally, I also prefer callbacks for collision handling because they more closely match what I'm trying to express, which is usually "when I collide with X, do this behavior", not "every frame, ask if I just collided with X, and if I did, do this behavior".


    Two other API suggestions that came to me recently: The first is to replace world:queryBoundingBox(...) with world:queryShape(shape). Right now, if you want to find the objects that overlap a circle, you need to create a Body, Shape, Fixture, set the fixture to be a sensor, update the world with a timestep of 0, and then iterate over the contacts. This all seems wasteful and cumbersome to me. The second suggestion is to have a fixture:draw(drawMode) (or PhysicsObject:draw(drawMode)) method, because it’s irritating to have to type all this just to draw an arbitrary physics object:

    if obj.shape.getRadius ~= nil then
        love.graphics.circle('fill', obj.body:getX(), obj.body:getY(), obj.shape:getRadius())
    else
        love.graphics.polygon('fill', obj.body:getWorldPoints(obj.shape:getPoints()))
    end
    
  7. adn adn

    SpriteKit seems to do with SKPhysicsBody the same idea I have with Colliders. Unity also does something similar although not from box2d. Their SKPhysicsWorld also seems to have the needed query functions (raycast, point, rect, missing circle and polygon I guess) as well as Fields, which is something that Unity's physics system also has (https://unity3d.com/learn/tutorials/modules/beginner/live-training-archive/2d-physics-fun-with-effectors) that are a really good idea on top of everything everyone has mentioned here.

  8. airstruck

    Two quick thoughts:

    I really like the current physics API (with one exception). I hope this new API will be a higher-level convenience thing on top of the current API, and the current API will be pretty much left alone.

    The one part of the current API I dislike is setMeter. I wonder if setMeter might be a better fit for the new API, and could be removed entirely from the current API. If nothing else, this will make things much easier to document accurately (for example, we can simply say that some force is measured in Newtons rather than something contorted like "kilogram units per second squared, where units means meters divided by the value most recently passed to setMeter").

  9. Bart van Strien reporter

    We've discussed that particular before, and I still think you're blowing it out of proportion. The unit is Newton (aka kg·m/s²), rather than something involving pixels (kg·px/s²), setMeter simply determines the conversion rate between meters and pixels. If you're using meters internally, setMeter(1) accurately reflects that. (And any place it doesn't, is a bug.)

  10. airstruck

    @Bart van Strien, what you're saying now seems to contradict what we discussed earlier. To recap the conversation, I asked if gravity was measured in m/s². Your response was: "No, it's not m/s², it's unit/s², where "unit" is whatever is defined using setMeter." Then @Pedro Gimeno Fortea replied: "I tend to think that setMeter is the pixels per metre conversion and that's not quite correct. It's actually for length unit conversion."

    From that conversation, my understanding is that force is actually measured in kg·px/s². I might just be confused about the whole thing, but that just indicates that setMeter is confusing (that, or I'm just too dense to understand it properly).

    I'm attaching a .love that might explain it better. My thinking is that if gravity were really measured in m/s², I could set the gravity of both worlds to the same value and they'd have the same gravity.

    Sorry if this is going off-topic, but in my view this could be relevant to both improving and simplifying the physics API.

  11. Bart van Strien reporter

    I do have this horrible habit of contradicting myself. Which I will now do again, by saying I was correct before. So basically, I got confused (which illustrates your point I guess), and yes, it would be measured in kg·px/s². I will defend that that makes more sense in code, if I set a speed of 50, I'd expect it to be 50 pixels further, not 50 meters, which may very well be off-screen.

  12. airstruck

    I agree, that does make sense, although I think it makes more sense for a simplified API than it does for an API that mostly mirrors the Box2D API in a Lua-fied sort of way. I just mentioned it again here because I wanted to get the suggestion about moving it to the simplified API on the record (and because I promised @Alex Szpakowski I'd update the wiki, but I honestly can't get my head around how to document units of measurement, so it's been on my mind again).

  13. itraykov

    I like Alex's idea of simplifying the API:

    fixture = Body:newChainShape(looping, points)
    fixture = Body:newCircleShape(x, y, radius)
    fixture = Body:newEdgeShape(x1, y1, x2, y2)
    fixture = Body:newPolygonShape(points)
    fixture = Body:newRectangleShape(width, height)
    

    This would be sort of like the old version of Box2D were fixtures/shapes were combined in one object. The only benefit of separating the two is you want to "re-use shapes" which is not very practical in Lua anyways.

    My personal gripe with the Love2D API is that some of "constructors" are out hand (ie newPulleyJoint).
    One possible solution could be to have globally-modifiable defaults:

    love.physics.setDefaultDensity(d)
    love.physics.setDefaultFriction(f)
    love.physics.setDefaultRestitution(r)
    
  14. Log in to comment