melonjs / melonJS

a fresh, modern & lightweight HTML5 game engine
https://melonjs.org
MIT License
5.86k stars 643 forks source link

Performance: multiple-phase collisions #103

Closed parasyte closed 10 years ago

parasyte commented 12 years ago

Objects should only do collision detection when they move. And the detection should be done in multiple phases using hash segments:

1. Broad phase

The first phase is called the "broad phase", and it only attempts to resolve which objects/tiles collide, not how to handle the collision. In the broad phase, an AABB is created for the object's entire range of motion. See image below:

Collision broad phase

In resolving the broad phase, all range-of-motion AABBs will be stored into their closest hash segments, and collisions detected between other AABBs within those segments.

(Here "hash segment" means a square area of the world. A good starting point is to break the world into hash segments that are 4x4 tiles in size, though any size is possible.)

2. Narrow phase

The narrow phase takes the collisions detected in the broad phase and finally resolves them by collision shape/position. Some broad-phase collisions will be false-positives, where the range-of-motion AABBs intersect, but an actual object collision did not take place. The narrow phase needs to be good at "bailing early" in these kinds of cases.

An algorithm like this should reduce the time complexity from O(n^2) to something more like O(log((n-s)^2)), where n is the total number of objects and s is the number of stationary objects. In other words, a huge performance gain in collision detection with a large number of objects.

melonjs commented 12 years ago

Another great article (but I guess you know it already) that also cover this topic and collision resolution itself : http://www.metanetsoftware.com/technique/tutorialA.html

+1 for creating a AABB box representing the object motion (that for sure will fix issue #16), however this can only be used with fixed element (like Tile), right ? else an object going where the object was previously placed could trigger a false-positive collision.

Else for the phases approach, I've been looking at that as well, but considering that basically melonJS only supports AABB boxes, I didn't find how to efficiently do the broad phase, as basically (if' I'm not wrong, which I'm sure I am) it's about detecting which objects are intersecting with the object range of motions (which is already the narrow phase).

parasyte commented 12 years ago

Yeah, it's a super great article that I read through a while back. (It made me decide to use chipmunk rather than roll my own collision detection, as I was short on time.)

The false positives are ok in the broad phase, as long as they can be "bailed early" and don't cause too many false positives each frame. That would be a worst-case scenario, and would probably be an uncommon situation. I think only having MANY very small & very faster moving objects will actually create a lot of false positives within the range-of-motion AABBs.

The "broad phase" is usually more about figuring out which objects should be considered for collision checking. It's generally a good idea to use spacial hashing as your broad phase. This will tell you which objects are potentially colliding. You then use their range-of-motion AABBs to further narrow down the search. The "narrow phase" will resolve those collisions; in a physics engine, this means computing and applying response forces.

melonjs commented 11 years ago

@parasyte

I'm moving this one to the next release, unless you are yourself super hyper motivated in implementing this before :) note, the tiled repo is quite active recently, the long due 0.9.0 version should be quite close now, and so the melonJS 0.9.5 release (else I guess at some point I should stop waiting for it and release is without the new image layer)

parasyte commented 11 years ago

@obiot ok! My motivation has turned to some other projects recently. :)

melonjs commented 11 years ago

@parasyte sure, anything exciting ? I hope anyway to see you soon back on melonJS :):):)

Have fun, Olivier

melonjs commented 11 years ago

Another great article : http://buildnewgames.com/broad-phase-collision-detection/

parasyte commented 11 years ago

I started on this tonight, and it got me thinking of the future of melonJS. We want to integrate a physics engine officially. And physics engines do all of their collision detection with rigid bodies; even the environment is composed of rigid bodies. There isn't a distinction between collisions with the environment and collisions with other entities. I guess this property is unique to melonJS in its current form. :)

I would like to remove that distinction in this ticket. All collision checking should occur in one function, using the same data structures. This will ease the integration of a physics engine, and also clean up and make collision detection less work to manage. For compatibility purposes, I am not opposed to converting the collision tile layer into discrete objects within the new collision detection system. (Conversion happens after level load.)

The other thing I want to mention is that I probably won't be implementing an SAT algorithm here. Although I can if it makes more sense to do now than later. My concept is just to eliminate the performance issues, and fix bugs along the way. AABB collisions might be just fine for now. ;)

melonjs commented 11 years ago

@parasyte
Wow, I can see it has been a busy weekend for melonjs :) On my side i'm still away, so i'm sorry i did not provide any real feedback, but I'll be back tomorrow morning !

parasyte commented 11 years ago

@obiot no worries!

My code for this ticket isn't ready for review yet. I want to keep melonJS simple, so I'm going to lump all collision phases into a single function. It isn't the "correct" way to do it, but we're not too concerned with physics accuracy anyway. :p

Regardless, this will be another big API-breaking change!

melonjs commented 11 years ago

@parasyte

a few comments on my side :

About big API-breaking change, please do whatever you think is the best (though I would suggest to keep it as close as possible from the physic API out-there, to ease later integration, or addition). And anyway, if we do want to bring melonJS to the "next level", that's something we cannot avoid.

I'm very excited about this one, this is one of melonjs' major remaining weakness, and if we could have some breakthrough on this one, it would really be fantastic ! :)

parasyte commented 11 years ago

@obiot That all sounds totally reasonable and awesome.

For AABB collisions against the special tiles like slopes: Those will still be handled the same as current, with "pixel perfect" collisions against the 45° slopes. It will need to change later for SAT, but at least we won't drop a feature during the upgrade.

Collision layer conversion code is done! I need to take care of a few more things in the outlying areas and do some testing. After that, I'll push the initial commit to a new branch. :D

One more thing we might enforce is doing all collision handling in callbacks, and getting rid of the explicit me.ObjectEntity.collide() and collideType() methods. Chipmunk, for example, only supports custom collision handling in callbacks, but it supports three different callbacks for handling collisions at different phases of collision resolution.

For the me.ObjectEntity.collisionBox, we shouldn't rename it body, but something else like shape or collisionShape. A "body" in Chipmunk terms is the data structure that contains information about the position, rotation, etc. It's analogous to our me.ObjectEntity already. :) The "shape" defines how the body may collide to with other shapes in the space. So that shape is a lot like collisionBox. Although, Chipmunk also supports multiple shapes per body, and maybe this is something we could consider, too.

melonjs commented 11 years ago

@parasyte Same, totally reasonable and awesome ;)

wow, that's become very concrete very fast I can't wait to play with the new branch !

parasyte commented 11 years ago

Not sure about getting rid of updateMovement! I was thinking about using it to call the new collision checks.

Excluding from collision is easy: I have a new property called collisionMask which can be set in Tiled (or JavaScript) which will mask collisions as desired. If not explicitly set, collisionMask defaults to 0xFFFFFFFF (i.e. collide with everything). It can be used kind of like this:

var masks = {
    PLAYER  : 0x00000001,
    ENEMY   : 0x00000002,
    COIN    : 0x00000004,
    POWERUP : 0x00000008,
};

game.PlayerObject.collisionMask = masks.PLAYER | masks.ENEMY | masks.COIN | masks.POWERUP;
game.CoinObject.collisionMask = masks.COIN;
game.EnemyObject.collisionMask = masks.ENEMY;
game.PowerupObject.collisionMask = masks.POWERUP;

Now game.PlayerObject can collide with everything, and enemies cannot collide with coins or powerups.

It can be a bit tricky to use masks, but it's also supported in Chipmunk!

melonjs commented 11 years ago

wow, that looks great, masks are actually very very good :)

about the collision check, I thought this would be actually automatically done, without requiring to manually call the function, it is not ? but I guess it will be clearer for me once I will see it :)

parasyte commented 11 years ago

Well, it's no more automatic than adding gravity to the velocity vector. ;) I'm sure we could clean this all up in some way, but I haven't really thought about it.

For performance reasons, we only want to check for collisions when an entity moves. And updateMovement is what does that right now.

melonjs commented 11 years ago

oh ok, got your point :)

parasyte commented 11 years ago

@obiot Here is the initial work! :dancers: It's really early still, but it's pretty cool to watch with debug enabled.

melonjs commented 11 years ago

@parasyte so cool, I think I definitely need some time to read your code and understand how it works, but so far I liked what I saw, except for the try/catch :)

melonjs commented 11 years ago

@parasyte just played with it a little, and effectively seeing it running in debug mode is quite cool :)

I did provide a few comments, I hope you don't mind (but nothing really constructive), as I'm still in the phase where I need to understand all the code changes :)

but hey, quite a good good good work you put here !!!!!

parasyte commented 11 years ago

I have some ideas to optimize the code, but I think even the algorithm as it is will be quite a bit better for performance. My next step before working on the default collision handler will be building a basic collision test example, to better understand how it changes performance.

parasyte commented 11 years ago

@obiot Funny news! The collision test demo has worse performance in this branch than in master! :p Updating the grid cells in the broad phase part takes waaayyy too much time. This is actually very easy to fix. But for now ... sleep time! I'll try again tomorrow!

obiot commented 11 years ago

@parasyte that's very good news then, because I was also playing with the code, and a bit worried about performances :)

parasyte commented 11 years ago

From the test cases mentioned in #192, I'm going to stop using Array.prototype.remove() for the me.collision.remove() API. And there are a few other changes I want to try:

  1. me.collision.add() and me.collision.remove() should set and remove the entire list of grid cells, so callers do not have to perform the iteration.
  2. Add some simple hashing to me.collision.updateMovement() so that it doesn't have to call add() and remove() on every frame.

My idea for hashing is creating a unique string that stores the effective "rectangle" of collision grid cells that the object already belongs to, and saving it to the object. At the start of me.game.updateMovement(), a new hash will be generated from the object's current range of motion AABB, and compare it to the saved hash. This should be really fast for determining whether the grid cells need to be updated.

This test shows that in all cases, comparing strings is faster than comparing object properties (like when using me.Rect.equals()): http://jsperf.com/string-vs-property-comparisons So the string-hashing should be the best way to do this.

parasyte commented 11 years ago

@obiot WOW!! I just added the collision_test example to master illustrating how the changes for this ticket improve performance.

On my Mac, the collision_test runs at ~11 fps on master. And runs at ~50 fps on ticket-103. I like it so far. :)

obiot commented 11 years ago

@parasyte

Sorry I was out for lunch !

unfortunately, the demo on the #103 branch seems to be stucked (the smileys are not going down), so i cannot really test it ( i guess it's linked to you two last changes since your message)

but anyway, if you have been able to provide that level of optimization, it's f\ kind of f*** impressive :):):)

my god version 0.9.7 will definitely kick ass !

parasyte commented 11 years ago

Yeah, I don't like the "not moving" part either! :frowning:

But try this patch, and BE AMAZED! :smile:

diff --git a/src/entity/entity.js b/src/entity/entity.js
index e105ec4..fe6d177 100644
--- a/src/entity/entity.js
+++ b/src/entity/entity.js
@@ -884,7 +884,8 @@
                        }

                        // FIXME: Handle collision with tiles
-                       this.vel.setZero();
+                       this.vel.x += (0.5 - Math.random()) * 10;
+                       this.vel.y += (0.5 - Math.random()) * 10;
                },

                /** @private */
obiot commented 11 years ago

wow, i'm truly amazed actually !!!!!

and 2x times amazed, as I'm not sure I would be able to put all this together by myself (at least not as fast as you)!

parasyte commented 11 years ago

Now I have a solid 60 fps in collision_test on my Mac! The smaller entities make it a little bit better on master, too. It gets 15 fps now. :laughing: Bumping the entity count to 400, I get ~38 fps on ticket-103, and ~2 fps on master!

Also, more importantly I started on collision reactions tonight. I have it at least partially working right now. Some bugs left: entities get frozen in air if they are touching, and high-velocity smashing causes strange things like entities bouncing in the wrong direction.

But it's a great improvement already, and a 100% solution for #16! i.e. Very-high-velocity collisions are detected appropriately.

obiot commented 11 years ago

The velocity trick is a simple but smart optimization :)

Next target is 60fps for 400 entities ! Once your code is stable I will give it a pass too for optimization, right now I'm afraid to break stuff ....

Amazing as well to see the difference with the old collision system, I knew it was not that good, but at that point no !

obiot commented 11 years ago

I was playing around with the collision example, and despite of the few bugs you mentioned, the difference is truly visually impressive :)

parasyte commented 11 years ago

Solving the rest of this ticket will be the tricky part, I think. The penetration depth code is kind of working, but it's not very accurate for determining the actual depth, just an approximation. There are some edge cases it needs to handle, and I need to also integrate the linear velocity vector to make sense of the data.

I tried a few different things so far, but nothing is really working any better. Scribbled some graphs on paper to try working out the best algorithm. This is something that should be really well-documented, but I haven't been able to find any good resources on it. Most assume you're looking for an accurate collision response for a physics engine.

I'll try a few more things before attempting to look for other ideas, again. The main trouble part is here: https://github.com/melonjs/melonJS/blob/42703ad830ed99d14c0f132a1f59dfa0d7032764/src/entity/entity.js#L886 It's a very simple algorithm that attempts to correct the velocity vector so the moving entity does not intersect with the second object. As simple as it is, it's not right because it does really strange things. I might need to actually use some if-statements instead of relying on pure dynamic maths!

@obiot If you have ideas, I'd love to hear them. :)

obiot commented 11 years ago

did you actually read the following article : http://www.wildbunny.co.uk/blog/2011/12/14/how-to-make-a-2d-platform-game-part-2-collision-detection/

it's quite interesting and very well documented, although they keep managing tile as tile, as opposed to regular rectangular object on our side, so i guess the solution they propose for the internal edges is not really applicable :)

I will try on my side to play with it, and see if I'm able to propose a solution !

obiot commented 11 years ago

@parasyte just to be clear about my understanding the onCollision function is now used for both collision with entities and the environment (world), right ?

Then if the case at that line, current velocity should not just be cancel (= 0) ? https://github.com/melonjs/melonJS/blob/42703ad830ed99d14c0f132a1f59dfa0d7032764/src/entity/entity.js#L886

parasyte commented 11 years ago

@obiot You are right, onCollision now handles all collisions. But the state of the objects at this callback is [usually] that they are not yet touching. Just that the currently moving object (this) is about to intersect, given its currently velocity.

I was originally just setting velocity to zero in the handler: https://github.com/melonjs/melonJS/blob/7f6996f0e62d9264001adeec4f164407f6d83f19/src/entity/entity.js#L886 This was undesirable because it makes falling entities "slow down" when they get closer to the solid ground tiles, instead of just landing. (Because the entities are not yet intersecting the ground tiles.) Zeroing the velocity also unfortunately makes it impossible to walk; gravity tries pushing the entity into the ground tiles, and then the velocity along both axes gets zeroed.

The updated code attempts to make it better by calculating the penetration depth, and adjusting the velocity to be flush with the colliding object. I think this is the right way to handle it, but there are problems with floating point precision which causes entities to slightly bounce when they are supposed to be resting on a solid tile (gravity, again). That was the purpose for the final floorSelf() on the velocity... even though that is incorrect, too!

I'm going to read over the wildbunny article again (I know I've seen it before). Maybe it will provide some new insight!

obiot commented 11 years ago

@parasyte

there is not a trick where we actually need to store the space difference between the 2 objects and then apply when required ? this is what I actually did in my poor implementation :)

obiot commented 11 years ago

@parasyte

I mean, once we know 2 entities will collide, we should clamp the entity position with the delta space between the 2 of them ? (i don't know if i'm clear).

parasyte commented 11 years ago

@obiot You are clear; that's what the code attempts to do. It's just wrong. Effectively, it clamps the velocity, which gets integrated into the position after all collision callbacks have run for this entity.

obiot commented 11 years ago

It's really to bad we cannot sit together and have a pass together on the code, it would be that much easier :(

parasyte commented 11 years ago

Another good update tonight! This one fixes a lot of the bugs in collision response, making the platformer example almost fully playable, and the smilies in collision test now stack nicely! It's no physics engine, but it will be good enough for simple games.

I have changed a few things with the API (again). Most importantly, the onCollision callback shouldn't be responsible for applying the collision response, because the callback needs the penetration depth information to accurately tell the directionality of collisions. The default collision response can be disabled when the callback returns false. This is in line with Chipmunk-js, where the pre-solve callback can be used to disable the response.

The code for calculating the collision response is getting ugly now, but I don't think it can be prevented! :(

obiot commented 11 years ago

wow great ! on my side I however did not have the time to do anything today, as I spent my day in meetings :( will you commit it tonight ?

parasyte commented 11 years ago

@obiot The commit was pushed last night just before my comment. :) Still not perfect, but much closer now!

obiot commented 11 years ago

@parasyte weirdly it does not show up when I display the branch list (i guess some cache issue with my browser!)

Else, this morning I had some chat with Noel (tokyonono), he was asking about our (well your) implementation as he spent lots of time himself implementing the wildbunny one, and he had some questions and comments he should soon provide to use here (that's good !)

parasyte commented 11 years ago

Nice! I'd love to get some additional feedback. It sounds like some API changes might be needed, looking at your code review comments. But every little bit helps!

parasyte commented 11 years ago

Adding this as a reminder for later:

TODO: Need to add "collision group" support, like Chipmunk. A collision group is a property on an object (similar to but distinct from the new collisionMask property) that allows filtering objects before AABB tests if the groups match. This is useful for example with bullets; usually you don't want bullets to collide with each other. (It would also be helpful for static collision shapes, but they don't generate collisions with the current implementation anyway; collisions are only generated when an object moves.)

Using it is very simple. By default, all objects will have collisionGroup = 0. And any non-zero collisionGroup will require objA.collisionGroup !== objB.collisionGroup before checking AABB overlaps. In other words, collisionGroup = "bullet" will never collide with any other object in the "bullet" collision group.

Chipmunk documentation for collision groups: http://chipmunk-physics.net/release/ChipmunkLatest-Docs/#cpShape-Filtering

Tokyonono commented 11 years ago

Just for info, my implementation of the wild bunny approach in Cocos2d-html5:

tileCoordForPosition:function(position){
        // helper method for physics
        var x = Math.floor(position.x / this.map.getTileSize().width);
        var levelHeightInPixels = this.map.getMapSize().height * this.map.getTileSize().height;
        var y = Math.floor((levelHeightInPixels - position.y) / this.map.getTileSize().height);
        return cc.p(x, y);
    },
    tileRectFromTileCoords:function(tileCoords){
        // helper method for physics
        var levelHeightInPixels = this.map.getMapSize().height * this.map.getTileSize().height;
        var origin = cc.p(tileCoords.x * this.map.getTileSize().width, levelHeightInPixels - ((tileCoords.y + 1) * this.map.getTileSize().height));
        return cc.rect(origin.x, origin.y, this.map.getTileSize().width, this.map.getTileSize().height);
    },
    getRelevantTilesForCollisionCheck:function(character){
        // get collision box
        var collisionBox = character.collisionBoundingBox();

        // get desired position box origin coordinates
        var desiredOriginX = character.desiredPosition.x-(collisionBox.width/2);
        var desiredOriginY = character.desiredPosition.y-(collisionBox.height/2);

        // get surrounding box containing both the collision box and the desired position box
        var pointBottomLeft = cc.p(Math.min(collisionBox.x,desiredOriginX),Math.min(collisionBox.y,desiredOriginY));
        var pointTopRight = cc.p(Math.max(collisionBox.x,desiredOriginX)+collisionBox.width,Math.max(collisionBox.y,desiredOriginY)+collisionBox.height);
        var pointBottomRight = cc.p(pointTopRight.x,pointBottomLeft.y);
        var pointTopLeft = cc.p(pointBottomLeft.x,pointTopRight.y);

        // get coordinates of tiles at each extremity
        var bottomLeft = this.tileCoordForPosition(pointBottomLeft);
        var topLeft = this.tileCoordForPosition(pointTopLeft);
        var bottomRight = this.tileCoordForPosition(pointBottomRight);
        var topRight = this.tileCoordForPosition(pointTopRight);

        // get max values for map
        var mapMaxX = this.map.getMapSize().width;
        var mapMaxY = this.map.getMapSize().height;

        // get surrounding tiles
        var surroundingTiles = new Array();
        for(var positionY = topLeft.y; positionY <= bottomLeft.y; positionY++){
            if ( positionY >= 0 && positionY < mapMaxY){
                for(var positionX = bottomLeft.x; positionX <= bottomRight.x; positionX++){
                    if ( positionX >= 0 && positionX < mapMaxX){
                        var tilePos = cc.p(positionX, positionY);
                        surroundingTiles.push(tilePos);
                    }
                }
            }
        }
        return surroundingTiles;
    },
    distanceBetweenRectangles:function(rect1,rect2){
        // Minkowski difference
        var center1 = cc.p(cc.rectGetMidX(rect1),cc.rectGetMidY(rect1));
        var center2 = cc.p(cc.rectGetMidX(rect2),cc.rectGetMidY(rect2));
        var diffX = center2.x - center1.x;
        var diffY = center2.y - center1.y;
        var halfwidth = (rect1.width + rect2.width)/2;
        var halfheight = (rect1.height + rect2.height)/2;

        // get normal vector
        var normalVector = null;
        var distanceX = Math.abs(diffX) - halfwidth;
        var distanceY = Math.abs(diffY) - halfheight;
        if(Math.abs(distanceX) < Math.abs(distanceY)) {
            if(distanceX < 0) normalVector = cc.p(0,0);
            else if(diffX<0) normalVector = cc.p(-distanceX,0);
            else normalVector = cc.p(distanceX,0);
        } else {
            if(distanceY < 0) normalVector = cc.p(0,0);
            else if(diffY<0) normalVector = cc.p(0,-distanceY);
            else normalVector = cc.p(0,distanceY);
        }
        return normalVector;
    },
    isInternalCollision:function(tile,direction){

        // get tile next to the current one in direction of the normal
        var nextToTile = null;
        if(direction.y<0) nextToTile = cc.p(tile.x,tile.y-1);
        else if(direction.y>0) nextToTile = cc.p(tile.x,tile.y+1);
        else if(direction.x<0) nextToTile = cc.p(tile.x+1,tile.y);
        else if(direction.x>0) nextToTile = cc.p(tile.x-1,tile.y);
        else return false;

        // get max values for map
        var mapMaxX = this.map.getMapSize().width;
        var mapMaxY = this.map.getMapSize().height;

        // check if the tile next to the current one is collidable
        if(nextToTile.x >= 0 && nextToTile.x < mapMaxX && nextToTile.y >= 0 && nextToTile.y < mapMaxY){
            var tgid = this.collisionLayer.getTileGIDAt(nextToTile);
            if (tgid) return true;
        }
        return false;
    },
    checkForCollisions:function(character){
        // set onGround property
        character.onGround = false;

        // buffer for the edge
        var buffer = 0.01;

        // get relevant tiles for collision check
        var relevantTiles = this.getRelevantTilesForCollisionCheck(character);

        // get current velocity and collision box
        var stepVelocity = cc.pSub(character.desiredPosition, character.getPosition());
        var collisionBox = character.collisionBoundingBox();

        // go threw all relevant tiles
        for (var tileIndex = 0; tileIndex < relevantTiles.length; tileIndex++) {
            // check for collision only if it is collidable
            var tgid = this.collisionLayer.getTileGIDAt(relevantTiles[tileIndex]);
            if(tgid){
                // check for collision (speculative contacts)
                var tileRect = this.tileRectFromTileCoords(relevantTiles[tileIndex]);
                var normalDistance = this.distanceBetweenRectangles(collisionBox,tileRect);
                // check on X axis
                if(normalDistance.x && Math.abs(normalDistance.x) < Math.abs(stepVelocity.x)){
                    if(!this.isInternalCollision(relevantTiles[tileIndex],normalDistance)){
                        if(normalDistance.x>0) stepVelocity.x = normalDistance.x - buffer;
                        if(normalDistance.x<0) stepVelocity.x = normalDistance.x + buffer;
                        character.velocity = cc.p(0.0, character.velocity.y);
                    }
                }
                // check on Y axis
                else if(normalDistance.y && Math.abs(normalDistance.y) < Math.abs(stepVelocity.y)){
                    if(!this.isInternalCollision(relevantTiles[tileIndex],normalDistance)){
                        if(normalDistance.y>0) stepVelocity.y = normalDistance.y - buffer;
                        else if(normalDistance.y<0){
                            stepVelocity.y = normalDistance.y + buffer;
                            character.onGround = true;
                            if (character.isJumping) character.endJump();
                        }
                        character.velocity = cc.p(character.velocity.x, 0.0);
                    }
                }
                else if(normalDistance.x == 0 && normalDistance.y == 0){
                    // fix for diagonals (Minkowski difference)
                    var center1 = cc.p(cc.rectGetMidX(collisionBox),cc.rectGetMidY(collisionBox));
                    var center2 = cc.p(cc.rectGetMidX(tileRect),cc.rectGetMidY(tileRect));
                    var diffX = center2.x - center1.x;
                    var diffY = center2.y - center1.y;
                    var halfwidth = (collisionBox.width + tileRect.width)/2;
                    var halfheight = (collisionBox.height + tileRect.height)/2;
                    var distanceX = Math.abs(diffX) - halfwidth;
                    var distanceY = Math.abs(diffY) - halfheight;
                    if(Math.abs(distanceX) > Math.abs(distanceY)) {
                        if(diffX<0) stepVelocity.x = distanceX - buffer;
                        else stepVelocity.x = -distanceX + buffer;
                        character.velocity = cc.p(0.0, character.velocity.y);
                    } else {
                        if(diffY<0) stepVelocity.y = distanceY - buffer;
                        else{
                            stepVelocity.y = -distanceY + buffer;
                            character.onGround = true;
                            if (character.isJumping) character.endJump();
                        }
                        character.velocity = cc.p(character.velocity.x, 0.0);
                    }
                }
            }
        }
        var newPosition = cc.pAdd(character.getPosition(), stepVelocity);
        character.setPosition(newPosition);
    }
obiot commented 11 years ago

:)

Tokyonono commented 11 years ago

Still, I switched to Chipmunk even for basic stuffs because: 1/ It is better than whatever I tried by myself, and I tried a lot of things (but always legal :-) ) 2/ You can argue that the perf are not as good as Box2D when you do crazy things, but for collision detection, it does the job 3/ I stopped trying re-inventing the wheel 4/ I always found limits in all the different approaches, and it is hard to maintain later on... Chipmunk is maintained and getting better and better all the time 5/ I needed to add a moving platform, and it was too time consuming to figure out what to change in my physics engine to make it work... In my opinion, now you are developing a simple physics engine which will end up... having similar perf than Chipmunk doing simple collision detection. I am not old enough to be wise, do whatever you want guys :-D

parasyte commented 11 years ago

The only reason Box2D performance is good on CocoonJS is because its compiled into the app (Objective-C). We don't have a chance to compete with the performance of native code; any JS physics engine will be slower. Until Chipmunk is also available as a native API, we may as well piggyback off the native Box2D. ;)

parasyte commented 11 years ago

Very interesting speed optimization for you guys!

Any time an object's absolute velocity is < 1 on both axes, I skip the collision detection entirely. The performance increase when doing this is actually quite substantial! About 25% faster in the general case (with gravity = 0.98)

As a result, it means velocities less than 1 are not usable. Gravity works because it's an additive velocity. But I can't do things like moving an object by half a pixel per frame with this.vel.x = 0.5; :(

I'm trying to get the best of both worlds (speedy + sub-pixel velocities) but it's proven tricky so far!

obiot commented 11 years ago

yeah, I realized the same issue event with my simple collision code. Maybe a way would be to somehow separate it and have some real velocity vs theoretical velocity with only the latter containing sub-pixel values ? anyway final position is rounded as well to avoid sub-pixel positioning (bad for performances and create some kind of blurry artifacts).