Hello everyone. I hope you’re doing well today.
I have had a disease plaguing my Player.cpp file. A disease so awful and stinky, it causes everything to feel worse as a result. A mold so prolific, it causes the entire experience to spoil.
There would be suspense here if you didn’t read the title.
I’m fixing the player->world collision function.
It is atrocious. Seriously. If you’re a programmer you may die. I advise you to look away.
This is the result of bodging together code until it works without any regard to readability or sensibility.
Behold, the pits of Hell:
auto toTileSpace = [](int x) { return x / 1_t; };
auto lookupColMap = [this](int y, int x) {
try {
auto& table = g_game->tileSet->tiles;
auto& grid = g_game->tileMap->tileData;
if (x < 0 || y < 0)
return 0Ui16;
if (y >= (int)grid.size())
return 0Ui16;
if (x >= (int)grid.at(y).size())
return 0Ui16;
else {
if (y > grid.size() || y < 0) {
return 0Ui16;
}
auto& row = grid.at(y);
if (x > row.size() || x < 0) {
return 0Ui16;
}
auto& col = row.at(x);
if (!col) {
return 0Ui16;
}
if (!table.contains(col))
return 0Ui16;
auto& tile = table.at(col);
if (tile)
return tile->opaqueCollision == true ? 1Ui16 : 0Ui16;
else
return 0Ui16;
}
}
catch (...) {
return 0Ui16;
}
};
auto tallLookupColMap = [this, lookupColMap](int y, int x) { return lookupColMap(y, x) | lookupColMap(y + 1, x); };
auto wideLookupColMap = [this, lookupColMap](int y, int x) { return lookupColMap(y, x) | lookupColMap(y, x + 1); };
// HACK:Collision is disabled if the coordinates are negative. This will
// need to be fixed eventually.
if (L.y < 0_z || L.x < 0_z) {
return;
}
try {
int playerX = (int)location.x;
int playerY = (int)location.y;
int tilePlayerX = toTileSpace(playerX);
int tilePlayerY = toTileSpace(playerY);
size_t xI = (size_t)L.x;
size_t yI = (size_t)L.y;
bool ignoreSnap = 0;
// Top, maybe.
if ((wideLookupColMap(tilePlayerY, tilePlayerX) == 1) && !ignoreSnap) {
velocity.vY = 0;
location.y = ((float)tilePlayerY + 1_z) * 1_t;
return;
;
}
// Left side collision
if (tallLookupColMap(tilePlayerY, toTileSpace((int)(playerX - 1_z))) == 1) {
double xCopy = location.x;
location.x = ((double)tilePlayerX * 1_t) + 1_z;
velocity.vX = 0;
if (xCopy - location.x > min(2.0_z, abs(velocity.vX * 2.0_z)))
location.x = (((double)tilePlayerX + 1_z) * 1_t) + 1_z;
}
// Right side collision
else if (tallLookupColMap(tilePlayerY, toTileSpace((int)(playerX + 1_t + 1_z))) == 1)
{
double xCopy = location.x;
location.x = (((double)tilePlayerX + 1_z) * 1_t) - 1_z;
velocity.vX = 0;
if (location.x - xCopy > min(2.0_z, abs(velocity.vX * 2.0_z)))
location.x = ((double)tilePlayerX * 1_t) - 1_z;
}
// Bottom; presumably.
if (
// tPX must be either 0 or +1 here because otherwise you fall
// off of the
// platform prematurely. (With 0 only, you fall off the second
// your X coordinate is less than the left edge of the
// platform. With 1 only, you fall off on the right side
// instead.)
(ignoreGround.count() <= 0) && (wideLookupColMap((int)(tilePlayerY + 2_z), tilePlayerX) == 1)
&& velocity.vY >= 0_z)
{
pStatus.Grounded = true;
if (!ignoreSnap && velocity.vY >= 0_z) {
velocity.vY = 0_z;
location.y = (16_z * (float)tilePlayerY);
pStatus.JumpsLeft = flags.HasWings ? 2 : 1;
}
}
else {
pStatus.Grounded = false;
}
}
catch (...) {
}
One hundred and three lines of pain.
A couple things to note beforehand: _t is a tile literal prefix, which essentially just multiplies the number by 16 (the tile size in game units). _z converts the literal to the Location’s internal type (ldec_t, which is double).
Let’s talk about all the shortcomings of this code.
The first thing we do is define two lambda functions toTileSpace and lookupColMap. ToTileSpace converts a coordinate to tile space. LookupColMap looks up the tile data and returns true if the tile is opaque.
Two extra lookups are created for “tall” and “wide” lookups, which were implemented to fix some weirdness that can occur and cause you to move through a floor.
This weird image shows a build of the game from almost a year ago, where I was using a nine-box system to determine world collisions. This code is from that version of the game.
The first thing we do is find the tile-space location of the player. We use this to determine what tiles near them are solid. If they are located in one, they are ejected out from it, relatively violently and only usually in the correct direction. Six points are used to determine these collisions: The corners and midpoints of the sides.
To stop you from getting caught by the ground when you jump, I added an ignoreGround timer, which disables all bottom collision. The bottom collision is also responsible for resetting your jump count.
Not only is this atrociously bad code, it isn’t even reusable for other entities – the code is so intertwined with Player, that it cannot be removed.
So I rewrote the entire thing. Finally.
entity_world_collision_result_t Collision::ProcessWorldCollision(Entity* ent) {
entity_world_collision_result_t res = { 0 };
res.invalidResult = true;
static auto& tilemap = g_game->tileMap->tileData;
static auto& tiledat = g_game->tileSet->tiles;
auto isTileWackyAsShit = [](Location l) {
l.locType = TILE;
if (!g_game->isLocationInMap(l)) {
return false;
}
else {
auto dat = tilemap[(int)l.y][(int)l.x];
return tiledat[dat]->opaqueCollision;
}
};
IMobile* mob = dynamic_cast<IMobile*>(ent);
if (mob == nullptr) {
return res;
}
Collider* col = dynamic_cast<Collider*>(ent);
if (col == nullptr) {
return res;
}
Location L = ent->location / 1_t;
bool checkLeft = mob->velocity.vX <= 0;
bool checkRight = mob->velocity.vX >= 0;
bool checkTop = mob->velocity.vY <= 0;
bool checkBottom = mob->velocity.vY >= 0;
bool detectedLeft = 0, detectedRight = 0, detectedTop = 0, detectedBottom = 0;
IERectangleF entBounds = col->getCollisionBounds();
int sampleRate = 1_t;
Location checkLoc = Location(0, 0);
for (int i = entBounds.y + (sampleRate / 4); i < entBounds.y + entBounds.h; i += (sampleRate / 2)) {
if (checkLeft) {
checkLoc = Location(entBounds.x / 1_t, i / 1_t, TILE);
detectedLeft |= isTileWackyAsShit(checkLoc);
}
if (checkRight) {
checkLoc = Location((entBounds.x + entBounds.w) / 1_t, i / 1_t, TILE);
detectedRight |= isTileWackyAsShit(checkLoc);
}
//Exit early if we have satisfied the conditions we are looking for.
if ((!checkLeft || detectedLeft) && (!checkRight || detectedRight)) {
break;
}
}
for (int i = entBounds.x + (sampleRate / 4); i < entBounds.x + entBounds.w; i += (sampleRate / 2)) {
if (checkTop) {
checkLoc = Location(i / 1_t, entBounds.y / 1_t, TILE);
detectedTop |= isTileWackyAsShit(checkLoc);
}
if (checkBottom) {
checkLoc = Location(i / 1_t, (entBounds.y + entBounds.h) / 1_t, TILE);
detectedBottom |= isTileWackyAsShit(checkLoc);
}
// Exit early if we have satisfied the conditions we are looking for.
if ((!checkTop || detectedTop) && (!checkBottom || detectedBottom)) {
break;
}
}
if (detectedLeft) {
mob->velocity.vX = 0;
// Eject right
ent->location.x = ceil(ent->location.x / 16.0) * 16.0;
}
if (detectedRight) {
mob->velocity.vX = 0;
// Eject left
ent->location.x = floor(ent->location.x / 16.0) * 16.0;
}
if (detectedTop) {
mob->velocity.vY = 0;
// Eject down
ent->location.y = ceil(ent->location.y / 16.0) * 16.0;
}
if (detectedBottom) {
mob->velocity.vY = 0;
//Eject up
ent->location.y = floor(ent->location.y / 16.0) * 16.0;
}
entity_world_collision_result_t ewcrt = { 0 };
ewcrt.invalidResult = false;
ewcrt.onGround = detectedBottom;
return ewcrt;
}
Not only is it six lines shorter, it’s a hell of a lot easier to read. It’s still not perfect, but it is much prettier.
For the first four lines, we just do some general setup for variables to use down the line. We then declare our only lambda, isTileWackyAsShit. I forgot to rename it before writing this, and I’m too lazy to find and replace it to be more sensible. Oh well, I’m not a professional anyway.
It simply looks up the tile’s data and tells if it is opaque or not. Then, we do some type checking to make sure we’re dealing with an entity that can move (for velocity) and also collide (for getCollisionBounds()). The player meets both of these interfaces (as does any LivingEntity). Then, we determine which sides we are checking based on the velocity. Once that’s done, we enter our main two loops.
In the first loop, we check the left and right sides, and in the second loop we check the top and bottom. We do not check at the corners because if your vY is positive (falling), then moving left or right on the ground will always detect a left or right collision, which means you are immobile on the ground. Admittedly, we could check only three points on the sides, but I think this is fine.
In the last few lines, we eject from any collisions using some convenient properties of our tile grid, and then return the result (which only consists of “are you on the ground>” and “did collision succeed?”.
This new collision system works immensely, reducing what I call “wacky collision behavior” to near-zero. There’s some issues when you land on the corner of a platform, caused by the lack of corner detection, but we’ll get there when we get there.
Dying
You can now die. Not really all that, it just resets your health and spawns you back at (0,0). But it plays a cool animation!
For you who noticed, I fixed it. For you who didn’t, don’t look for it.
Anyway see you later. Join my discord if you want.
Leave a Reply