parent
965361dc28
commit
199941385f
@ -0,0 +1,750 @@ |
|||||||
|
/*
|
||||||
|
olcPGEX_RayCastWorld.h |
||||||
|
|
||||||
|
+-------------------------------------------------------------+ |
||||||
|
| OneLoneCoder Pixel Game Engine Extension | |
||||||
|
| Ray Cast World v1.0 | |
||||||
|
+-------------------------------------------------------------+ |
||||||
|
|
||||||
|
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 |
||||||
|
*/ |
||||||
|
|
||||||
|
#ifndef OLC_PGEX_RAYCASTWORLD_H |
||||||
|
#define OLC_PGEX_RAYCASTWORLD_H |
||||||
|
|
||||||
|
#include <unordered_map> |
||||||
|
#include <algorithm> |
||||||
|
|
||||||
|
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<olc::rcw::Object> 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<olc::rcw::Object> object1, std::shared_ptr<olc::rcw::Object> 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<uint32_t, std::shared_ptr<olc::rcw::Object>> 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<float[]> 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<olc::rcw::Object> object = ob.second; |
||||||
|
if (!object->bIsActive) continue; |
||||||
|
|
||||||
|
// Determine where object is trying to be
|
||||||
|
olc::vf2d vPotentialPosition = object->pos + object->vel * fElapsedTime; |
||||||
|
|
||||||
|
// 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<olc::rcw::Object> 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 }; |
||||||
|
|
||||||
|
bool bRecheck = false; |
||||||
|
do |
||||||
|
{ |
||||||
|
bRecheck = false; |
||||||
|
|
||||||
|
// 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 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)); |
||||||
|
} |
||||||
|
|
||||||
|
bRecheck = true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
while (bRecheck); |
||||||
|
} |
||||||
|
|
||||||
|
// 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<olc::rcw::Object> 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<olc::rcw::Object> 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<olc::rcw::Object> object1, std::shared_ptr<olc::rcw::Object> 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
|
Loading…
Reference in new issue