collinhover / impactplusplus

Impact++ is a collection of additions to ImpactJS with full featured physics, dynamic lighting, UI, abilities, and more.
http://collinhover.github.com/impactplusplus
MIT License
276 stars 59 forks source link

Player vs entity + collision map issue. #175

Closed austinrussell closed 10 years ago

austinrussell commented 10 years ago

First off, thanks for this awesome addition to Impact! I'm having a wonderful time with all the pieces and parts this packages has to offer.

I'm working on a small puzzle platformer where the player pushes around blocks to get to an exit. In so doing, I've discovered strange behavior that I can't resolve. If the player collides horizontally with a FIXED entity which is perfectly aligned with a solid collision tile below (but no collision tile below the player), the player will not fall so long as he continues to press left/right in to the entity (see pic below). The player registers as being on the ground from this location (is able to jump, etc). I believe this is caused by an issue between the entity-entity collision detection/resolution vs the player-collision map collision detection, but I can't put my finger on how to fix it.

Screenshots to illustrate: impact-example In this picture, the player is green, the fixed entity is red, and the collision map is blue. If the player jumps and moves to the right (in to the red entity):

impact-example-bug it will stick here "onGround" so long as the player continues to hold the right button. Letting go of the right button will allow the player to fall.

Here's the code for the example: lib/game/main.js

ig.module( 
    'game.main' 
)
.requires(
    'plusplus.core.plusplus',
    'game.levels.test'
)
.defines(function(){

MyGame = ig.GameExtended.extend({
    init: function() {
        // Initialize your game here; bind keys etc.
        this.parent();

        this.loadLevel("LevelTest");                
    }
});

ig.main( '#canvas', MyGame, 60, 320, 240, 2, ig.LoaderExtended);

});

lib/game/entities/player.js

ig.module('game.entities.player')
    .requires(
        'plusplus.abstractities.player'
)
    .defines(function() {
        ig.EntityPlayer = ig.global.EntityPlayer = ig.Player.extend({
            collides: ig.EntityExtended.COLLIDES.PASSIVE,
            size: {
                x: 16,
                y: 16
            },
            animSheet: new ig.AnimationSheet("media/tiles.png", 16, 16),
            animInit: "idleX",
            animSettings: {
                idleX: {
                    sequence: [0],
                    frameTime: 0.1
                }
            },
            jumpForce: 10,
            frictionUngrounded: {x: 500, y: 100},
            maxVelGrounded: {x: 60, y: 100},
            maxVelUngrounded: {x: 60, y: 120},
            gravityFactor: 1
        });
    });

lib/game/entities/block.js

ig.module('game.entities.block')
    .requires(
        'plusplus.core.entity'
)
    .defines(function() {
        ig.EntityBlock = ig.global.EntityBlock = ig.EntityExtended.extend({
            collides: ig.EntityExtended.COLLIDES.FIXED,
            size: {
                x: 16,
                y: 16
            },
            animSheet: new ig.AnimationSheet("media/tiles.png", 16, 16),
            animSettings: {
                idleX: {
                    sequence: [1],
                    frameTime: 0.1
                }
            },
            gravityFactor: 1,
            performance: ig.EntityExtended.PERFORMANCE.DYNAMIC,
            bounciness: 0
        });
    });

lib/game/levels/test.js

ig.module( 'game.levels.test' )
.requires( 'impact.image','game.entities.block','game.entities.player' )
.defines(function(){
LevelTest=/*JSON[*/{"entities":[{"type":"EntityBlock","x":240,"y":272},{"type":"EntityPlayer","x":160,"y":240}],"layer":[{"name":"collision","width":30,"height":20,"linkWithCollision":false,"visible":1,"tilesetName":"","repeat":false,"preRender":false,"distance":1,"tilesize":16,"foreground":false,"data":[[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]]},{"name":"level","width":30,"height":20,"linkWithCollision":true,"visible":1,"tilesetName":"media/tiles.png","repeat":false,"preRender":false,"distance":"1","tilesize":16,"foreground":false,"data":[[3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3],[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],[3,0,0,0,0,0,0,0,0,0,0,0,3,0,0,3,0,0,3,0,0,0,0,0,0,0,0,0,0,3],[3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3]]}]}/*]JSON*/;
LevelTestResources=[new ig.Image('media/tiles.png')];
});

And here's the tilesheet (media/tiles.png): tiles

If anybody has any ideas on how to resolve this such that the player will simply fall past this entity instead of getting stuck next to it, I'm all ears.

austinrussell commented 10 years ago

Oops. Here's my config-user.js as well, for completeness:

ig.CONFIG_USER = {
            SCALE: 2,
            GAME_WIDTH: 160*2,
            GAME_HEIGHT: 160*2,

            CAMERA: {
                AUTO_FOLLOW_PLAYER: true,
                KEEP_CENTERED: false,
                KEEP_INSIDE_LEVEL: true,
                SNAP_FIRST_FOLLOW: true
            }
        };
collinhover commented 10 years ago

One immediate thought to check: open the debug panel and turn on the collision map option. This should show you where the player is colliding with the map. Is the player colliding with the corner of your map for some reason?

Also your ungrounded friction seems very high relative to your player velocity (and maybe your global gravity too). Can you try changing that?

austinrussell commented 10 years ago

In the debug panel, it does not show the player entity colliding with the map, however, I notice that anytime the player and a block are touching, the player entity does not register a map collision, even on a smooth surface. As soon as the two entities separate at all, map collision happens as expected. So it's possible the player is registering a collision with the map, but we wouldn't see it.

A note on this: if I make the blocks collide ACTIVE, they behave like the player entity (when touching another entity, no map collision detected, etc).

Regarding friction, I have this setting in order to give the player better control over their momentum in the air. That said, I tested a variety of settings (0,10,50,100,500,1000), all with the same result.

The world gravity used is the default. I used that to calibrate the velocities to give the player a slightly higher than one block jump height. I again tried a number of values (0,10,100,200,400,500,1000), all with the same result. In 0 gravity, the same effect can be achieved by colliding into the side of a block and pressing down... the player still gets stuck on that edge.

austinrussell commented 10 years ago

All right, I managed to work around this quirk, but have discovered another...

To resolve the original issue, I have each block check it's position during update. If any block perfectly aligns with the collision map, a solid collision tile is created behind the block. Once the block is moved, it resets the tile back to being empty. The solution is very specific to my game's mechanic, so it's not a good general fix, but it will work for what I need. It also bears the limitation that the blocks must have sizes that are multiples of the collision map tilesize, which luckily is not a concern for me at this time.

Which leads to the new issue. I am using loadLevelDeferred tied to a button press to allow the player to restart a level if he/she messes it up. When the level reloads, the changes to the collision map are being maintained, leaving invisible collision tiles where they shouldn't be.

I have tested this behavior in my simple example above. It can be reproduced by adding/replacing the following code in to main.js:

    update: function() {
        // Update all entities and backgroundMaps
        this.parent();

        // Add your own, additional update code here
        if (ig.input.pressed('shoot'))
        {
            ig.game.collisionMap.data[17][12] = 1;
            console.log("setting collision data...");
        }

        if (ig.input.pressed('reset'))
        {
            console.log("reseting level...");
            this.loadLevelDeferred("LevelTest");
        }
    },

    inputStart: function() {
        this.parent();
        ig.input.bind(ig.KEY.F, 'shoot');
        ig.input.bind(ig.KEY.Q, 'reset');
    },
    inputEnd: function() {
        this.parent();
        ig.input.unbind(ig.KEY.F, 'shoot');
        ig.input.unbind(ig.KEY.Q, 'reset');
    },

When the level first loads, you can stand directly on the left column without issue. Pressing F will make the top of that column (position 12,17) in to a solid collision tile. Press Q to reset the level. Notice that the tile that you made solid prior to reloading is still solid.

So the question is, what is maintained when loadLevel() is called? It seems at least the collision map is not getting reset. Is this intended? Or is there a setting to make loadLevel() reset everything that is disabled by default?

Thanks!

collinhover commented 10 years ago

My guess is that impact only keeps a single reference to the collision map data, and in your sample code you modify the collision map data directly. I'd just retain another array of locations at which you've added your custom collision blocks and use that to reset the level collision map data on load.

This of course is not at all ideal, but I don't have the time to jump into collision level debugging for a while yet.

austinrussell commented 10 years ago

Fair enough. I'll sort something out. Thanks!