/// <summary> /// gets a list of all the tiles intersecting bounds. The returned list is ordered for collision detection based on the /// direction passed in so they can be processed in order. /// </summary> /// <returns>The colliding tiles.</returns> /// <param name="bounds">Bounds.</param> /// <param name="direction">Direction.</param> void populateCollidingTiles( Rectangle bounds, Edge direction ) { _collidingTiles.Clear(); var isHorizontal = direction.isHorizontal(); var primaryAxis = isHorizontal ? Axis.X : Axis.Y; var oppositeAxis = primaryAxis == Axis.X ? Axis.Y : Axis.X; var oppositeDirection = direction.oppositeEdge(); var firstPrimary = worldToTilePosition( bounds.getSide( oppositeDirection ), primaryAxis ); var lastPrimary = worldToTilePosition( bounds.getSide( direction ), primaryAxis ); var primaryIncr = direction.isMax() ? 1 : -1; var min = worldToTilePosition( isHorizontal ? bounds.Top : bounds.Left, oppositeAxis ); var mid = worldToTilePosition( isHorizontal ? bounds.getCenter().Y : bounds.getCenter().X, oppositeAxis ); var max = worldToTilePosition( isHorizontal ? bounds.Bottom : bounds.Right, oppositeAxis ); var isPositive = mid - min < max - mid; var secondaryIncr = isPositive ? 1 : -1; var firstSecondary = isPositive ? min : max; var lastSecondary = !isPositive ? min : max; for( var primary = firstPrimary; primary != lastPrimary + primaryIncr; primary += primaryIncr ) { for( var secondary = firstSecondary; secondary != lastSecondary + secondaryIncr; secondary += secondaryIncr ) { var col = isHorizontal ? primary : secondary; var row = !isHorizontal ? primary : secondary; _collidingTiles.Add( collisionLayer.getTile( col, row ) ); #if DEBUG_MOVER if( direction.isHorizontal() ) { var pos = tiledMap.tileToWorldPosition( new Point( col, row ) ); _debugTiles.Add( new Rectangle( (int)pos.X, (int)pos.Y, 16, 16 ) ); } #endif } } }
/// <summary> /// Tests the tile for a collision. Returns via out the position in world space where the collision occured. /// </summary> /// <returns>The tile collision.</returns> /// <param name="tile">Tile.</param> /// <param name="edgeToTest">the opposite side of movement, the side the leading edge will collide with</param> /// <param name="perpindicularPosition">Perpindicular position.</param> /// <param name="leadingPosition">Leading position.</param> /// <param name="shouldTestSlopes">Should test slopes.</param> /// <param name="collisionResponse">Collision response.</param> bool testTileCollision( TiledTile tile, Edge edgeToTest, int perpindicularPosition, int leadingPosition, bool shouldTestSlopes, out int collisionResponse ) { collisionResponse = leadingPosition; // one way platforms are only collideable from the top when the player is already above them if( tile.isOneWayPlatform() ) { // only the top edge of one way platforms are checked for collisions if( edgeToTest != Edge.Top ) return false; // our response should be the top of the platform collisionResponse = tiledMap.tileToWorldPositionX( tile.y ); return _boxColliderBounds.Bottom <= collisionResponse; } var forceSlopedTileCheckAsWall = false; // when moving horizontally the only time a slope is considered for collision testing is when its closest side is the tallest side // and we were not intesecting the tile before moving. // this prevents clipping through a tile when hitting its edge: -> |\ if( edgeToTest.isHorizontal() && tile.isSlope() && tile.getNearestEdge( leadingPosition ) == tile.getHighestSlopeEdge() ) { var moveDir = edgeToTest.oppositeEdge(); var leadingPositionPreMovement = _boxColliderBounds.getSide( moveDir ); // we need the tile x position that is on the opposite side of our move direction. Moving right we want the left edge var tileX = moveDir == Edge.Right ? tiledMap.tileToWorldPositionX( tile.x ) : tiledMap.tileToWorldPositionX( tile.x + 1 ); // using the edge before movement, we see if we were colliding before moving. var wasCollidingBeforeMove = moveDir == Edge.Right ? leadingPositionPreMovement > tileX : leadingPositionPreMovement < tileX; // if we were not colliding before moving we need to consider this tile for a collision check as if it were a wall tile forceSlopedTileCheckAsWall = !wasCollidingBeforeMove; } if( forceSlopedTileCheckAsWall || !tile.isSlope() ) { switch( edgeToTest ) { case Edge.Top: collisionResponse = tiledMap.tileToWorldPositionY( tile.y ); break; case Edge.Bottom: collisionResponse = tiledMap.tileToWorldPositionY( tile.y + 1 ); break; case Edge.Left: collisionResponse = tiledMap.tileToWorldPositionX( tile.x ); break; case Edge.Right: collisionResponse = tiledMap.tileToWorldPositionX( tile.x + 1 ); break; } return true; } if( shouldTestSlopes ) { var tileWorldX = tiledMap.tileToWorldPositionX( tile.x ); var tileWorldY = tiledMap.tileToWorldPositionX( tile.y ); var slope = tile.getSlope(); var offset = tile.getSlopeOffset(); // calculate the point on the slope at perpindicularPosition collisionResponse = (int)( edgeToTest.isVertical() ? slope * ( perpindicularPosition - tileWorldX ) + offset + tileWorldY : ( perpindicularPosition - tileWorldY - offset ) / slope + tileWorldX ); var isColliding = edgeToTest.isMax() ? leadingPosition <= collisionResponse : leadingPosition >= collisionResponse; // this code ensures that we dont consider collisions on a slope while jumping up that dont intersect our collider. // It also makes sure when testing the bottom edge that the leadingPosition is actually above the collisionResponse. // HACK: It isn't totally perfect but it does the job if( isColliding && edgeToTest == Edge.Bottom && leadingPosition <= collisionResponse ) isColliding = false; return isColliding; } return false; }
bool testMapCollision( Rectangle collisionRect, Edge direction, out int collisionResponse ) { collisionResponse = 0; var side = direction.oppositeEdge(); var perpindicularPosition = side.isVertical() ? collisionRect.Center.X : collisionRect.Center.Y; var leadingPosition = collisionRect.getSide( direction ); var shouldTestSlopes = side.isVertical(); populateCollidingTiles( collisionRect, direction ); for( var i = 0; i < _collidingTiles.Count; i++ ) { if( _collidingTiles[i] == null ) continue; // TODO: is this necessary? seems to work without it // disregard horizontal collisions if the last tile we were grounded on was a slope // this is not a fantastic solution if( direction.isHorizontal() && _lastGroundTile != null && _lastGroundTile.isSlope() ) continue; if( testTileCollision( _collidingTiles[i], side, perpindicularPosition, leadingPosition, shouldTestSlopes, out collisionResponse ) ) { // store off our last ground tile if we collided below if( direction == Edge.Bottom ) _lastGroundTile = _collidingTiles[i]; return true; } // special case for sloped ground tiles if( _lastGroundTile != null && direction == Edge.Bottom ) { // if grounded on a slope and intersecting a slope or if grounded on a wall and intersecting a tall slope we go sticky // tall slope here means one where the the slopeTopLeft/Right is 0, i.e. it connects to a wall if( ( _lastGroundTile.isSlope() && _collidingTiles[i].isSlope() ) || ( !_lastGroundTile.isSlope() && _collidingTiles[i].isSlope() /* should be tall slope check */ ) ) { // store off our last ground tile if we collided below _lastGroundTile = _collidingTiles[i]; return true; } } } return false; }
bool testMapCollision( Rectangle collisionRect, Edge direction, CollisionState collisionState, out int collisionResponse ) { collisionResponse = 0; var side = direction.oppositeEdge(); var perpindicularPosition = side.isVertical() ? collisionRect.Center.X : collisionRect.Center.Y; var leadingPosition = collisionRect.getSide( direction ); var shouldTestSlopes = side.isVertical(); populateCollidingTiles( collisionRect, direction ); for( var i = 0; i < _collidingTiles.Count; i++ ) { if( _collidingTiles[i] == null ) continue; // disregard horizontal collisions if the last tile we were grounded on was a slope. Our y movement will push us up on the slope. // this is not a fantastic solution if( direction.isHorizontal() && collisionState._lastGroundTile != null && collisionState._lastGroundTile.isSlope() && _collidingTiles[i].isSlope() ) return false; if( testTileCollision( _collidingTiles[i], side, perpindicularPosition, leadingPosition, shouldTestSlopes, out collisionResponse ) ) { // store off our last ground tile if we collided below if( direction == Edge.Bottom ) { collisionState._lastGroundTile = _collidingTiles[i]; collisionState.isGroundedOnOneWayPlatform = collisionState._lastGroundTile.isOneWayPlatform(); } return true; } // special case for sloped ground tiles if( collisionState._lastGroundTile != null && direction == Edge.Bottom ) { // if grounded on a slope and intersecting a slope or if grounded on a wall and intersecting a tall slope we go sticky. // tall slope here means one where the the slopeTopLeft/Right is 0, i.e. it connects to a wall var isHighSlopeNearest = _collidingTiles[i].getNearestEdge( perpindicularPosition ) == _collidingTiles[i].getHighestSlopeEdge(); if( ( collisionState._lastGroundTile.isSlope() && _collidingTiles[i].isSlope() ) || ( !collisionState._lastGroundTile.isSlope() && isHighSlopeNearest ) ) { // store off our last ground tile if we collided below collisionState._lastGroundTile = _collidingTiles[i]; return true; } } } return false; }
/// <summary> /// gets a list of all the tiles intersecting bounds. The returned list is ordered for collision detection based on the /// direction passed in so they can be processed in order. /// </summary> /// <returns>The colliding tiles.</returns> /// <param name="bounds">Bounds.</param> /// <param name="direction">Direction.</param> void populateCollidingTiles( Rectangle bounds, Edge direction ) { _collidingTiles.Clear(); var isHorizontal = direction.isHorizontal(); var primaryAxis = isHorizontal ? Axis.X : Axis.Y; var oppositeAxis = primaryAxis == Axis.X ? Axis.Y : Axis.X; // if we are going up or left we subtract 1 from our opposite edge so that it doesnt get the column/row of tiles when we are against them var oppositeDirection = direction.oppositeEdge(); var minSidePad = direction.isMin() ? -1 : 0; var firstPrimary = worldToTilePosition( bounds.getSide( oppositeDirection ) + minSidePad, primaryAxis ); var lastPrimary = worldToTilePosition( bounds.getSide( direction ), primaryAxis ); var primaryIncr = direction.isMax() ? 1 : -1; var min = worldToTilePosition( isHorizontal ? bounds.Top : bounds.Left, oppositeAxis ); var mid = worldToTilePosition( isHorizontal ? bounds.getCenter().Y : bounds.getCenter().X, oppositeAxis ); // same as above here. we subtract 1 to not grab an extra column/row of tiles which will mess up collisions var max = worldToTilePosition( isHorizontal ? bounds.Bottom - 1 : bounds.Right - 1, oppositeAxis ); var isPositive = mid - min < max - mid; var secondaryIncr = isPositive ? 1 : -1; var firstSecondary = isPositive ? min : max; var lastSecondary = !isPositive ? min : max; for( var primary = firstPrimary; primary != lastPrimary + primaryIncr; primary += primaryIncr ) { for( var secondary = firstSecondary; secondary != lastSecondary + secondaryIncr; secondary += secondaryIncr ) { var col = isHorizontal ? primary : secondary; var row = !isHorizontal ? primary : secondary; _collidingTiles.Add( _collisionLayer.getTile( col, row ) ); } } }