Possible bug with the new collision system?

155 views
Skip to first unread message

Brandon Channell

unread,
Jan 7, 2015, 11:45:14 AM1/7/15
to mel...@googlegroups.com
The tiles are 48x48 in size and I have a section of collision shapes aligned on the Y axis. (each tile has a unique collision shape of equal size)

If my entity is on the LEFT side of the tiles and moves to the RIGHT, collision happens as expected and the player can't pass through the collision shape from LEFT to RIGHT. If the entity moves RIGHT and DOWN simultaneously the character moves down (as expected) until reaching the next tile (again on the y axis). The player gets stuck and no longer moves DOWN. I'm not sure how to simulate collision in a manner similar to the old system. (The old system worked as expected)

Again my collision shapes are equal in size and aligned pixel perfect on the Y axis. I am expecting the character to basically scale vertically as the RIGHT/DOWN or RIGHT/UP combos are pressed .

If this is not a bug, can you please offer suggestions?


game.PlayerEntity = me.Entity.extend({
    
    init: function(x, y, settings) {
        
        // call the constructor
        this._super(me.Entity, "init", [x, y, settings]);
        
        // set the default horizontal & vertical speed (accel vector)
        this.body.setVelocity(4, 4);
        
        // set the gravity
        this.body.gravity = 0;
        
        // set the display to follow our position on both axis
        me.game.viewport.follow(this.pos, me.game.viewport.AXIS.BOTH);
        
        // ensure the player is updated even when outside of the viewport
        this.alwaysUpdate = true;
        
        // define animations
        this.renderable.addAnimation("push_left",  [5,6],   120);
        this.renderable.addAnimation("push_right", [9,10],  120);
        this.renderable.addAnimation("push_down",  [1,2],   120);
        this.renderable.addAnimation("push_up",    [13,14], 120);
        this.renderable.addAnimation("run_left",   [5,6],   120);
        this.renderable.addAnimation("run_right",  [9,10],  120);
        this.renderable.addAnimation("run_down",   [1,2],   120);
        this.renderable.addAnimation("run_up",     [13,14], 120);
        this.renderable.addAnimation("idle_left",  [4]);
        this.renderable.addAnimation("idle_right", [8]);
        this.renderable.addAnimation("idle_down",  [0]);
        this.renderable.addAnimation("idle_up",    [12]);
        
        //  set starting animation
        this.renderable.setCurrentAnimation("idle_down");
        
        // possible key commands
        this.directions = {
            "down":  [0,  1],
            "left":  [-1, 0],
            "right": [1,  0],
            "up":    [0, -1]
        };       
        
        // current key commands
        this.keys = [];


    },
  
    update: function(dt) {   
        var animation = currentAnimation = this.renderable.current.name,
            move = { x:0, y:0 };
        
        this.pushing = false;

        // create array of current commands
        for (var d in this.directions) {
            var index = this.keys.indexOf(d);
            if (me.input.isKeyPressed(d)) {
                move.x+= this.directions[d][0];
                move.y+= this.directions[d][1];
                if (index < 0) {
                    this.keys.push(d);
                }
            } else if (index >= 0) {
                this.keys.splice(index, 1);
            }
        }
    
        if (this.keys[0]) {
            animation = "run_" + this.keys[0];
        }
        
        // assign an idle animation
        if (move.x === 0 && move.y === 0) {
            animation = "idle_" + currentAnimation.split("_")[1];
            if (animation !== currentAnimation) {
                this.renderable.setCurrentAnimation(animation);
            }
        }
        
        // assign a running animation
        if (this.keys.length === 3) {
            for (var d in this.directions) {
                if (this.directions[d][0] === move.x &&
                    this.directions[d][1] === move.y) {
                    animation = "run_" + d;
                }
            }
        }
        
        // assign the velocity
        this.body.vel.x = move.x * this.body.accel.x * me.timer.tick;
        this.body.vel.y = move.y * this.body.accel.y * me.timer.tick;
        
        // apply physics to the body (this moves the entity)
        this.body.update();
     
        // handle collisions against other shapes
        if (me.collision.check(this)) {
            this.pushing = true;
        }
        
        // assign a push animation of moving colliding against pushable objects tiles
        if (this.pushing && this.keys.length < 2) {
            animation = animation.replace("run", "push");
        }
        
        // reset the current animation
        if (animation !== currentAnimation) {
            this.renderable.setCurrentAnimation(animation);
            this.renderable.setAnimationFrame(0);
        }
        
        // update animation if necessary
        return this._super(me.Entity, "update", [dt]) ||
            move.x !== 0 ||
            move.y !== 0 ||
            animation !== currentAnimation;
    },
   
    /**
     * The collision handler, called when colliding with other objects
     */
    
    onCollision: function (response, other) {
        if (this.alive && response.b.body.collisionType == me.collision.types.ENEMY_OBJECT) {
            this.renderable.flicker(750);
            return true;
        }
        return true;
    }
});

Aaron McLeod

unread,
Jan 7, 2015, 12:23:37 PM1/7/15
to mel...@googlegroups.com
Can you post a link to your code or the game?

--
You received this message because you are subscribed to the Google Groups "melonJS - A lightweight HTML5 game engine" group.
To unsubscribe from this group and stop receiving emails from it, send an email to melonjs+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Aaron McLeod

unread,
Jan 7, 2015, 12:24:00 PM1/7/15
to mel...@googlegroups.com
The reason for this is i want to see the tiled map in addition to your player entity :)

To unsubscribe from this group and stop receiving emails from it, send an email to melonjs+unsubscribe@googlegroups.com.

Aaron McLeod

unread,
Jan 7, 2015, 1:04:52 PM1/7/15
to mel...@googlegroups.com
So i had a look. The issue is you replace the tiles with individual blocks, what you want to do is drag a single rectangle over the whole raised section. So for that 8x5 area, instead of 40 rectangles, you just have one.

Olivier, this brings up a good point. Should side by side rectangles that have perfect positioning cause this problem?

Jay Oster

unread,
Jan 7, 2015, 3:25:48 PM1/7/15
to mel...@googlegroups.com
Yes, use bigger collision shapes.

There is a very well-known issue with collision detection against multiple shapes where edges are aligned. See for example the following article: http://www.wildbunny.co.uk/blog/2012/10/31/2d-polygonal-collision-detection-and-internal-edges/

While we don't currently include any of the solutions suggested in this article, it's clear to see that the fewer "internal edges" you have in world geometry, the more stable it will be.

As an aside, getting caught on internal edges depends mostly on the velocity of the moving entity. If the velocity is applied to both X and Y axes with the same force (moving at a perfect 45° angle) then you will get caught on internal edges depending only on the random selection of which edge is considered the shortest penetration depth. It's "random" in the sense that the polygons are defined with vertices in clockwise order, starting from the upper-left vertex, and the edges are similarly ordered, starting with the upper edge. If a convex polygon is described with the vertices rotated, then a different edge would be chosen.

Now, if instead the velocity is not applied on the axis where there is already a collision (or the velocity is reduced along that axis), then you won't get caught on internal edges at all, because the shortest colliding edge will always be on the axis with the smaller velocity.

This is something that the game engine cannot decide for you, because by the time it's checking for collisions to reduce the velocity, it's already too late, and you get caught on the randomly chosen edge. But if your code is smart, and remembers that it was last colliding with an edge on the X or Y axis, then it can apply a gentler force to that axis (say, half of the original force) and solve the problem.

Brandon Channell

unread,
Jan 7, 2015, 5:50:30 PM1/7/15
to mel...@googlegroups.com
Thanks, I'll work on handling this using larger collision shapes, but it will be challenging since my maps are going to be randomly generated. :P

Jay Oster

unread,
Jan 7, 2015, 6:24:17 PM1/7/15
to mel...@googlegroups.com
Merging adjacent rectangles is a simple algorithm... You can also take a hybrid approach using both the merged rectangles and the preventative suggestion of velocity kludging in my response above.

Brandon Channell

unread,
Jan 7, 2015, 10:00:43 PM1/7/15
to mel...@googlegroups.com
Here's the solution I am going with for now since it works like a charm. It's about 10 lines of code, thanks guys!

        if (this.collidesX) {
            this.body.vel.x = this.body.vel.x < 0 ? -1 : 1;
        }
        if (this.collidesY) {
            this.body.vel.y = this.body.vel.y < 0 ? -1 : 1;
        }
        
        // reset collision flags
        this.collidesX = false;
        this.collidesY = false;
        
        // apply physics to the body (this moves the entity)
        this.body.update();
     
        // handle collisions against other shapes
        me.collision.check(this);
        
        // reset the current animation
        if (animation !== currentAnimation) {
            this.renderable.setCurrentAnimation(animation);
            this.renderable.setAnimationFrame(0);
        }
        
        // assign a push animation of moving colliding against pushable objects tiles
        if (this.pushing && this.keys.length < 2) {
            animation = animation.replace("run", "push");
        }
        
        // update animation if necessary
        return this._super(me.Entity, "update", [dt]) ||
            move.x !== 0 ||
            move.y !== 0 ||
            animation !== currentAnimation;
    },
   
    /**
     * The collision handler, called when colliding with other objects
     */
    
    onCollision: function (response, other) {
        if (this.alive && response.b.body.collisionType === me.collision.types.ENEMY_OBJECT) {
            this.renderable.flicker(750);
        }
        this.collidesX = response.overlapV.x !== 0 ? true : false;
        this.collidesY = response.overlapV.y !== 0 ? true : false;
        this.pushing = true;
        return true;
    }
});

Brandon Channell

unread,
Jun 18, 2015, 11:34:49 AM6/18/15
to mel...@googlegroups.com
Better solution! (simplified version)

onCollision: function (response, other) {
    if (response.overlapV.y && (
        response.a._absPos.x <= response.b.pos.x - response.a.width ||
        response.a._absPos.x >= response.b.pos.x + response.b.width)) {
            return false;
    }
    return true;
}

Jay Oster

unread,
Jun 18, 2015, 3:26:11 PM6/18/15
to mel...@googlegroups.com, thedrum...@gmail.com
Hi Brandon, we recommend avoiding direct access of the private variables (for obvious reasons). The official method of retrieving the absolute position is through the `getBounds` function:

response.a.getBounds().pos.x vs response.a._absPos.x

The _absPos is a private variable used by the container hierarchy.
Message has been deleted

Brandon Channell

unread,
Jun 18, 2015, 6:47:40 PM6/18/15
to mel...@googlegroups.com, thedrum...@gmail.com
This seems to give me the projected position instead of the absolute position. I am using the private var (maybe there's another public?) because it doesn't include the overlap.
Message has been deleted

Brandon Channell

unread,
Jun 18, 2015, 7:57:59 PM6/18/15
to mel...@googlegroups.com
Would might be beneficial to include a getAbsPos function?

Jay Oster

unread,
Jun 18, 2015, 8:01:18 PM6/18/15
to mel...@googlegroups.com, thedrum...@gmail.com
The bounds rectangle returned is the absolute position (relative to the map origin). This is really only true in melonJS 2.1.x. Prior to the 2.1 series, the bounds rectangle is relative to its parent.

If you call the getBounds method on both entities (a and b) in the collision response object, you should be able to do the collision checking with the normalized bounding rectangles.

Jay Oster

unread,
Jun 18, 2015, 8:03:24 PM6/18/15
to mel...@googlegroups.com, thedrum...@gmail.com
Like I said, the return value of getBounds() is already absolutely positioned ... What difference are you seeing?

Brandon Channell

unread,
Jun 18, 2015, 9:01:11 PM6/18/15
to mel...@googlegroups.com, thedrum...@gmail.com
The difference is a small matter of overlapV.x (4 or -4 in my case). If the entity runs into a shape from the right side, getBounds() returns the x coord of the right side of the shape - overlapV.x. this was throwing me off because I was expecting getBounds().pos.x to match the right side of the shape.

I ended up storing a local version of absPos and resetting the values after collision is handled. The solution is still much better than my original solution and doesn't use the local variables.

        // handle collisions against other shapes
        me.collision.check(this);
        
        // reset absolute position flags
        this.absPos.x = this.pos.x;
        this.absPos.y = this.pos.y;

        // update absolute position
                this.absPos.y -= response.overlapV.y;
                this.absPos.x -= response.overlapV.x;

                // ignore seamless collision shapes
                if (response.overlapV.y) {
                    if (response.a.absPos.x <= response.b.pos.x - response.a.width ||
                        response.a.absPos.x >= response.b.pos.x + response.b.width) {
                        return false;
                    }
                }

Jay Oster

unread,
Jun 19, 2015, 2:31:04 AM6/19/15
to mel...@googlegroups.com, thedrum...@gmail.com
Maybe if I explain how we envision these things to be used, it will give you a clearer picture of what melonJS is expecting.

The me.Container API provides a hierarchical method of structuring entities, and it's this hierarchy (tree) that uses the private `_absPos` vector for absolute positioning. The container itself has its _absPos updated automatically during the update loop, and each object gets updated as they are processed. It's otherwise unused on non-container children (and should probably be deprecated/abandoned in that case). For container children, it's great because it allows the grandchildren to gather a proper absolute position based on it's parent's own _absPos vector. In other words, the _absPos cascades down the tree.

The short of it is that if you are not using me.Container at all, then the `_absPos` is just a clone of `pos`. And for that matter, getBounds().pos will give you yet another clone.

Ok, so that explains the purpose of `_absPos`; Basically it's internal stuff that computes the absolute position returned by getBounds(). Next, here's the documentation for the collision response object: http://melonjs.github.io/docs/me.collision.html#ResponseObject Take note of the description for `overlapV`:

"If this vector is subtracted from the position of a, a and b will no longer be colliding"

Your code above is doing what is described here in the docs; your local `absPos` is the "corrected" position which will be the final result if the collision handler returns true (assuming me.Container is not being used anywhere). You're testing `overlapV.y`, which means the condition body will only run when correcting a vertical collision (the position correction will move the entity up or down -- keep in mind that it's not possible for a collision response object to use both axes simultaneously; it only gives the smallest of the two, called the penetration depth).

So now we know the position will be corrected up or down to resolve the collision, but you return false based on the X-axis? If the decision only relies on the X-axis when it will be corrected vertically, why do you need the corrected position at all? Correcting the Y-axis will not have any effect on the X-axis. What I'm saying is, the following code is logically and functionally identical, but much simplified:

me.collision.check(this);

if (response.overlapV.y && (response.a.right <= response.b.left || response.a.left >= response.b.right)) {
    return false;
}

One thing to notice is that the `left` and `right` properties can be used as a shortcut for the position & geometry math. I don't have confidence that this code will be perfect in all cases. An obvious example: There are conditions where slow-moving entities (or with high gravity in a side-scroller) will still get stuck with this code, because the overlapV.y will be zero in these cases; it will want to correct the position along the X-axis. There's also the issue of getting caught on an internal edges in the walls; where the penetration depth is usually along the horizontal axis.

Given these shortcomings, I still go back to my original recommendation that the velocities should be relaxed when pressing up against a wall. I could draw some diagrams, if it will help explain the processes.

Brandon Channell

unread,
Jun 19, 2015, 3:29:47 AM6/19/15
to mel...@googlegroups.com, thedrum...@gmail.com
Thanks for the explanation, but your posted solution is still placing object a INSIDE of the collision shape and snagging on the top corner, overlapping by 4 pixels. Ex: Player is 64px wide and shape is 64px wide. If the shape.left is 0 and shape.right is 64 then the player collides with the shape from the right side and the players position (this.pos.x or this.getBounds().pos.x) returns 60 when in reality the x position of player is 60 and the visible x position displayed to the player and is 64.

In short, assuming my entities velocity is 4 _absPos returns 64 and getBounds returns 60.

Brandon Channell

unread,
Jun 19, 2015, 3:37:21 AM6/19/15
to mel...@googlegroups.com
Here are 2 screenshots. idle.png shows the player idle along the side of the shape and collides.png shows the player in the same position but the bounds are passing through while pressing left against the shape.
collides.png
idle.png

Jason Oster

unread,
Jun 19, 2015, 2:46:20 PM6/19/15
to mel...@googlegroups.com, thedrum...@gmail.com
That happens due to synchronization issues; _absPos is one frame behind (remember it's only updated •after• the update method returns). This property should be ignored; you can compute the same value with:

    // Subtract velocity from the current absolute position
    var lastPos = this.getBounds().pos.clone().sub(this.body.vel);

You can use lastPos.left (and right) in place of response.a.pos.left if you want to compare the position prior to the velocity being applied. But again, this will just give you some unexpected results (try moving diagonally into the corner of a lone rectangle).

On Jun 19, 2015, at 00:29, Brandon Channell <thedrum...@gmail.com> wrote:

Thanks for the explanation, but your posted solution is still placing object a INSIDE of the collision shape and snagging on the top corner, overlapping by 4 pixels. Ex: Player is 64px wide and shape is 64px wide. If the shape.left is 0 and shape.right is 64 then the player collides with the shape from the right side and the players position (this.pos.x or this.getBounds().pos.x) returns 60 when in reality the x position of player is 60 and the visible x position displayed to the player and is 64.

In short, assuming my entities velocity is 4 _absPos returns 64 and getBounds returns 60.

--
You received this message because you are subscribed to the Google Groups "melonJS - A lightweight HTML5 game engine" group.
To unsubscribe from this group and stop receiving emails from it, send an email to melonjs+u...@googlegroups.com.

Jay Oster

unread,
Jun 19, 2015, 3:28:47 PM6/19/15
to mel...@googlegroups.com, thedrum...@gmail.com
The screenshot with pressing left is showing unusual behavior (e.g. behavior that does not exist in other melonJS examples). Are the treetops using the WORLD_SHAPE collisionType? You shared this game with me privately before. Would you be willing to send the updated build with this unexpected behavior?

Brandon Channell

unread,
Jun 19, 2015, 9:34:31 PM6/19/15
to mel...@googlegroups.com, thedrum...@gmail.com
Ahhhhhhhh! It is the players weapon entity that is crossing the bounds so that explains the debugging squares in the picture. I added the weapon logic after the collision check and that solved at least the position of the weapon entity.

Brandon Channell

unread,
Jun 19, 2015, 9:47:24 PM6/19/15
to mel...@googlegroups.com
Your solution about subtracting the previous updates velocity is the appears to be the cleanest solution so far. Thank you Jay!

Jay Oster

unread,
Jun 19, 2015, 11:59:04 PM6/19/15
to mel...@googlegroups.com, thedrum...@gmail.com
Ok! I'll still check the code out if you like. I was at a company party when I received the email. Glad you were able to catch the culprit and get an acceptable workaround for the moment. I think this will also be good for experimentation with ignoring internal edges. It's a tricky one!
Reply all
Reply to author
Forward
0 new messages