// // Created by trotfunky on 27/05/19. // #include "World.h" #include #ifdef IMGUI #include #include #endif World::World(int w, int h, sf::Color groundColor, sf::Color ceilingColor, std::vector worldMap) : player(0,0,0), w(w), h(h), map(std::move(worldMap)), groundColor(groundColor), ceilingColor(ceilingColor) { map.resize(w*h,BlockType::WALL); } int World::getW() const { return w; } int World::getH() const { return h; } BlockType World::getBlock(int x, int y) const { return map[x + w*y]; } BlockType World::getBlock(float x, float y) const { return map[static_cast(x) + w*static_cast(y)]; } void World::setBlock(BlockType block, int x, int y, int width, int height) { for(int i = 0;i(world.player.x) == i%world.w && static_cast(world.player.y) == i/world.h) { ostream << "P"; } else { ostream << " "; } break; } case BlockType::WALL: { ostream << "W"; break; } case BlockType::DOOR: { ostream << "D"; break; } case BlockType::WINDOW: { ostream << "W"; break; } } } return(ostream); } 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° * - Those formulas consider regular axes (x→,y↑), however the world is * built around left-handed axes (x→,y↓), so the rendered world is * mirrored. This also explains some weird signs for rotations. */ /* 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; } 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; } 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. */ while (hCheckX >= 0 && hCheckX <= static_cast(w) && hCheckY >= 0 && hCheckY <= static_cast(h) && i < h) { if (getBlock(floorf(hCheckX), floorf(hCheckY) + hRound) == BlockType::WALL) { break; } hCheckX += hTan; hCheckY += hDir; i++; } i = 0; float vCheckX = vOffsetX; float vCheckY = originY + vOffsetY; /* Bounds + sanity check. */ while (vCheckX >= 0 && vCheckX < static_cast(w) && vCheckY >= 0 && vCheckY < static_cast(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)); return hDist > vDist ? vDist : hDist; } void World::fillColumn(sf::RenderWindow& window, unsigned int column, float scale, sf::Color fillColor) const { float columnHeight = static_cast(window.getSize().y)*scale; sf::RectangleShape pixelColumn(sf::Vector2f(1,columnHeight)); pixelColumn.setPosition(static_cast(column), (static_cast(window.getSize().y)-columnHeight)/2.0f); pixelColumn.setFillColor(fillColor); window.draw(pixelColumn); } void World::render(sf::RenderWindow& window) const { float windowX = static_cast(window.getSize().x); float windowY = static_cast(window.getSize().y); /* * 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. */ sf::RectangleShape ground = sf::RectangleShape(sf::Vector2f(windowX,windowY/2.0f)); ground.setFillColor(groundColor); ground.setPosition(0,windowY/2.0f); sf::RectangleShape ceiling = sf::RectangleShape(sf::Vector2f(windowX,windowY/2.0f)); ceiling.setFillColor(ceilingColor); window.draw(ground); window.draw(ceiling); const float worldToCamera = (player.focalLength*2)/player.sensorSize; /* * Throw rays and draw walls over the ceiling and ground. * Only throws in the plane, which doesn't work for levels/3D. */ for(unsigned int i = 0 ; i < window.getSize().x ; i++) { float deltaAngle = (player.fov/windowX) * (static_cast(i)-windowX/2.0f); float rayAngle = player.orientation + deltaAngle; if (rayAngle < 0) { rayAngle += 360; } else if (rayAngle > 360) { rayAngle -= 360; } float obstacleScale = worldToCamera / castRay(player.x, player.y, rayAngle); /* 2 Is wall height in meters. */ fillColumn(window, i, obstacleScale); } } 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(player.x, player.y) != BlockType::AIR) { player.move(-player.currentMoveSpeedX*stepTime, -player.currentMoveSpeedY*stepTime); } player.rotate(player.currentRotationSpeed*stepTime); #ifdef IMGUI ImGui::Begin("MapEdit"); static int blockToPlace = 1; /* * Group all the select boxes together : makes it a big block in the * Dear ImGui layout an allows adding things on the side. */ ImGui::BeginGroup(); for (int y = 0 ; y < h ; y++) { for (int x = 0 ; x < w ; x++) { ImGui::PushID(x + y*w); if (x > 0) ImGui::SameLine(); BlockType currentBlock = map[x + w*y]; ImVec4 hoverColor; switch ((BlockType)blockToPlace) { case BlockType::WALL: hoverColor = (ImVec4)Colors::Wall; break; case BlockType::DOOR: hoverColor = (ImVec4)Colors::Door; break; case BlockType::WINDOW: hoverColor = (ImVec4)Colors::Window; break; default: /* Default header color, it seems ? */ hoverColor = (ImVec4)ImColor(188, 120, 32); break; } ImGui::PushStyleColor(ImGuiCol_HeaderHovered, hoverColor); ImVec4 blockColor; switch (currentBlock) { case BlockType::WALL: blockColor = (ImVec4)Colors::Wall; break; case BlockType::DOOR: blockColor = (ImVec4)Colors::Door; break; case BlockType::WINDOW: blockColor = (ImVec4)Colors::Window; break; default: blockColor = (ImVec4)ImColor(188, 120, 32); break; } ImGui::PushStyleColor(ImGuiCol_Header, blockColor); if(ImGui::Selectable("", currentBlock != BlockType::AIR, 0, ImVec2(10, 10))) { map[x + w*y] = currentBlock == (BlockType)blockToPlace ? BlockType::AIR : (BlockType)blockToPlace; } ImGui::PopStyleColor(2); ImGui::PopID(); } } ImGui::EndGroup(); ImGui::SameLine(); ImGui::BeginGroup(); ImGui::Indent(); ImGui::Text("Place :"); ImGui::RadioButton("Wall", &blockToPlace, (int)BlockType::WALL); ImGui::RadioButton("Door", &blockToPlace, (int)BlockType::DOOR); ImGui::RadioButton("Window", &blockToPlace, (int)BlockType::WINDOW); ImGui::Unindent(); ImGui::EndGroup(); ImGui::End(); #endif }