|
|
|
|
/*
|
|
|
|
|
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, <EFBFBD>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 <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;
|
|
|
|
|
|
|
|
|
|
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<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 };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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<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
|