From be78434a9c6d5db29f2b81ec324d1ab9e5fb77f5 Mon Sep 17 00:00:00 2001 From: Teo-CD Date: Sun, 21 Jan 2024 20:31:51 +0000 Subject: [PATCH 1/4] Player: Fix rotate Rotation wasn't handled properly at all. Properly bound between 0 and 360, and use a proper modulo for floats. Switch to float to have a more fluid rotation between frames. --- Player.cpp | 8 ++++---- Player.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Player.cpp b/Player.cpp index b23f8c5..4f2024d 100644 --- a/Player.cpp +++ b/Player.cpp @@ -14,14 +14,14 @@ void Player::move(float dx, float dy) y += dy; } -void Player::rotate(int alpha) +void Player::rotate(float alpha) { - orientation += alpha%360; - if(orientation >= 360) + orientation += fmodf(alpha, 360); + if(orientation > 360) { orientation -= 360; } - if(orientation <= 360) + else if(orientation < 0) { orientation += 360; } diff --git a/Player.h b/Player.h index 05426c5..fd9cf84 100644 --- a/Player.h +++ b/Player.h @@ -17,7 +17,7 @@ public: float fov = 70; void move(float dx, float dy); - void rotate(int alpha); + void rotate(float alpha); }; From c6b09d668ce58d2159082bc3b779c31eb86d11ed Mon Sep 17 00:00:00 2001 From: Teo-CD Date: Sun, 21 Jan 2024 20:41:19 +0000 Subject: [PATCH 2/4] World: Use nicer custom colors --- World.h | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/World.h b/World.h index eeff8f5..c008cc0 100644 --- a/World.h +++ b/World.h @@ -23,8 +23,10 @@ class World { public: Player player; - World(int w, int h, sf::Color groundColor = sf::Color::Green, sf::Color ceilingColor = sf::Color::Blue, - std::vector worldMap = {}); + World(int w, int h, + sf::Color groundColor = sf::Color{67, 137, 39}, + sf::Color ceilingColor = sf::Color{39, 69, 137}, + std::vector worldMap = {}); int getW() const; int getH() const; @@ -42,7 +44,8 @@ private: sf::Color groundColor; sf::Color ceilingColor; - void fillColumn(sf::RenderWindow&, int column, float scale, sf::Color wallColor = sf::Color(127,127,127)) const; + void fillColumn(sf::RenderWindow&, int column, float scale, + sf::Color wallColor = sf::Color(84,56,34)) const; /** * Cast a ray from a given position and return the on-screen scale. * @param originX Ray X origin From cad775f88e006458a18e856c12f772c388644037 Mon Sep 17 00:00:00 2001 From: Teo-CD Date: Sun, 21 Jan 2024 22:03:04 +0000 Subject: [PATCH 3/4] World: Implement raycaster Implement the basic raycaster, checking every block intersection and the block linked to that position. Split the screen uniformally along the width. Add data needed for camera maths in the Player class Specify constraints on the raycast parameters. Render the screen using the raycasts. Call the renderer in the main loop. --- Player.h | 6 +++ World.cpp | 125 ++++++++++++++++++++++++++++++++++++++++++++++++------ World.h | 8 ++-- main.cpp | 15 +++---- 4 files changed, 126 insertions(+), 28 deletions(-) diff --git a/Player.h b/Player.h index fd9cf84..d39e571 100644 --- a/Player.h +++ b/Player.h @@ -5,6 +5,9 @@ #ifndef RAYCASTING_PLAYER_H #define RAYCASTING_PLAYER_H +#include + +static constexpr float deg_to_rad = 3.14159265/180; class Player { public: @@ -14,7 +17,10 @@ public: float y; float orientation; + /* View properties. */ float fov = 70; + float sensorSize = 0.035; /* 35mm, about equivalent to human eye ? */ + float focalLength = sensorSize / (2*tanf((fov*deg_to_rad)/2)); void move(float dx, float dy); void rotate(float alpha); diff --git a/World.cpp b/World.cpp index 9c8ccb0..84be37c 100644 --- a/World.cpp +++ b/World.cpp @@ -3,6 +3,7 @@ // #include "World.h" +#include World::World(int w, int h, sf::Color groundColor, sf::Color ceilingColor, std::vector worldMap) : w(w), h(h), @@ -92,32 +93,119 @@ void World::fillColumn(sf::RenderWindow& window, int column, float scale, sf::Co window.draw(pixelColumn); } -float World::castRay(float originX, float originY, float orientation) +float World::castRay(float originX, float originY, float orientation) const { /* * 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 + * 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° */ - float deltaX; - float deltaY; - if(orientation < 45 || orientation > 315) - { + /* 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; } - else if(orientation < 135) - { + 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; } - else if(orientation < 225) - { + vTan *= vDir; + vOffsetY *= vTan; - } - else - { + /* + * 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. */ + while (hCheckX >= 0 && hCheckX <= w && hCheckY >= 0 && hCheckY <= h && i < h) { + if (getBlock(floorf(hCheckX), floorf(hCheckY) + hRound) == BlockType::WALL) { + break; + } + hCheckX += hTan; + hCheckY += hDir; + i++; } - return 0; + i = 0; + float vCheckX = vOffsetX; + float vCheckY = originY + vOffsetY; + + /* Bounds + sanity check. */ + while (vCheckX >= 0 && vCheckX < w && vCheckY >= 0 && vCheckY < h && i < w) { + if (getBlock(floorf(vCheckX) + vRound, floorf(vCheckY)) == BlockType::WALL) { + break; + } + + vCheckX += vDir; + vCheckY += vTan; + i++; + } + + /* + * 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; } void World::render(sf::RenderWindow& window) const @@ -143,7 +231,16 @@ void World::render(sf::RenderWindow& window) const */ for(int i = 0;i 360) { + rayAngle -= 360; + } + float obstacleScale = castRay(player.x, player.y, rayAngle)/player.sensorSize; + fillColumn(window, i, obstacleScale); } +} } diff --git a/World.h b/World.h index c008cc0..91236c0 100644 --- a/World.h +++ b/World.h @@ -48,12 +48,12 @@ private: sf::Color wallColor = sf::Color(84,56,34)) const; /** * Cast a ray from a given position and return the on-screen scale. - * @param originX Ray X origin - * @param originY Ray Y origin - * @param orientation Angle to cast to, in degrees + * @param originX Ray X origin, strictly positive + * @param originY Ray Y origin, strictly positive + * @param orientation Angle to cast to, in degrees between 0 and 360 * @return Scale on the screen of the hit wall. */ - float castRay(float originX, float originY, float orientation); + float castRay(float originX, float originY, float orientation) const; }; diff --git a/main.cpp b/main.cpp index 7b883d3..19879b2 100644 --- a/main.cpp +++ b/main.cpp @@ -1,22 +1,13 @@ #include -#include "SFML/Graphics.hpp" +#include #include "World.h" // TODO: Handle inputs to move player -// TODO: Raycast from player position -// TODO: Fix raycasts and use it correctly -// TODO: Render world in event loop -// TODO: Understand what the code was doing (looks like scale from fill column is the output of the raycast) -// Ref: https://lodev.org/cgtutor/raycasting.html -// Ref: http://yunes.informatique.univ-paris-diderot.fr/wp-content/uploads/cours/INFOGRAPHIE/08-Raycasting.pdf -// TODO: Update player for camera plane -// TODO: Camera plane represents screen, rays go through it proportional to the screen // TODO: Find a way to go to edges instead of just equally split (?) int main() { - std::cout << "Hello, World!" << std::endl; World world(10,10); world.setBlock(BlockType::AIR,1,1,8,8); world.setBlock(BlockType::WALL,4,4,2,2); @@ -35,6 +26,10 @@ int main() if (event.type == sf::Event::Closed) window.close(); } + window.clear(); + + world.render(window); + window.display(); } return 0; From b47052aa4d3a48fdb5c17abe423c2fe9da60db69 Mon Sep 17 00:00:00 2001 From: Teo-CD Date: Sun, 21 Jan 2024 22:11:46 +0000 Subject: [PATCH 4/4] Implement moving the player Player: Add rotation and linear speeds, as well as their current state. Player: Allow to move in the local coordinates and compute world-speed equivalent. Player: Update movement axis while rotating. World: Add a function to advance a step. Main: Handle key events, add world step in the main loop. --- Player.cpp | 17 +++++++++++++++++ Player.h | 9 +++++++++ World.cpp | 9 +++++++++ World.h | 8 +++++++- main.cpp | 40 ++++++++++++++++++++++++++++++++++++++-- 5 files changed, 80 insertions(+), 3 deletions(-) diff --git a/Player.cpp b/Player.cpp index 4f2024d..b6fa26c 100644 --- a/Player.cpp +++ b/Player.cpp @@ -25,4 +25,21 @@ void Player::rotate(float alpha) { orientation += 360; } + + /* + * Rotate the movement vector along the new angle, assumes that the only + * speed influencing the player is its own movement. + */ + float prevSpeedX = currentMoveSpeedX; + float prevSpeedY = currentMoveSpeedY; + currentMoveSpeedX = cosf(-alpha * deg_to_rad) * prevSpeedX + - sinf(-alpha * deg_to_rad) * prevSpeedY; + currentMoveSpeedY = sinf(-alpha * deg_to_rad) * prevSpeedX + + cosf(-alpha * deg_to_rad) * prevSpeedY; +} + +void Player::updateSpeed(float localX, float localY) { + // TODO: Support strafing. + currentMoveSpeedX = localX * sinf(orientation*deg_to_rad)*moveSpeed; + currentMoveSpeedY = localX * cosf(orientation*deg_to_rad)*moveSpeed; } diff --git a/Player.h b/Player.h index d39e571..eab2795 100644 --- a/Player.h +++ b/Player.h @@ -17,6 +17,14 @@ public: float y; float orientation; + float moveSpeed = 5; + float rotationSpeed = 180; + + float currentMoveSpeedX = 0; + float currentMoveSpeedY = 0; + + float currentRotationSpeed = 0; + /* View properties. */ float fov = 70; float sensorSize = 0.035; /* 35mm, about equivalent to human eye ? */ @@ -24,6 +32,7 @@ public: void move(float dx, float dy); void rotate(float alpha); + void updateSpeed(float localX, float localY); }; diff --git a/World.cpp b/World.cpp index 84be37c..f632b86 100644 --- a/World.cpp +++ b/World.cpp @@ -243,4 +243,13 @@ void World::render(sf::RenderWindow& window) const } } +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. */ + if (getBlock((int)player.x, (int)player.y) != BlockType::AIR) { + player.move(-player.currentMoveSpeedX*stepTime, + -player.currentMoveSpeedY*stepTime); + } + player.rotate(player.currentRotationSpeed*stepTime); } diff --git a/World.h b/World.h index 91236c0..c098e68 100644 --- a/World.h +++ b/World.h @@ -30,11 +30,17 @@ public: int getW() const; int getH() const; - BlockType getBlock(float x, float y) const; + inline BlockType getBlock(float x, float y) const; void setBlock(BlockType block, int x, int y, int width = 1, int height = 1); void render(sf::RenderWindow&) const; + /** + * Move the world one step forward. + * @param stepTime delta time since last step, in seconds + */ + void step(const float& stepTime); + friend std::ostream& operator<<(std::ostream& ostream, World const & world); private: int w; diff --git a/main.cpp b/main.cpp index 19879b2..73c4dfc 100644 --- a/main.cpp +++ b/main.cpp @@ -3,7 +3,6 @@ #include "World.h" -// TODO: Handle inputs to move player // TODO: Find a way to go to edges instead of just equally split (?) int main() @@ -18,16 +17,53 @@ int main() world.render(window); window.display(); + sf::Event event{}; + sf::Clock frameTime; while (window.isOpen()) { - sf::Event event; while (window.pollEvent(event)) { if (event.type == sf::Event::Closed) window.close(); + else if (event.type == sf::Event::KeyPressed) { + switch (event.key.code) { + case sf::Keyboard::Key::Escape: + window.close(); + break; + case sf::Keyboard::Key::Left: + world.player.currentRotationSpeed = -world.player.rotationSpeed; + break; + case sf::Keyboard::Key::Right: + world.player.currentRotationSpeed = world.player.rotationSpeed; + break; + case sf::Keyboard::Key::Up: + world.player.updateSpeed(1, 0); + break; + case sf::Keyboard::Key::Down: + world.player.updateSpeed(-1, 0); + break; + default: + break; + } + } + else if (event.type == sf::Event::KeyReleased) { + switch (event.key.code) { + case sf::Keyboard::Key::Left: + case sf::Keyboard::Key::Right: + world.player.currentRotationSpeed = 0; + break; + case sf::Keyboard::Key::Up: + case sf::Keyboard::Key::Down: + world.player.updateSpeed(0, 0); + break; + default: + break; + } + } } window.clear(); + world.step(frameTime.restart().asSeconds()); world.render(window); window.display(); }