/* olcPGEX_RayCastWorld.h +-------------------------------------------------------------+ | OneLoneCoder Pixel Game Engine Extension | | Ray Cast World v1.02 | +-------------------------------------------------------------+ NOTE: UNDER ACTIVE DEVELOPMENT - THERE ARE BUGS/GLITCHES What is this? ~~~~~~~~~~~~~ This is an extension to the olcPixelGameEngine, which provides a quick and easy to use, flexible, skinnable, ray-cast 3D world engine, which handles all graphics and collisions within a pseudo 3D world. It is designed to be implementation independent. Please see example files for usage instructions. Video: https://youtu.be/Vij_obgv9h4 License (OLC-3) ~~~~~~~~~~~~~~~ Copyright 2018 - 2020 OneLoneCoder.com Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions or derivations of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions or derivative works in binary form must reproduce the above copyright notice. This list of conditions and the following disclaimer must be reproduced in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Links ~~~~~ YouTube: https://www.youtube.com/javidx9 Discord: https://discord.gg/WhwHUMV Twitter: https://www.twitter.com/javidx9 Twitch: https://www.twitch.tv/javidx9 GitHub: https://www.github.com/onelonecoder Homepage: https://www.onelonecoder.com Author ~~~~~~ David Barr, aka javidx9, ŠOneLoneCoder 2019, 2020 Revisions: 1.00: Initial Release 1.01: Fix NaN check on overlap distance (Thanks Dandistine) 1.02: Added dynamic step size for collisions */ #ifndef OLC_PGEX_RAYCASTWORLD_H #define OLC_PGEX_RAYCASTWORLD_H #include #include namespace olc { namespace rcw { // Base class for objects that exist in world class Object { public: // Linkage to user object description uint32_t nGenericID = 0; // Position in tile/world space olc::vf2d pos; // Velocity in tile/world space olc::vf2d vel; // Speed of object float fSpeed = 0.0f; // Angular direction of object float fHeading = 0.0f; // Collision radius of object float fRadius = 0.5f; // Is drawn? bool bVisible = true; // Flag to be removed form world bool bRemove = false; // Can collide with scenery? bool bCollideWithScenery = true; // Notify scenery collision? bool bNotifySceneryCollision = false; // Can collide with other objects? bool bCollideWithObjects = false; // Notify object collisions? bool bNotifyObjectCollision = false; // Can this object be moved by another object? bool bCanBeMoved = true; // Has physics/collisions applied bool bIsActive = true; void Walk(const float fWalkSpeed); void Strafe(const float fStrafeSpeed); void Turn(const float fTurnSpeed); void Stop(); }; // The RayCastWorld Engine - Inherit from this, implement abstract // methods, call Update() and Render() when required class Engine : public olc::PGEX { public: // Identifies side of cell enum class CellSide { North, East, South, West, Top, Bottom, }; public: // Construct world rednering parameters Engine(const int screen_w, const int screen_h, const float fov); protected: // ABSTRACT - User must return a suitable olc::Pixel depending on world location information provided virtual olc::Pixel SelectSceneryPixel(const int tile_x, const int tile_y, const olc::rcw::Engine::CellSide side, const float sample_x, const float sample_y, const float distance) = 0; // ABSTRACT - User must return a boolean indicating if the tile is solid or not virtual bool IsLocationSolid(const float tile_x, const float tile_y) = 0; // ABSTRACT - User must return sizes of requested objects in Unit Cell Size virtual float GetObjectWidth(const uint32_t id) = 0; virtual float GetObjectHeight(const uint32_t id) = 0; // ABSTRACT - User must return suitable olc::Pixel for object sprite sample location virtual olc::Pixel SelectObjectPixel(const uint32_t id, const float sample_x, const float sample_y, const float distance, const float angle) = 0; // OPTIONAL - User can handle collsiion response with scenery should they choose to virtual void HandleObjectVsScenery(std::shared_ptr object, const int tile_x, const int tile_y, const olc::rcw::Engine::CellSide side, const float offset_x, const float offset_y); // OPTIONAL - User can handle collsiion response with objects should they choose to virtual void HandleObjectVsObject(std::shared_ptr object1, std::shared_ptr object2); public: // Sets In-Game Camera position void SetCamera(const olc::vf2d& pos, const float heading); // Called to update world state virtual void Update(float fElapsedTime); // Called to draw the world and its contents void Render(); public: std::unordered_map> mapObjects; private: // A convenient utility struct to store all the info required to understand how a ray // has hit a specific tile struct sTileHit { olc::vi2d vTilePos = { 0,0 }; olc::vf2d vHitPos = { 0,0 }; float fLength = 0.0f; float fSampleX = 0.0f; Engine::CellSide eSide = Engine::CellSide::North; }; // Cast ray into tile world, and return info about what it hits (if anything) bool CastRayDDA(const olc::vf2d& vOrigin, const olc::vf2d& vDirection, sTileHit& hit); // Convenient constants in algorithms const olc::vi2d vScreenSize; const olc::vi2d vHalfScreenSize; const olc::vf2d vFloatScreenSize; float fFieldOfView = 0.0f; // A depth buffer used to sort pixels in Z-Axis std::unique_ptr pDepthBuffer; // Local store of camera position and direction olc::vf2d vCameraPos = { 5.0f, 5.0f }; float fCameraHeading = 0.0f; }; } } #ifdef OLC_PGEX_RAYCASTWORLD #undef OLC_PGEX_RAYCASTWORLD #undef min #undef max void olc::rcw::Object::Walk(const float fWalkSpeed) { fSpeed = fWalkSpeed; vel = olc::vf2d(std::cos(fHeading), std::sin(fHeading)) * fSpeed; } void olc::rcw::Object::Strafe(const float fStrafeSpeed) { fSpeed = fStrafeSpeed; vel = olc::vf2d(std::cos(fHeading), std::sin(fHeading)).perp() * fSpeed; } void olc::rcw::Object::Turn(const float fTurnSpeed) { fHeading += fTurnSpeed; // Wrap heading to sensible angle if (fHeading < -3.14159f) fHeading += 2.0f * 3.14159f; if (fHeading > 3.14159f) fHeading -= 2.0f * 3.14159f; } void olc::rcw::Object::Stop() { fSpeed = 0; vel = { 0,0 }; } olc::rcw::Engine::Engine(const int screen_w, const int screen_h, const float fov) : vScreenSize(screen_w, screen_h), vHalfScreenSize(screen_w / 2, screen_h / 2), vFloatScreenSize(float(screen_w), float(screen_h)) { fFieldOfView = fov; pDepthBuffer.reset(new float[vScreenSize.x * vScreenSize.y]); } void olc::rcw::Engine::SetCamera(const olc::vf2d& pos, const float heading) { vCameraPos = pos; fCameraHeading = heading; } void olc::rcw::Engine::Update(float fElapsedTime) { // Update the position and statically resolve for collisions against the map for (auto& ob : mapObjects) { std::shared_ptr object = ob.second; if (!object->bIsActive) continue; int nSteps = 1; float fDelta = fElapsedTime; float fTotalTravel = (object->vel * fElapsedTime).mag2(); float fTotalRadius = (object->fRadius * object->fRadius); if(fTotalTravel >= fTotalRadius) { float fSteps = std::ceil(fTotalTravel / fTotalRadius); nSteps = int(fSteps); fDelta = fElapsedTime / fSteps; } for (int nStep = 0; nStep < nSteps; nStep++) { // Determine where object is trying to be olc::vf2d vPotentialPosition = object->pos + object->vel * fDelta; // If the object can collide with other objects if (object->bCollideWithObjects) { // Iterate through all other objects (this can be costly) for (auto& ob2 : mapObjects) { std::shared_ptr target = ob2.second; // Ignore if target object cant interact if (!target->bCollideWithObjects) continue; // Don't test against self if (target == object) continue; // Quick check to see if objects overlap... if ((target->pos - object->pos).mag2() <= (target->fRadius + object->fRadius) * (target->fRadius + object->fRadius)) { // ..they do. Calculate displacement required float fDistance = (target->pos - object->pos).mag(); float fOverlap = 1.0f * (fDistance - object->fRadius - target->fRadius); // Object will always give way to target vPotentialPosition -= (object->pos - target->pos) / fDistance * fOverlap; if (target->bCanBeMoved) target->pos += (object->pos - target->pos) / fDistance * fOverlap; if (object->bNotifyObjectCollision) HandleObjectVsObject(object, target); } } } // If the object can collide with scenery... if (object->bCollideWithScenery) { // ...Determine an area of cells to check for collision. We use a region // to account for diagonal collisions, and corner collisions. olc::vi2d vCurrentCell = object->pos; olc::vi2d vTargetCell = vPotentialPosition; olc::vi2d vAreaTL = { std::min(vCurrentCell.x, vTargetCell.x) - 1, std::min(vCurrentCell.y, vTargetCell.y) - 1 }; olc::vi2d vAreaBR = { std::max(vCurrentCell.x, vTargetCell.x) + 1, std::max(vCurrentCell.y, vTargetCell.y) + 1 }; // Iterate through each cell in test area olc::vi2d vCell; for (vCell.y = vAreaTL.y; vCell.y <= vAreaBR.y; vCell.y++) { for (vCell.x = vAreaTL.x; vCell.x <= vAreaBR.x; vCell.x++) { // Check if the cell is actually solid... olc::vf2d vCellMiddle = olc::vf2d(float(vCell.x) + 0.5f, float(vCell.y) + 0.5f); if (IsLocationSolid(vCellMiddle.x, vCellMiddle.y)) { // ...it is! So work out nearest point to future player position, around perimeter // of cell rectangle. We can test the distance to this point to see if we have // collided. olc::vf2d vNearestPoint; // Inspired by this (very clever btw) // https://stackoverflow.com/questions/45370692/circle-rectangle-collision-response vNearestPoint.x = std::max(float(vCell.x), std::min(vPotentialPosition.x, float(vCell.x + 1))); vNearestPoint.y = std::max(float(vCell.y), std::min(vPotentialPosition.y, float(vCell.y + 1))); // But modified to work :P olc::vf2d vRayToNearest = vNearestPoint - vPotentialPosition; float fOverlap = object->fRadius - vRayToNearest.mag(); if (std::isnan(fOverlap)) fOverlap = 0;// Thanks Dandistine! // If overlap is positive, then a collision has occurred, so we displace backwards by the // overlap amount. The potential position is then tested against other tiles in the area // therefore "statically" resolving the collision if (fOverlap > 0) { // Statically resolve the collision vPotentialPosition = vPotentialPosition - vRayToNearest.norm() * fOverlap; // Notify system that a collision has occurred if (object->bNotifySceneryCollision) { olc::rcw::Engine::CellSide side = olc::rcw::Engine::CellSide::Bottom; if (vNearestPoint.x == float(vCell.x)) side = olc::rcw::Engine::CellSide::West; if (vNearestPoint.x == float(vCell.x + 1)) side = olc::rcw::Engine::CellSide::East; if (vNearestPoint.y == float(vCell.y)) side = olc::rcw::Engine::CellSide::North; if (vNearestPoint.y == float(vCell.y + 1)) side = olc::rcw::Engine::CellSide::South; HandleObjectVsScenery(object, vCell.x, vCell.y, side, vNearestPoint.x - float(vCell.x), vNearestPoint.y - float(vCell.y)); } } } } } } // Set the objects new position to the allowed potential position object->pos = vPotentialPosition; } } } void olc::rcw::Engine::Render() { // Utility lambda to draw to screen and depth buffer auto DepthDraw = [&](int x, int y, float z, olc::Pixel p) { if (z <= pDepthBuffer[y * vScreenSize.x + x]) { pge->Draw(x, y, p); pDepthBuffer[y * vScreenSize.x + x] = z; } }; // Clear screen and depth buffer ======================================== // pge->Clear(olc::BLACK); <- Left to user to decide for (int i = 0; i < vScreenSize.x * vScreenSize.y; i++) pDepthBuffer[i] = INFINITY; // Draw World =========================================================== // For each column on screen... for (int x = 0; x < vScreenSize.x; x++) { // ...create a ray eminating from player position into world... float fRayAngle = (fCameraHeading - (fFieldOfView / 2.0f)) + (float(x) / vFloatScreenSize.x) * fFieldOfView; // ...create unit vector for that ray... olc::vf2d vRayDirection = { std::cos(fRayAngle), std::sin(fRayAngle) }; // ... and cast ray into world, see what it hits (if anything) sTileHit hit; // Assuming it hits nothing, then we draw to the middle of the screen (far far away) float fRayLength = INFINITY; // Otherwise... if (CastRayDDA(vCameraPos, vRayDirection, hit)) { // It has hit something, so extract information to draw column olc::vf2d vRay = hit.vHitPos - vCameraPos; // Length of ray is vital for pseudo-depth, but we'll also cosine correct to remove fisheye fRayLength = vRay.mag() * std::cos(fRayAngle - fCameraHeading); } // Calculate locations in column that divides ceiling, wall and floor float fCeiling = (vFloatScreenSize.y / 2.0f) - (vFloatScreenSize.y / fRayLength); float fFloor = vFloatScreenSize.y - fCeiling; float fWallHeight = fFloor - fCeiling; float fFloorHeight = vFloatScreenSize.y - fFloor; // Now draw the column from top to bottom for (int y = 0; y < vScreenSize.y; y++) { if (y <= int(fCeiling)) { // For floors and ceilings, we don't use the ray, instead we just pseudo-project // a plane, a la Mode 7. First calculate depth into screen... float fPlaneZ = (vFloatScreenSize.y / 2.0f) / ((vFloatScreenSize.y / 2.0f) - float(y)); // ... then project polar coordinate (r, theta) from camera into screen (x, y), again // compensating with cosine to remove fisheye olc::vf2d vPlanePoint = vCameraPos + vRayDirection * fPlaneZ * 2.0f / std::cos(fRayAngle - fCameraHeading); // Work out which planar tile we are in int nPlaneTileX = int(vPlanePoint.x); int nPlaneTileY = int(vPlanePoint.y); // Work out normalised offset into planar tile float fPlaneSampleX = vPlanePoint.x - nPlaneTileX; float fPlaneSampleY = vPlanePoint.y - nPlaneTileY; // Location is marked as ceiling olc::Pixel pixel = SelectSceneryPixel(nPlaneTileX, nPlaneTileY, olc::rcw::Engine::CellSide::Top, fPlaneSampleX, fPlaneSampleY, fPlaneZ); // Draw ceiling pixel - no depth buffer required pge->Draw(x, y, pixel); } else if (y > int(fCeiling) && y <= int(fFloor)) { // Location is marked as wall. Here we will sample the appropriate // texture at the hit location. The U sample coordinate is provided // by the "hit" structure, though we will need to scan the V sample // coordinate based upon the height of the wall in screen space // Create normalised "V" based on height of wall --> (0.0 to 1.0) float fSampleY = (float(y) - fCeiling) / fWallHeight; // Select appropriate texture of that wall olc::Pixel pixel = SelectSceneryPixel(hit.vTilePos.x, hit.vTilePos.y, hit.eSide, hit.fSampleX, fSampleY, fRayLength); // Finally draw the screen pixel doing a depth test too DepthDraw(x, y, fRayLength, pixel); } else { // For floors and ceilings, we don't use the ray, instead we just pseudo-project // a plane, a la Mode 7. First calculate depth into screen... float fPlaneZ = (vFloatScreenSize.y / 2.0f) / (float(y) - (vFloatScreenSize.y / 2.0f)); // ... then project polar coordinate (r, theta) from camera into screen (x, y), again // compensating with cosine to remove fisheye olc::vf2d vPlanePoint = vCameraPos + vRayDirection * fPlaneZ * 2.0f / std::cos(fRayAngle - fCameraHeading); // Work out which planar tile we are in int nPlaneTileX = int(vPlanePoint.x); int nPlaneTileY = int(vPlanePoint.y); // Work out normalised offset into planar tile float fPlaneSampleX = vPlanePoint.x - nPlaneTileX; float fPlaneSampleY = vPlanePoint.y - nPlaneTileY; // Location is marked as floor olc::Pixel pixel = SelectSceneryPixel(nPlaneTileX, nPlaneTileY, olc::rcw::Engine::CellSide::Bottom, fPlaneSampleX, fPlaneSampleY, fPlaneZ); // Draw floor pixel - no depth buffer required pge->Draw(x, y, pixel); } } } // Scenery is now drawn, and depth buffer is filled. We can now draw // the ingame objects. Assuming binary transparency, we've no need to // sort objects // Iterate through all in game objects for (const auto& ob : mapObjects) { const std::shared_ptr object = ob.second; // If object is invisible, nothing to do - this is useful // for both effects, and making sure we dont render the // "player" at the camera location perhaps if (!object->bVisible) continue; // Create vector from camera to object olc::vf2d vObject = object->pos - vCameraPos; // Calculate distance object is away from camera float fDistanceToObject = vObject.mag(); // Check if object center is within camera FOV... float fObjectAngle = atan2f(vObject.y, vObject.x) - fCameraHeading; if (fObjectAngle < -3.14159f) fObjectAngle += 2.0f * 3.14159f; if (fObjectAngle > 3.14159f) fObjectAngle -= 2.0f * 3.14159f; // ...with a bias based upon distance - allows us to have object centers offscreen bool bInPlayerFOV = fabs(fObjectAngle) < (fFieldOfView + (1.0f / fDistanceToObject)) / 2.0f; // If object is within view, and not too close to camera, draw it! if (bInPlayerFOV && vObject.mag() >= 0.5f) { // Work out its position on the floor... olc::vf2d vFloorPoint; // Horizontal screen location is determined based on object angle relative to camera heading vFloorPoint.x = (0.5f * ((fObjectAngle / (fFieldOfView * 0.5f))) + 0.5f) * vFloatScreenSize.x; // Vertical screen location is projected distance vFloorPoint.y = (vFloatScreenSize.y / 2.0f) + (vFloatScreenSize.y / fDistanceToObject) / std::cos(fObjectAngle / 2.0f); // First we need the objects size... olc::vf2d vObjectSize = { float(GetObjectWidth(object->nGenericID)), float(GetObjectHeight(object->nGenericID)) }; // ...which we can scale into world space (maintaining aspect ratio)... vObjectSize *= 2.0f * vFloatScreenSize.y; // ...then project into screen space vObjectSize /= fDistanceToObject; // Second we need the objects top left position in screen space... olc::vf2d vObjectTopLeft; // ...which is relative to the objects size and assumes the middle of the object is // the location in world space vObjectTopLeft = { vFloorPoint.x - vObjectSize.x / 2.0f, vFloorPoint.y - vObjectSize.y }; // Now iterate through the objects screen pixels for (float y = 0; y < vObjectSize.y; y++) { for (float x = 0; x < vObjectSize.x; x++) { // Create a normalised sample coordinate float fSampleX = x / vObjectSize.x; float fSampleY = y / vObjectSize.y; // Get pixel from a suitable texture float fNiceAngle = fCameraHeading - object->fHeading + 3.14159f / 4.0f; if (fNiceAngle < 0) fNiceAngle += 2.0f * 3.14159f; if (fNiceAngle > 2.0f * 3.14159f) fNiceAngle -= 2.0f * 3.14159f; olc::Pixel p = SelectObjectPixel(object->nGenericID, fSampleX, fSampleY, fDistanceToObject, fNiceAngle); // Calculate screen pixel location olc::vi2d a = { int(vObjectTopLeft.x + x), int(vObjectTopLeft.y + y) }; // Check if location is actually on screen (to not go OOB on depth buffer) // and if the pixel is indeed visible (has no transparency component) if (a.x >= 0 && a.x < vScreenSize.x && a.y >= 0 && a.y < vScreenSize.y && p.a == 255) { // Draw the pixel taking into account the depth buffer DepthDraw(a.x, a.y, fDistanceToObject, p); } } } } } } void olc::rcw::Engine::HandleObjectVsScenery(std::shared_ptr object, const int tile_x, const int tile_y, const olc::rcw::Engine::CellSide side, const float offset_x, const float offset_y) {} void olc::rcw::Engine::HandleObjectVsObject(std::shared_ptr object1, std::shared_ptr object2) {} // Will be explained in upcoming video... bool olc::rcw::Engine::CastRayDDA(const olc::vf2d& vOrigin, const olc::vf2d& vDirection, sTileHit& hit) { olc::vf2d vRayDelta = { sqrt(1 + (vDirection.y / vDirection.x) * (vDirection.y / vDirection.x)), sqrt(1 + (vDirection.x / vDirection.y) * (vDirection.x / vDirection.y)) }; olc::vi2d vMapCheck = vOrigin; olc::vf2d vSideDistance; olc::vi2d vStepDistance; if (vDirection.x < 0) { vStepDistance.x = -1; vSideDistance.x = (vOrigin.x - (float)vMapCheck.x) * vRayDelta.x; } else { vStepDistance.x = 1; vSideDistance.x = ((float)vMapCheck.x + 1.0f - vOrigin.x) * vRayDelta.x; } if (vDirection.y < 0) { vStepDistance.y = -1; vSideDistance.y = (vOrigin.y - (float)vMapCheck.y) * vRayDelta.y; } else { vStepDistance.y = 1; vSideDistance.y = ((float)vMapCheck.y + 1.0f - vOrigin.y) * vRayDelta.y; } olc::vf2d vIntersection; olc::vi2d vHitTile; float fMaxDistance = 100.0f; float fDistance = 0.0f; bool bTileFound = false; while (!bTileFound && fDistance < fMaxDistance) { if (vSideDistance.x < vSideDistance.y) { vSideDistance.x += vRayDelta.x; vMapCheck.x += vStepDistance.x; } else { vSideDistance.y += vRayDelta.y; vMapCheck.y += vStepDistance.y; } olc::vf2d rayDist = { (float)vMapCheck.x - vOrigin.x, (float)vMapCheck.y - vOrigin.y }; fDistance = rayDist.mag(); if (IsLocationSolid(float(vMapCheck.x), float(vMapCheck.y))) { vHitTile = vMapCheck; bTileFound = true; hit.vTilePos = vMapCheck; // Find accurate Hit Location float m = vDirection.y / vDirection.x; // From Top Left if (vOrigin.y <= vMapCheck.y) { if (vOrigin.x <= vMapCheck.x) { hit.eSide = olc::rcw::Engine::CellSide::West; vIntersection.y = m * (vMapCheck.x - vOrigin.x) + vOrigin.y; vIntersection.x = float(vMapCheck.x); hit.fSampleX = vIntersection.y - std::floor(vIntersection.y); } else if (vOrigin.x >= (vMapCheck.x + 1)) { hit.eSide = olc::rcw::Engine::CellSide::East; vIntersection.y = m * ((vMapCheck.x + 1) - vOrigin.x) + vOrigin.y; vIntersection.x = float(vMapCheck.x + 1); hit.fSampleX = vIntersection.y - std::floor(vIntersection.y); } else { hit.eSide = olc::rcw::Engine::CellSide::North; vIntersection.y = float(vMapCheck.y); vIntersection.x = (vMapCheck.y - vOrigin.y) / m + vOrigin.x; hit.fSampleX = vIntersection.x - std::floor(vIntersection.x); } if (vIntersection.y < vMapCheck.y) { hit.eSide = olc::rcw::Engine::CellSide::North; vIntersection.y = float(vMapCheck.y); vIntersection.x = (vMapCheck.y - vOrigin.y) / m + vOrigin.x; hit.fSampleX = vIntersection.x - std::floor(vIntersection.x); } } else if (vOrigin.y >= vMapCheck.y + 1) { if (vOrigin.x <= vMapCheck.x) { hit.eSide = olc::rcw::Engine::CellSide::West; vIntersection.y = m * (vMapCheck.x - vOrigin.x) + vOrigin.y; vIntersection.x = float(vMapCheck.x); hit.fSampleX = vIntersection.y - std::floor(vIntersection.y); } else if (vOrigin.x >= (vMapCheck.x + 1)) { hit.eSide = olc::rcw::Engine::CellSide::East; vIntersection.y = m * ((vMapCheck.x + 1) - vOrigin.x) + vOrigin.y; vIntersection.x = float(vMapCheck.x + 1); hit.fSampleX = vIntersection.y - std::floor(vIntersection.y); } else { hit.eSide = olc::rcw::Engine::CellSide::South; vIntersection.y = float(vMapCheck.y + 1); vIntersection.x = ((vMapCheck.y + 1) - vOrigin.y) / m + vOrigin.x; hit.fSampleX = vIntersection.x - std::floor(vIntersection.x); } if (vIntersection.y > (vMapCheck.y + 1)) { hit.eSide = olc::rcw::Engine::CellSide::South; vIntersection.y = float(vMapCheck.y + 1); vIntersection.x = ((vMapCheck.y + 1) - vOrigin.y) / m + vOrigin.x; hit.fSampleX = vIntersection.x - std::floor(vIntersection.x); } } else { if (vOrigin.x <= vMapCheck.x) { hit.eSide = olc::rcw::Engine::CellSide::West; vIntersection.y = m * (vMapCheck.x - vOrigin.x) + vOrigin.y; vIntersection.x = float(vMapCheck.x); hit.fSampleX = vIntersection.y - std::floor(vIntersection.y); } else if (vOrigin.x >= (vMapCheck.x + 1)) { hit.eSide = olc::rcw::Engine::CellSide::East; vIntersection.y = m * ((vMapCheck.x + 1) - vOrigin.x) + vOrigin.y; vIntersection.x = float(vMapCheck.x + 1); hit.fSampleX = vIntersection.y - std::floor(vIntersection.y); } } hit.vHitPos = vIntersection; } } return bTileFound; } #endif // OLC_PGEX_RAYCASTWORLD #endif // OLC_PGEX_RAYCASTWORLD_H