2019-05-27 13:50:49 +02:00
|
|
|
//
|
|
|
|
// Created by trotfunky on 27/05/19.
|
|
|
|
//
|
|
|
|
|
|
|
|
#include "World.h"
|
2024-01-21 22:03:04 +00:00
|
|
|
#include <cmath>
|
2019-05-27 13:50:49 +02:00
|
|
|
|
|
|
|
|
2024-01-24 23:16:44 +00:00
|
|
|
World::World(int w, int h, sf::Color groundColor, sf::Color ceilingColor, std::vector<BlockType> worldMap) :
|
|
|
|
player(0,0,0), w(w), h(h), map(std::move(worldMap)),
|
|
|
|
groundColor(groundColor), ceilingColor(ceilingColor)
|
2019-05-27 13:50:49 +02:00
|
|
|
{
|
|
|
|
map.resize(w*h,BlockType::WALL);
|
|
|
|
}
|
|
|
|
|
|
|
|
int World::getW() const
|
|
|
|
{
|
|
|
|
return w;
|
|
|
|
}
|
|
|
|
|
|
|
|
int World::getH() const
|
|
|
|
{
|
|
|
|
return h;
|
|
|
|
}
|
|
|
|
|
2024-01-24 23:16:44 +00:00
|
|
|
BlockType World::getBlock(int x, int y) const
|
|
|
|
{
|
|
|
|
return map[x + w*y];
|
|
|
|
}
|
|
|
|
|
2019-05-27 13:50:49 +02:00
|
|
|
BlockType World::getBlock(float x, float y) const
|
|
|
|
{
|
2024-01-24 23:16:44 +00:00
|
|
|
return map[static_cast<int>(x) + w*static_cast<int>(y)];
|
2019-05-27 13:50:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void World::setBlock(BlockType block, int x, int y, int width, int height)
|
|
|
|
{
|
|
|
|
for(int i = 0;i<height;i++)
|
|
|
|
{
|
|
|
|
for(int j = 0;j<width;j++)
|
|
|
|
{
|
|
|
|
if(x+j<w && y+i < h)
|
|
|
|
{
|
|
|
|
map.at((y+i)*w+x+j) = block;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
std::ostream& operator<<(std::ostream& ostream, World const& world)
|
|
|
|
{
|
|
|
|
for(int i = 0;i<world.w*world.h;i++)
|
|
|
|
{
|
|
|
|
if(i%world.w == 0)
|
|
|
|
{
|
|
|
|
ostream << std::endl;
|
|
|
|
}
|
|
|
|
switch(world.getBlock(i%world.w,i/world.w))
|
|
|
|
{
|
|
|
|
case BlockType::AIR:
|
|
|
|
{
|
2019-06-01 06:42:29 +02:00
|
|
|
if(static_cast<int>(world.player.x) == i%world.w && static_cast<int>(world.player.y) == i/world.h)
|
|
|
|
{
|
|
|
|
ostream << "P";
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
ostream << " ";
|
|
|
|
}
|
2019-05-27 13:50:49 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case BlockType::WALL:
|
|
|
|
{
|
|
|
|
ostream << "W";
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case BlockType::DOOR:
|
|
|
|
{
|
|
|
|
ostream << "D";
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case BlockType::WINDOW:
|
|
|
|
{
|
|
|
|
ostream << "W";
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return(ostream);
|
|
|
|
}
|
2019-06-01 06:42:29 +02:00
|
|
|
|
2024-01-21 22:03:04 +00:00
|
|
|
float World::castRay(float originX, float originY, float orientation) const
|
2019-06-01 06:42:29 +02:00
|
|
|
{
|
2023-12-30 14:50:35 +01:00
|
|
|
/*
|
|
|
|
* Reference used for ray intersection computations :
|
|
|
|
* https://web.archive.org/web/20220628034315/https://yunes.informatique.univ-paris-diderot.fr/wp-content/uploads/cours/INFOGRAPHIE/08-Raycasting.pdf
|
2024-01-21 22:03:04 +00:00
|
|
|
* The logic is as follows :
|
|
|
|
* - This computes one set of point per edge crossings (horizontal/vertical)
|
|
|
|
* - The origin not being confined to the grid, offsets are computed to
|
|
|
|
* align the intersections properly
|
|
|
|
* - The intersections are at multiples of the tangent of the relevant
|
|
|
|
* angle for the axis of interest, and simply on successive edges of
|
|
|
|
* the grid for the other one
|
|
|
|
* - Depending on the orientation, signs must be taken into account
|
|
|
|
* to work 360°
|
2023-12-30 14:50:35 +01:00
|
|
|
*/
|
2024-01-21 22:03:04 +00:00
|
|
|
/* Offsets to get back on the grid from the ray's origin. */
|
|
|
|
float hOffsetX;
|
|
|
|
float hOffsetY;
|
|
|
|
float vOffsetX;
|
|
|
|
float vOffsetY;
|
|
|
|
|
|
|
|
/* Signs controlling the direction of travel. */
|
|
|
|
float hDir;
|
|
|
|
float vDir;
|
|
|
|
/* Need offset for rounding in the right direction ? */
|
|
|
|
float hRound;
|
|
|
|
float vRound;
|
|
|
|
|
|
|
|
float rads = orientation * deg_to_rad;
|
|
|
|
/* Used for vertical intersections. */
|
|
|
|
float rads_offset = (90 - orientation) * deg_to_rad;
|
|
|
|
|
|
|
|
/* Tangents used for the different axes. */
|
|
|
|
float hTan = tanf(rads);
|
|
|
|
float vTan = tanf(rads_offset);
|
|
|
|
|
|
|
|
/* Check if cos > 0 for horizontal hits formulas. */
|
|
|
|
if (orientation < 90 || orientation > 270) {
|
|
|
|
hOffsetX = ceilf(originY) - originY;
|
|
|
|
hOffsetY = ceilf(originY);
|
|
|
|
hDir = +1;
|
|
|
|
hRound = 0;
|
|
|
|
} else {
|
|
|
|
hOffsetX = originY - floorf(originY);
|
|
|
|
hOffsetY = floorf(originY);
|
|
|
|
hDir = -1;
|
|
|
|
hRound = -1;
|
2019-06-01 06:42:29 +02:00
|
|
|
}
|
2024-01-21 22:03:04 +00:00
|
|
|
hTan *= hDir;
|
|
|
|
hOffsetX *= hTan;
|
|
|
|
|
|
|
|
/* Check if sin > 0 for vertical hits formulas. */
|
|
|
|
if (orientation < 180) {
|
|
|
|
vOffsetX = ceilf(originX);
|
|
|
|
vOffsetY = ceilf(originX) - originX;
|
|
|
|
vDir = +1;
|
|
|
|
vRound = 0;
|
|
|
|
} else {
|
|
|
|
vOffsetX = floorf(originX);
|
|
|
|
vOffsetY = originX - floorf(originX);
|
|
|
|
vDir = -1;
|
|
|
|
vRound = -1;
|
2019-06-01 06:42:29 +02:00
|
|
|
}
|
2024-01-21 22:03:04 +00:00
|
|
|
vTan *= vDir;
|
|
|
|
vOffsetY *= vTan;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Now we have all the constants and deltas to work with, cast the ray.
|
|
|
|
* Generated points follow the formulas :
|
|
|
|
* - h-intersect : (originX + hOffsetX + hTan*i, hOffsetY + hDir*i)
|
|
|
|
* - v-intersect : (vOffsetX + vDir*i, originY + vOffsetY + vTan*i)
|
|
|
|
*/
|
|
|
|
int i = 0;
|
|
|
|
float hCheckX = originX + hOffsetX;
|
|
|
|
float hCheckY = hOffsetY;
|
|
|
|
/* Bounds + sanity check. */
|
2024-01-24 23:16:44 +00:00
|
|
|
while (hCheckX >= 0 && hCheckX <= static_cast<float>(w) &&
|
|
|
|
hCheckY >= 0 && hCheckY <= static_cast<float>(h) && i < h) {
|
2024-01-21 22:03:04 +00:00
|
|
|
if (getBlock(floorf(hCheckX), floorf(hCheckY) + hRound) == BlockType::WALL) {
|
|
|
|
break;
|
|
|
|
}
|
2019-06-01 06:42:29 +02:00
|
|
|
|
2024-01-21 22:03:04 +00:00
|
|
|
hCheckX += hTan;
|
|
|
|
hCheckY += hDir;
|
|
|
|
i++;
|
2019-06-01 06:42:29 +02:00
|
|
|
}
|
|
|
|
|
2024-01-21 22:03:04 +00:00
|
|
|
i = 0;
|
|
|
|
float vCheckX = vOffsetX;
|
|
|
|
float vCheckY = originY + vOffsetY;
|
|
|
|
|
|
|
|
/* Bounds + sanity check. */
|
2024-01-24 23:16:44 +00:00
|
|
|
while (vCheckX >= 0 && vCheckX < static_cast<float>(w) &&
|
|
|
|
vCheckY >= 0 && vCheckY < static_cast<float>(h) && i < w) {
|
2024-01-21 22:03:04 +00:00
|
|
|
if (getBlock(floorf(vCheckX) + vRound, floorf(vCheckY)) == BlockType::WALL) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
vCheckX += vDir;
|
|
|
|
vCheckY += vTan;
|
|
|
|
i++;
|
2019-06-01 06:42:29 +02:00
|
|
|
}
|
|
|
|
|
2024-01-21 22:03:04 +00:00
|
|
|
/*
|
|
|
|
* We may or may not have hit something. Check which coordinates are closest
|
|
|
|
* and use those for computing the apparent size on screen.
|
|
|
|
*/
|
|
|
|
float hDist = sqrtf((originX - hCheckX)*(originX - hCheckX) +
|
|
|
|
(originY - hCheckY)*(originY - hCheckY));
|
|
|
|
float vDist = sqrtf((originX - vCheckX)*(originX - vCheckX) +
|
|
|
|
(originY - vCheckY)*(originY - vCheckY));
|
|
|
|
float finalDist = hDist > vDist ? vDist : hDist;
|
|
|
|
|
|
|
|
/* 2 Is wall height in meters. */
|
|
|
|
return player.focalLength*2/finalDist;
|
2019-06-01 06:42:29 +02:00
|
|
|
}
|
|
|
|
|
2024-01-24 23:16:44 +00:00
|
|
|
void World::fillColumn(sf::RenderWindow& window, unsigned int column,
|
|
|
|
float scale, sf::Color wallColor) const
|
|
|
|
{
|
|
|
|
float columnHeight = static_cast<float>(window.getSize().y)*scale;
|
|
|
|
sf::RectangleShape pixelColumn(sf::Vector2f(1,columnHeight));
|
|
|
|
pixelColumn.setPosition(static_cast<float>(column),
|
|
|
|
(static_cast<float>(window.getSize().y)-columnHeight)/2.0f);
|
|
|
|
pixelColumn.setFillColor(wallColor);
|
|
|
|
|
|
|
|
window.draw(pixelColumn);
|
|
|
|
}
|
|
|
|
|
2019-06-01 06:42:29 +02:00
|
|
|
void World::render(sf::RenderWindow& window) const
|
|
|
|
{
|
2024-01-24 23:16:44 +00:00
|
|
|
float windowX = static_cast<float>(window.getSize().x);
|
|
|
|
float windowY = static_cast<float>(window.getSize().y);
|
2023-12-30 14:50:35 +01:00
|
|
|
/*
|
|
|
|
* Draw ground and sky planes through half of the screen, as the walls
|
|
|
|
* will get drawn over them.
|
|
|
|
* This doesn't work if we support textures/levels.
|
|
|
|
*/
|
2024-01-24 23:16:44 +00:00
|
|
|
sf::RectangleShape ground = sf::RectangleShape(sf::Vector2f(windowX,windowY/2.0f));
|
2019-06-01 06:42:29 +02:00
|
|
|
ground.setFillColor(groundColor);
|
2024-01-24 23:16:44 +00:00
|
|
|
ground.setPosition(0,windowY/2.0f);
|
2019-06-01 06:42:29 +02:00
|
|
|
|
2024-01-24 23:16:44 +00:00
|
|
|
sf::RectangleShape ceiling = sf::RectangleShape(sf::Vector2f(windowX,windowY/2.0f));
|
2019-06-01 06:42:29 +02:00
|
|
|
ceiling.setFillColor(ceilingColor);
|
|
|
|
|
|
|
|
window.draw(ground);
|
|
|
|
window.draw(ceiling);
|
|
|
|
|
2023-12-30 14:50:35 +01:00
|
|
|
/*
|
|
|
|
* Throw rays and draw walls over the ceiling and ground.
|
|
|
|
* Only throws in the plane, which doesn't work for levels/3D.
|
|
|
|
*/
|
2024-01-24 23:16:44 +00:00
|
|
|
for(unsigned int i = 0 ; i < window.getSize().x ; i++)
|
2019-06-01 06:42:29 +02:00
|
|
|
{
|
2024-01-24 23:16:44 +00:00
|
|
|
float deltaAngle = (player.fov/windowX) * (static_cast<float>(i)-windowX/2.0f);
|
2024-01-21 22:03:04 +00:00
|
|
|
float rayAngle = player.orientation + deltaAngle;
|
|
|
|
if (rayAngle < 0) {
|
|
|
|
rayAngle += 360;
|
|
|
|
} else if (rayAngle > 360) {
|
|
|
|
rayAngle -= 360;
|
|
|
|
}
|
|
|
|
float obstacleScale = castRay(player.x, player.y, rayAngle)/player.sensorSize;
|
|
|
|
fillColumn(window, i, obstacleScale);
|
2019-06-01 06:42:29 +02:00
|
|
|
}
|
2024-01-21 22:03:04 +00:00
|
|
|
}
|
2019-06-01 06:42:29 +02:00
|
|
|
|
2024-01-21 22:11:46 +00:00
|
|
|
void World::step(const float& stepTime) {
|
|
|
|
player.move(player.currentMoveSpeedX*stepTime,
|
|
|
|
player.currentMoveSpeedY*stepTime);
|
|
|
|
/* Undo last move if the player would end up in a wall. */
|
2024-01-24 23:16:44 +00:00
|
|
|
if (getBlock(player.x, player.y) != BlockType::AIR) {
|
2024-01-21 22:11:46 +00:00
|
|
|
player.move(-player.currentMoveSpeedX*stepTime,
|
|
|
|
-player.currentMoveSpeedY*stepTime);
|
|
|
|
}
|
|
|
|
player.rotate(player.currentRotationSpeed*stepTime);
|
2019-06-01 06:42:29 +02:00
|
|
|
}
|