15.2 C
New York

2D Tilemap Collision decision in C#/MonoGame


A little bit of context about my recreation:

I am almost 2 years deep into improvement of my recreation referred to as Cosmorists. It is programmed in C# utilizing the .NET 7.0.11 framework, and MonoGame 3.8.1 recreation framework and it is a top-down tile-based recreation with repeatedly repeating sq. tiles. One downside has been weighing me down all through the course of my improvement journey, and that is collision decision.

Some context for implementation:

The sport makes use of a tilemap, and it stretches to the 32-bit signed integer limits in each instructions. They’re saved as 16×16 chunks, and tile positions are represented as advanced integers with actual and imaginary values. Every tile is saved as a 16-bit unsigned integer representing the tile’s index referred to as “decor tiles”. 0 is an empty tile.

Because it makes use of a tilemap, I do know that this downside will probably be a lot simpler for my case, as every tile is a 1×1 sq., and the participant’s place is saved as a fancy 64-bit floating level tile place.

What I find out about this downside:

I do know that this downside includes rays casting outwards, the place the ray begins on the participant’s earlier tile place, and ends on the present tile place. If the ray’s endpoint is inside a stable tile, the participant’s place must be set someplace in between the beginning and endpoints of the ray, and one of many dimensions of the velocity vector will probably be set to zero. That is the place a lot of the struggles lie, or I could also be unsuitable someplace in there with out understanding.

The C# code:

Here is the code that I’ve that handles collision resolutions:

void HandleCollisions() {

    if (!vehicleTarget.IsNull) {
        return;
    }

    ComplexDouble previousPreciseTilePosition = preciseTile;
    MoveX(frameLengthSeconds);
    MoveY(frameLengthSeconds);
    ComplexDouble currentPreciseTilePosition = preciseTile;

    if (GameDebug.NoPlayerCollision || (GameDebug.FastMovement && MaxTileMovementSpeed != defaultMaxTileMovementSpeed)) return;

    //Go away the tactic if the earlier and present positions are equal, that means that the participant hasn't moved.
    if (previousPreciseTilePosition == currentPreciseTilePosition) return;

    //These two shift the coordinates to the right positions relying on the signal of the tile positions.
    int xShift = currentPreciseTilePosition.A < 0 ? 0 : 1;
    int yShift = currentPreciseTilePosition.B < 0 ? 0 : 1;

    ushort GetDecor(ComplexInt tileOffset) {
        strive {
            return MainWorld.GetDecorTile((ComplexInt)(preciseTile + (xShift - 1, yShift - 1)) + tileOffset, worldLayer);
        } catch (KeyNotFoundException) {
            return 0;
        }
    }

    //Take the mod one of many quantity, if it is adverse, add one, in order that the modulus would not go adverse and is per the constructive modulus.
    double ModOne(double num) {
        double consequence = num - (int)num;
        if (consequence < 0) {
            consequence++;
        }
        return consequence;
    }
    //Rounds down the double to an integer much like Math.Flooring, that is in order that the adverse values are per constructive values.
    int RoundDown(double num) {
        int consequence = (int)num;
        if (num < 0 && consequence != num) {
            result--;
        }
        return consequence;
    }

    ushort topLeftTile = GetDecor((0, 0));
    ushort topRightTile = GetDecor((1, 0));
    ushort bottomLeftTile = GetDecor((0, 1));
    ushort bottomRightTile = GetDecor((1, 1));

    ComplexInt movementNormal = (Math.Signal(unnormalizedMovementSpeed.A), Math.Signal((float)unnormalizedMovementSpeed.B));

    bool IsSolidTileID(ushort tileID) => tileID != 0
    && (TileDecorProperties.ClassificationOf(World.IDToDecor[tileID]) == TileDecorProperties.Classification.Stable ||
        TileDecorProperties.ClassificationOf(World.IDToDecor[tileID]) == TileDecorProperties.Classification.Tree);

    if (movementNormal.A == -1 && (IsSolidTileID(topLeftTile) || IsSolidTileID(bottomLeftTile))) {
        if (ModOne(currentPreciseTilePosition.A) > 0.5) {
            preciseTile.A = RoundDown(currentPreciseTilePosition.A) + 1;
            unnormalizedMovementSpeed.A = 0;
        }
    }
    if (movementNormal.A == 1 && (IsSolidTileID(topRightTile) || IsSolidTileID(bottomRightTile))) {
        if (ModOne(currentPreciseTilePosition.A) <= 0.5) {
            preciseTile.A = RoundDown(currentPreciseTilePosition.A);
            unnormalizedMovementSpeed.A = 0;
        }
    }
    if (movementNormal.B == -CMath.I && (IsSolidTileID(topLeftTile) || IsSolidTileID(topRightTile))) {
        if (ModOne((double)currentPreciseTilePosition.B) > 0.5) {
            preciseTile.B = RoundDown((double)currentPreciseTilePosition.B) + 1;
            unnormalizedMovementSpeed.B = 0;
        }
    }
    if (movementNormal.B == CMath.I && (IsSolidTileID(bottomLeftTile) || IsSolidTileID(bottomRightTile))) {
        if (ModOne((double)currentPreciseTilePosition.B) <= 0.5) {
            preciseTile.B = RoundDown((double)currentPreciseTilePosition.B);
            unnormalizedMovementSpeed.B = 0;
        }
    }
}

Some context within the code above

The variable “preciseTile” is the participant’s present tile place, and it is a advanced worth with actual and imaginary 64-bit float values.

“frameLengthSeconds” is a 32-bit float worth representing how a lot time has handed between the earlier and present body in seconds.

“vehicleTarget” initially would not relate to the issue, because it’s chargeable for understanding if the participant is in a spaceship or not, and is merely an optimization.

“GameDebug.NoPlayerCollision ||…” It is a boolean that merely turns collision on or off after I debug the sport, additionally would not relate to the issue.

Complicated quantity context

The x place correlates to “A”, and the y place correlates to “B”, and the advanced numbers can both be ComplexInt, ComplexFloat, or ComplexDouble. Here is instance code demonstrating my implementation of them.

ComplexInt num = (10, 4);
num += 1;
//num: 11 + 4i
num += CMath.I;
//num: 11 + 5i
num += (2, -3);
//num: 13 + 2i
Console.WriteLine(num.A);
//num: 13
Console.WriteLine(num.B);
//num: 2i
Console.WriteLine(num.B * num.B);
//num: -4

Listed below are the strategies that deal with participant motion.

non-public void MoveX(float frameLengthSeconds) {
    ComplexInt leftTile = (ComplexInt)preciseTile;
    ComplexInt rightTile = leftTile + 1;
    ushort leftDecorIndex;
    ushort rightDecorIndex;
    strive {
        leftDecorIndex = Game1.MainWorld.GetDecorTile(leftTile, worldLayer);
        rightDecorIndex = Game1.MainWorld.GetDecorTile(rightTile, worldLayer);
    } catch (KeyNotFoundException) {
        leftDecorIndex = 0;
        rightDecorIndex = 0;
    }
    //Slows down the participant if strolling on a tile like water/oil, or hurries up if strolling on a flooring
    if (leftDecorIndex * rightDecorIndex != 0) {
        double xLeft = preciseTile.A - (int)preciseTile.A;
        if (xLeft < 0) {
            xLeft++;
        }
        double xRight = 1 - xLeft;
        xLeft = 1 - xLeft;
        xRight = 1 - xRight;
        TileDecor leftDecor = TileDecorProperties.ToDecor(leftDecorIndex);
        TileDecor rightDecor = TileDecorProperties.ToDecor(rightDecorIndex);
        double movementMultiplier = TileDecorProperties.SpeedMultiplierOf(leftDecor) * xLeft;
        movementMultiplier += TileDecorProperties.SpeedMultiplierOf(rightDecor) * xRight;
        preciseTile += movementSpeed.A * frameLengthSeconds * movementMultiplier;
    } else {
        preciseTile += movementSpeed.A * frameLengthSeconds;
    }
}
non-public void MoveY(float frameLengthSeconds) {
    ComplexInt upperTile = (ComplexInt)preciseTile;
    ComplexInt lowerTile = upperTile + CMath.I;
    ushort upDecorIndex;
    ushort downDecorIndex;
    strive {
        upDecorIndex = Game1.MainWorld.GetDecorTile(upperTile, worldLayer);
        downDecorIndex = Game1.MainWorld.GetDecorTile(lowerTile, worldLayer);
    } catch (KeyNotFoundException) {
        upDecorIndex = 0;
        downDecorIndex = 0;
    }
    //Slows down the participant if strolling on a tile like water/oil, or hurries up if strolling on a flooring
    if (upDecorIndex * downDecorIndex != 0) {
        double xUp = (double)preciseTile.B - (int)preciseTile.B;
        if (xUp < 0) {
            xUp++;
        }
        double xDown = 1 - xUp;
        TileDecor upDecor = TileDecorProperties.ToDecor(upDecorIndex);
        TileDecor downDecor = TileDecorProperties.ToDecor(downDecorIndex);
        double movementMultiplier = TileDecorProperties.SpeedMultiplierOf(upDecor) * xUp;
        movementMultiplier += TileDecorProperties.SpeedMultiplierOf(downDecor) * xDown;
        preciseTile += movementSpeed.B * frameLengthSeconds * movementMultiplier;
    } else {
        preciseTile += movementSpeed.B * frameLengthSeconds;
    }
}

The present implementation of the collision decision solely kind of works. It does reach protecting the participant out of stable tiles more often than not, however it’s nonetheless has bugs.

The bugs I’ve discovered up to now:

  1. The participant will typically clip to the corners of tiles when near them.
  2. If the framerate is low, the participant can penetrate via stable tiles.
  3. If the participant strikes very quick, they’re going to additionally penetrate via stable tiles.

My present implementation in motion

The bug in action

Within the gif above, the participant will typically teleport over small distances to the closest tile to resolve to, and the participant will not slide in opposition to a wall and so they’ll simply clip in opposition to them when I attempt to transfer diagonally.

What I am in search of:

Since I attempted to make this downside simpler for me by using the truth that it is a sq. tilemap, I am going to go together with single level to tilemap collision resolutions as an alternative to maintain issues easy.

Here is the method that is probably the most promising for me, however I additionally had a tough time attempting to implement. I’ve a ray in a sq. grid, the place the scale of every sq. is 1 and I wish to get all of the factors which are intersecting this grid, and for the ensuing record of factors to be so as from the begin to the top of the ray.

I am in search of these blue factors within the instance photos under ordered from the primary quantity to the final one simply earlier than the arrow:

enter image description here
enter image description here

After getting these blue factors and figuring out if the participant has collided with a stable tile, I additionally one way or the other have to determine which facet the purpose it is on to know whether or not to set both the x or y velocity vector to 0 to allow the participant to slip in opposition to a wall, in addition to enabling journey via 1 tile extensive gaps.
I additionally need this resolution to eliminate the bug the place the participant will clip to the sides of the tile, and undergo stable tiles at low body charges or excessive speeds, even when it signifies that this resolution will permit the sprite to partially overlap partitions.

The query that I’ve, is how do I get hold of these blue factors so as from the begin to the top of the ray as 64-bit float values, and determine which facet of the tile the purpose is colliding with to be able to permit wall sliding and 1 tile extensive gaps?

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles