Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A nicer love.physics api #1130

Open
slime73 opened this issue Jan 25, 2016 · 21 comments
Open

A nicer love.physics api #1130

slime73 opened this issue Jan 25, 2016 · 21 comments
Labels
feature New feature or request

Comments

@slime73
Copy link
Member

slime73 commented Jan 25, 2016

Original report by Bart van Strien (Bitbucket: bartbes, GitHub: bartbes).


As highlighted in #1023, 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 @adonaac linked in #1023: https://github.com/adonaac/hxdx

@slime73
Copy link
Member Author

slime73 commented Jan 29, 2016

Original comment by David Serrano (Bitbucket: Bobbyjones, GitHub: Bobbyjones).


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.

@slime73
Copy link
Member Author

slime73 commented Feb 16, 2016

Original comment by Sasha Szpakowski (Bitbucket: slime73, GitHub: slime73).


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.:

#!Lua

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.

@slime73
Copy link
Member Author

slime73 commented Feb 16, 2016

Original comment by Sasha Szpakowski (Bitbucket: slime73, GitHub: slime73).


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.:

#!Lua

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

@slime73
Copy link
Member Author

slime73 commented Feb 18, 2016

Original comment by Bruce Hill (Bitbucket: spilt, GitHub: spilt).


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.)

@slime73
Copy link
Member Author

slime73 commented Feb 18, 2016

Original comment by adn adn (Bitbucket: adonaac, ).


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:

#!lua
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:

#!lua
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:

#!lua
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:

#!lua
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:

#!lua
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:

#!lua
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

@slime73
Copy link
Member Author

slime73 commented Feb 25, 2016

Original comment by Bruce Hill (Bitbucket: spilt, GitHub: spilt).


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:

#!lua
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):

#!lua
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:

#!lua
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

@slime73
Copy link
Member Author

slime73 commented Feb 29, 2016

@slime73
Copy link
Member Author

slime73 commented Mar 1, 2016

Original comment by adn adn (Bitbucket: adonaac, ).


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.

@slime73
Copy link
Member Author

slime73 commented Mar 1, 2016

@slime73
Copy link
Member Author

slime73 commented Aug 25, 2016

Original comment by airstruck (Bitbucket: airstruck, GitHub: 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").

@slime73
Copy link
Member Author

slime73 commented Aug 25, 2016

Original comment by Bart van Strien (Bitbucket: bartbes, GitHub: bartbes).


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.)

@slime73
Copy link
Member Author

slime73 commented Aug 25, 2016

Original comment by airstruck (Bitbucket: airstruck, GitHub: airstruck).


@bartbes, 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 @pgimeno 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.

@slime73
Copy link
Member Author

slime73 commented Aug 25, 2016

Original comment by Bart van Strien (Bitbucket: bartbes, GitHub: bartbes).


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.

@slime73
Copy link
Member Author

slime73 commented Aug 25, 2016

Original comment by airstruck (Bitbucket: airstruck, GitHub: 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 @slime73 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).

@slime73
Copy link
Member Author

slime73 commented Aug 25, 2016

Original comment by Sasha Szpakowski (Bitbucket: slime73, GitHub: slime73).


What would be a more accurate / descriptive name for it?

@slime73
Copy link
Member Author

slime73 commented Aug 26, 2016

Original comment by Pedro Gimeno Fortea (Bitbucket: pgimeno, ).


I don't know, setLengthScale maybe?

@slime73
Copy link
Member Author

slime73 commented Sep 14, 2016

Original comment by Julio Felipe Angelini (Bitbucket: brocoli, GitHub: brocoli).


+1 on keeping the current physics API as is, except maybe for setMeter, and building any convenience and ease-of-use API on top of it.

@slime73
Copy link
Member Author

slime73 commented Jan 14, 2018

Original comment by itraykov (Bitbucket: itraykov, GitHub: itraykov).


I like Sasha's idea of simplifying the API:

#!lua

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:

#!lua

love.physics.setDefaultDensity(d)
love.physics.setDefaultFriction(f)
love.physics.setDefaultRestitution(r)

@slime73 slime73 added minor change Change to existing functionality labels Feb 20, 2020
@slime73
Copy link
Member Author

slime73 commented Feb 22, 2020

Original attachment:
setmeter-test.love.zip

@slime73 slime73 added feature New feature or request and removed minor change Change to existing functionality labels Feb 25, 2020
@elemel
Copy link
Contributor

elemel commented Sep 14, 2020

Original comment by Sasha Szpakowski (Bitbucket: slime73, GitHub: slime73).

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.

How about:

fixture = Body:newChainFixture(looping, points)
fixture = Body:newCircleFixture(x, y, radius)
fixture = Body:newEdgeFixture(x1, y1, x2, y2)
fixture = Body:newPolygonFixture(points)
fixture = Body:newRectangleFixture(width, height)

@nekromoff
Copy link

nekromoff commented May 3, 2023

Sorry for a ping from the future, but as I have had to solve this (overcomplication) recently, I would definitely vote for having some high level API for simple physics.

It took me maybe a solid couple of hours to grasp what are the differences between body, shape, fixture etc. and why do I need fixture with multiple bodies instead of using joints (thanks to the forum as well, where somebody mentioned a weld joint is clearly not the right way to take). A case in point.

In the end I decided to build a helper that solves this by simplifying shape(s)->body/ies->fixture creation into one function. That function still returns a fixture, but love.physics already allows easy access to bodies and shapes via each other.

PS: I liked the approach of HC although a couple of things could be further simplified (e.g. just generic function to create a collider instead of individual shape ones).

slime73 added a commit that referenced this issue Oct 4, 2023
#1130.

- Add love.physics.newCircleBody(world, bodytype, x, y, radius)
- Add love.physics.newRectangleBody(world, bodytype, x, y, w, h [, angle])
- Add love.physics.newPolygonBody(world, bodytype, coords)
- Add love.physics.newEdgeBody(world, bodytype, x1, y1, x2, y2 [, onesided])
- Add love.physics.newChainBody(world, bodytype, loop, coords)

All new functions return a Body object. The body's world position is at the center of the given coordinates, and the shape's local origin is at its center.
slime73 added a commit that referenced this issue Oct 7, 2023
#1130

- Shapes are now directly attached to Bodies when they're created (similar to love 0.7 and older).
- Fixtures are removed.
- All methods that were in Fixtures now exist in Shapes.
- All APIs that used or returned a Fixture now do the same with a Shape.

- Add new love.physics.new*Shape variants that take a Body as the first parameter.
- Deprecate the new*Shape APIs that don't take a Body.
- Deprecate love.physics.newFixture (the deprecated function now returns a Shape).
- Replace Body:getFixture and Body:getFixtures with Body:getShape and Body:getShapes (Body:getFixtures is deprecated).
- Replace World:queryFixturesInArea and World:getFixturesInArea with World:queryShapesInArea and World:getShapesInArea (queryFixturesInArea is deprecated).
- Replace Contact:getFixtures with Contact:getShapes (Contact:getFixtures is deprecated).
- Replace all love.physics callback Fixture parameters with Shape parameters.
- Deprecate ChainShape:getChildEdge.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants