schteppe / p2.js

JavaScript 2D physics library
Other
2.64k stars 330 forks source link

Add ability to step individual objects in the world #257

Open thomasboyt opened 8 years ago

thomasboyt commented 8 years ago

I've got a somewhat-complex use case for p2, and I think I need the ability to step individual objects in the world to make it best work. I may be totally wrong, though, so let me describe why I think I need this (I'd be very happy to find out there are better ways to do what I'm trying to do!).

I'm working on an online game called Manygolf that uses p2 for a 2D physics simulation of golf balls on a course. The course is made up of 3 Convex polygons (one on each side of the hole and one for the terrain under the hole, which has a different material). The balls are just Circle bodies. The balls have their own collision group and do not collide with each other, only the ground.

The current netcode on https://manygolf.club puts all of the balls into a single world, fires them off at vector when a swing message comes in from the server, and uses a once-a-second sync to try to prevent lag. This sync is very naive, and simply says:

If the player's position at the timestamp of the sync message is more than a certain threshold away from the position in the sync message, move all players in the world back to the synced location, and then step the world forward the delta between the current time and the synced time.

The downside of this lies in the all players in the world bit. If one ball's swing message happens to come very late, and the ball needs to be synced, all the balls are synced, even if they weren't so far off their threshold. This creates significant "jumpiness" in the world.

The reason all players in the world have to be synced is because of the "step the world forward" logic. There's no way to only step a certain object in the world forward, so there's no way to "rewind and replay" an individual ball, only all of them.

My short-term fix for this was to give every ball an individual world. Every world is stepped the same amount on every frame, and an individual world can be "rewound and played forward" on sync if the threshold is too intense.

This fix works great at small player counts, 20-50 people. However, when you start getting to 50-100 players, the CPU usage becomes far too much for a stable framerate (at least on my laptop and on my iPhone). Profiling indicates that the majority of the time is spent in collision detection, especially SAPBroadphase.getCollisionPairs and runNarrowphase.

When I use the old netcode with a single world, while the game is basically unplayable due to "jumpiness," the CPU usage is significantly less and I maintain a stable 60fps. runNarrowphase still uses about the same percentage of CPU time, but getCollisionPairs and other functions use far less.

My assumption here, then, is that trying to use multiple worlds is not going to work from a CPU usage perspective because of the amount of duplicated work between each world - e.g., the AABB of the complex ground polygons has be recomputed for every single world. With this solution out of the picture, then, I circled back around to my initial, shared world, and wondered whether something could be added to allow stepping individual objects in the world?

FWIW, if this isn't a common enough use case to justify inclusion in p2, I might try to just write a function using internal APIs to do this. And, again, if you can think of a better way to accomplish what I'm trying to do, I'd be happy to hear it :)

thomasboyt commented 8 years ago

as usual, right after posting this I came up with a decent workaround:

When I sync the balls, I step the world using a negative delta-time between the sync time and the game time, update the positions+velocities of only the balls that need to be synced (are over the previously-mentioned delta position threshold), and then step forward using the same delta-time but positive. This means only the players that need to be synced actually end up with new positions. I know that explanation was a bit confusing, but the code is fairly simple :)

This works, but is pretty weird. Not sure if this is a better solution than being able to step individual balls.

schteppe commented 8 years ago

Hi, I haven't checked the links you provided yet, or thought about a good solution to your problem, but I'd like you to try the latest master branch to reduce CPU usage. This commit makes sure static bodies don't integrate + invalidate AABBs every step. If your ground bodies are static this should optimize your game.

(Make sure to run grunt to rebuild build/p2.js and build/p2.min.js)

thomasboyt commented 8 years ago

Oh wow, thanks for the heads up on that! It significantly improves performance, to the point that collisions are no longer my bottleneck :)

schteppe commented 7 years ago

Nice :D

Btw I checked out the game now and I must say it's pretty cool!

Maybe you want to use a more scalable approach to the physics part. It seems like the ball trajectories are 100% independent, right? In that case they could be computed in own threads (workers)... or even on different machines in the cloud. The client would send the direction and strength of the shot, then the worker/cloud computes the trajectory, and then you replay that on the client with some interpolation. Of course, the client could compute the on-screen trajectories too, to avoid too much network traffic. This is just an idea, let me know if it makes sense or not :)

If you still want to use a single thread then maybe you want to implement an own broadphase class specific to the game. This would only check AABBs between circles and the ground. Or, alternatively, monkey-patch the method you want to optimize:

// Monkey patch start
p2.SAPBroadphase.prototype.getCollisionPairs = function(world){
    this.result.length = 0;

    // Update ground AABB if needed
    if(groundBody.aabbNeedsUpdate){
        groundBody.updateAABB();
    }

    for(var i=0; i<world.bodies.length; i++){
        var body = world.bodies[i];

        if(body === groundBody) continue; // Skip ground/ground check

        // Update circle AABB if needed
        if(body.aabbNeedsUpdate){
            body.updateAABB();
        }

        if(body.aabb.overlaps(groundBody.aabb)){
            // Report AABB overlap
            this.result.push(body, groundBody);
        }
    }

    return this.result;
}
// Monkey patch end

(warning: untested code)

If you know more about the game you can probably optimize this method even more! For example, perhaps it's OK to just check the distance between the ball center and the top part of the ground AABB? I'll leave this to you.

Good luck with your awesome game, and if you have more feedback on p2.js please let me know!