#include "transform_system.h" #include "game.h" #include "map.h" bool checkCollisionOptionsContains(CheckCollisionOptions options, CheckCollisionOptions value) { return (static_cast(options) & static_cast(value)) != 0; } void TransformSystem::initSprite(int spriteId, vec2i loc, SpriteLayer layer) { Sprite& sprite = game_.getSprite(spriteId); sprite.loc = loc; sprite.layer = layer; spritesByY_[static_cast(layer)].emplace(loc.y(), spriteId); } void TransformSystem::setUpCollision(int spriteId, vec2i offset, vec2i size, bool solid) { Sprite& sprite = game_.getSprite(spriteId); sprite.collidable = true; sprite.collisionOffset = offset; sprite.collisionSize = size; sprite.solid = solid; addCollidable(spriteId); } void TransformSystem::undoCollision(int spriteId) { Sprite& sprite = game_.getSprite(spriteId); sprite.collidable = false; sprite.solid = false; removeCollidable(spriteId); } void TransformSystem::moveSprite(int spriteId, vec2i newLoc) { Sprite& sprite = game_.getSprite(spriteId); if (sprite.collidable) { removeCollidable(spriteId); } bool changedY = (sprite.loc.y() != newLoc.y()); if (changedY) { spritesByY_[static_cast(sprite.layer)].erase(std::make_tuple(sprite.loc.y(), spriteId)); } sprite.loc = newLoc; if (changedY) { spritesByY_[static_cast(sprite.layer)].emplace(newLoc.y(), spriteId); } if (sprite.collidable) { addCollidable(spriteId); } } CollisionResult TransformSystem::checkCollision(int spriteId, vec2i curLoc, vec2i newLoc, Direction dir, CheckCollisionOptions options) { CollisionResult result; Sprite& sprite = game_.getSprite(spriteId); const Map& map = game_.getMap(); bool blocked = false; vec2i mapBounds = map.getMapSize() * map.getTileSize(); // First check horizontal movement only. vec2i horizMovement = { newLoc.x(), curLoc.y() }; bool horizBlocked = false; vec2i oldColUL = curLoc + sprite.collisionOffset; vec2i oldColDR = oldColUL + sprite.collisionSize - vec2i{1,1}; vec2i newColUL = horizMovement + sprite.collisionOffset; vec2i newColDR = newColUL + sprite.collisionSize - vec2i{1,1}; vec2i oldTileUL = oldColUL / map.getTileSize(); vec2i newTileUL = newColUL / map.getTileSize(); vec2i oldTileDR = oldColDR / map.getTileSize(); vec2i newTileDR = newColDR / map.getTileSize(); const Zone* enclosureZone = nullptr; if (!sprite.enclosureZone.empty()) { enclosureZone = &map.getZone(sprite.enclosureZone); } if (dirHasDir(dir, Direction::right)) { if (!sprite.floating && newTileDR.x() > oldTileDR.x() && newColDR.x() < mapBounds.w()) { for (int y = newTileUL.y(); y <= newTileDR.y(); y++) { if (map.isBlocked(newTileDR.x(), y)) { horizBlocked = true; result.dir = Direction::right; break; } } if (checkCollisionOptionsContains(options, CheckCollisionOptions::AllowSliding) && sprite.sliding && dir == Direction::right && horizBlocked && oldTileUL.y() != oldTileDR.y()) { // If sliding is enabled for this sprite, check if we can slide // either perpendicular direction. Because sliding can only happen // if we were moving in a cardinal direction, we can use tail // recursion and exit early. if (!map.isBlocked(oldTileDR.x()+1, oldTileUL.y())) { return checkCollision(spriteId, curLoc, curLoc - vec2i{0,1} * sprite.movementSpeed, Direction::up); } else if (!map.isBlocked(oldTileDR.x()+1, oldTileDR.y())) { return checkCollision(spriteId, curLoc, curLoc + vec2i{0,1} * sprite.movementSpeed, Direction::down); } } } if (!horizBlocked && enclosureZone) { if (oldColDR.x() <= enclosureZone->dr.x()-1 && newColDR.x() > enclosureZone->dr.x()-1) { horizBlocked = true; result.dir = Direction::right; } } if (!horizBlocked) { auto it = rightCollidables_.lower_bound({oldColDR.x()+1, 0}); for (; (it != std::end(rightCollidables_) && std::get<0>(it->first) <= newColDR.x()); it++) { if (newColDR.y() >= it->second.lower && newColUL.y() <= it->second.upper) { int colliderSpriteId = std::get<1>(it->first); Sprite& collider = game_.getSprite(colliderSpriteId); horizBlocked = collider.solid; result.dir = Direction::right; result.colliders.push_back(colliderSpriteId); break; } } } } if (dirHasDir(dir, Direction::left)) { if (!sprite.floating && newTileUL.x() < oldTileUL.x() && newColUL.x() >= 0) { for (int y = newTileUL.y(); y <= newTileDR.y(); y++) { if (map.isBlocked(newTileUL.x(), y)) { horizBlocked = true; result.dir = Direction::left; break; } } if (checkCollisionOptionsContains(options, CheckCollisionOptions::AllowSliding) && sprite.sliding && dir == Direction::left && horizBlocked && oldTileUL.y() != oldTileDR.y()) { // If sliding is enabled for this sprite, check if we can slide // either perpendicular direction. Because sliding can only happen // if we were moving in a cardinal direction, we can use tail // recursion and exit early. if (!map.isBlocked(oldTileUL.x()-1, oldTileUL.y())) { return checkCollision(spriteId, curLoc, curLoc - vec2i{0,1} * sprite.movementSpeed, Direction::up); } else if (!map.isBlocked(oldTileUL.x()-1, oldTileDR.y())) { return checkCollision(spriteId, curLoc, curLoc + vec2i{0,1} * sprite.movementSpeed, Direction::down); } } } if (!horizBlocked && enclosureZone) { if (oldColUL.x() >= enclosureZone->ul.x() && newColUL.x() < enclosureZone->ul.x()) { horizBlocked = true; result.dir = Direction::left; } } if (!horizBlocked) { auto it = leftCollidables_.lower_bound({oldColUL.x()-1, INT_MAX}); for (; (it != std::end(leftCollidables_) && std::get<0>(it->first) >= newColUL.x()); it++) { if (newColDR.y() >= it->second.lower && newColUL.y() <= it->second.upper) { int colliderSpriteId = std::get<1>(it->first); Sprite& collider = game_.getSprite(colliderSpriteId); horizBlocked = collider.solid; result.dir = Direction::left; result.colliders.push_back(colliderSpriteId); break; } } } } // For vertical movement, assume that the horizontal movement has already // taken place (as long as it was not in fact blocked). vec2i effectiveCurLoc = curLoc; vec2i vertMovement = newLoc; if (horizBlocked) { vertMovement.x() = curLoc.x(); } else { effectiveCurLoc.x() = newLoc.x(); } bool vertBlocked = false; oldColUL = effectiveCurLoc + sprite.collisionOffset; oldColDR = oldColUL + sprite.collisionSize - vec2i{1,1}; newColUL = vertMovement + sprite.collisionOffset; newColDR = newColUL + sprite.collisionSize - vec2i{1,1}; oldTileUL = oldColUL / map.getTileSize(); newTileUL = newColUL / map.getTileSize(); oldTileDR = oldColDR / map.getTileSize(); newTileDR = newColDR / map.getTileSize(); if (dirHasDir(dir, Direction::down)) { if (!sprite.floating && newTileDR.y() > oldTileDR.y() && newColDR.y() < mapBounds.h()) { for (int x = newTileUL.x(); x <= newTileDR.x(); x++) { if (map.isBlocked(x, newTileDR.y())) { vertBlocked = true; result.dir = Direction::down; break; } } if (checkCollisionOptionsContains(options, CheckCollisionOptions::AllowSliding) && sprite.sliding && dir == Direction::down && vertBlocked && oldTileUL.x() != oldTileDR.x()) { // If sliding is enabled for this sprite, check if we can slide // either perpendicular direction. Because sliding can only happen // if we were moving in a cardinal direction, we can use tail // recursion and exit early. if (!map.isBlocked(oldTileUL.x(), oldTileDR.y()+1)) { return checkCollision(spriteId, curLoc, curLoc - vec2i{1,0} * sprite.movementSpeed, Direction::left); } else if (!map.isBlocked(oldTileDR.x(), oldTileDR.y()+1)) { return checkCollision(spriteId, curLoc, curLoc + vec2i{1,0} * sprite.movementSpeed, Direction::right); } } } if (!vertBlocked && enclosureZone) { if (oldColDR.y() <= enclosureZone->dr.y()-1 && newColDR.y() > enclosureZone->dr.y()-1) { vertBlocked = true; result.dir = Direction::down; } } if (!vertBlocked) { auto it = downCollidables_.lower_bound({oldColDR.y()+1, 0}); for (; (it != std::end(downCollidables_) && std::get<0>(it->first) <= newColDR.y()); it++) { if (newColDR.x() >= it->second.lower && newColUL.x() <= it->second.upper) { int colliderSpriteId = std::get<1>(it->first); Sprite& collider = game_.getSprite(colliderSpriteId); vertBlocked = collider.solid; result.dir = Direction::down; result.colliders.push_back(colliderSpriteId); break; } } } } if (dirHasDir(dir, Direction::up)) { if (!sprite.floating && newTileUL.y() < oldTileUL.y() && newColUL.y() >= 0) { for (int x = newTileUL.x(); x <= newTileDR.x(); x++) { if (map.isBlocked(x, newTileUL.y())) { vertBlocked = true; result.dir = Direction::up; break; } } if (checkCollisionOptionsContains(options, CheckCollisionOptions::AllowSliding) && sprite.sliding && dir == Direction::up && vertBlocked && oldTileUL.x() != oldTileDR.x()) { // If sliding is enabled for this sprite, check if we can slide // either perpendicular direction. Because sliding can only happen // if we were moving in a cardinal direction, we can use tail // recursion and exit early. if (!map.isBlocked(oldTileUL.x(), oldTileUL.y()-1)) { return checkCollision(spriteId, curLoc, curLoc - vec2i{1,0} * sprite.movementSpeed, Direction::left); } else if (!map.isBlocked(oldTileDR.x(), oldTileUL.y()-1)) { return checkCollision(spriteId, curLoc, curLoc + vec2i{1,0} * sprite.movementSpeed, Direction::right); } } } if (!vertBlocked && enclosureZone) { if (oldColUL.y() >= enclosureZone->ul.y() && newColUL.y() < enclosureZone->ul.y()) { vertBlocked = true; result.dir = Direction::up; } } if (!vertBlocked) { auto it = upCollidables_.lower_bound({oldColUL.y()-1, INT_MAX}); for (; (it != std::end(upCollidables_) && std::get<0>(it->first) >= newColUL.y()); it++) { if (newColDR.x() >= it->second.lower && newColUL.x() <= it->second.upper) { int colliderSpriteId = std::get<1>(it->first); Sprite& collider = game_.getSprite(colliderSpriteId); vertBlocked = collider.solid; result.dir = Direction::up; result.colliders.push_back(colliderSpriteId); break; } } } } result.adjustedLoc = vertMovement; if (vertBlocked) { result.adjustedLoc.y() = effectiveCurLoc.y(); } result.blocked = horizBlocked || vertBlocked; return result; } CharacterMedium TransformSystem::getMediumAtPosition(int spriteId, vec2i newLoc) { Sprite& sprite = game_.getSprite(spriteId); const Map& map = game_.getMap(); vec2i newColUL = newLoc + sprite.collisionOffset; vec2i newColDR = newColUL + sprite.collisionSize - vec2i{1,1}; vec2i newTileUL = newColUL / map.getTileSize(); vec2i newTileDR = newColDR / map.getTileSize(); CharacterMedium result = CharacterMedium::Normal; for (int y=newTileUL.y(); y<=newTileDR.y(); y++) { for (int x=newTileUL.x(); x<=newTileDR.x(); x++) { CharacterMedium tileMedium = map.getMedium(x, y); if (tileMedium > result) { result = tileMedium; } } } return result; } void TransformSystem::addCollidable(int spriteId) { Sprite& sprite = game_.getSprite(spriteId); vec2i colUL = sprite.loc + sprite.collisionOffset; vec2i colDR = colUL + sprite.collisionSize; leftCollidables_.emplace(std::piecewise_construct, std::tie(colDR.x(), spriteId), std::tie(colUL.y(), colDR.y())); rightCollidables_.emplace(std::piecewise_construct, std::tie(colUL.x(), spriteId), std::tie(colUL.y(), colDR.y())); upCollidables_.emplace(std::piecewise_construct, std::tie(colDR.y(), spriteId), std::tie(colUL.x(), colDR.x())); downCollidables_.emplace(std::piecewise_construct, std::tie(colUL.y(), spriteId), std::tie(colUL.x(), colDR.x())); } void TransformSystem::removeCollidable(int spriteId) { Sprite& sprite = game_.getSprite(spriteId); vec2i colUL = sprite.loc + sprite.collisionOffset; vec2i colDR = colUL + sprite.collisionSize; leftCollidables_.erase({ colDR.x(), spriteId }); rightCollidables_.erase({ colUL.x(), spriteId }); upCollidables_.erase({ colDR.y(), spriteId }); downCollidables_.erase({ colUL.y(), spriteId }); } void TransformSystem::destroySprite(int spriteId) { Sprite& sprite = game_.getSprite(spriteId); spritesByY_[static_cast(sprite.layer)].erase(std::make_tuple(sprite.loc.y(), spriteId)); if (sprite.collidable) { removeCollidable(spriteId); } }