431 lines
14 KiB
C++
431 lines
14 KiB
C++
/*
|
||
Dungeon Warping via Orthographic Projections
|
||
"For my Mother-In-Law, you will be missed..." - javidx9
|
||
|
||
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.
|
||
|
||
Relevant Video: https://youtu.be/Ql5VZGkL23o
|
||
|
||
Links
|
||
~~~~~
|
||
YouTube: https://www.youtube.com/javidx9
|
||
https://www.youtube.com/javidx9extra
|
||
Discord: https://discord.gg/WhwHUMV
|
||
Twitter: https://www.twitter.com/javidx9
|
||
Twitch: https://www.twitch.tv/javidx9
|
||
GitHub: https://www.github.com/onelonecoder
|
||
Patreon: https://www.patreon.com/javidx9
|
||
Homepage: https://www.onelonecoder.com
|
||
|
||
Community Blog: https://community.onelonecoder.com
|
||
|
||
Author
|
||
~~~~~~
|
||
David Barr, aka javidx9, <20>OneLoneCoder 2018, 2019, 2020
|
||
*/
|
||
|
||
#define OLC_PGE_APPLICATION
|
||
#include "olcPixelGameEngine.h"
|
||
|
||
/*
|
||
|
||
NOTE! This program requires a tile spritesheet NOT
|
||
provided in this github. You only need a few tiles,
|
||
see video for details.
|
||
|
||
*/
|
||
|
||
class olcDungeon : public olc::PixelGameEngine
|
||
{
|
||
public:
|
||
olcDungeon()
|
||
{
|
||
sAppName = "Dungeon Explorer";
|
||
}
|
||
|
||
struct Renderable
|
||
{
|
||
Renderable() {}
|
||
|
||
void Load(const std::string& sFile)
|
||
{
|
||
sprite = new olc::Sprite(sFile);
|
||
decal = new olc::Decal(sprite);
|
||
}
|
||
|
||
~Renderable()
|
||
{
|
||
delete decal;
|
||
delete sprite;
|
||
}
|
||
|
||
olc::Sprite* sprite = nullptr;
|
||
olc::Decal* decal = nullptr;
|
||
};
|
||
|
||
struct vec3d
|
||
{
|
||
float x, y, z;
|
||
};
|
||
|
||
struct sQuad
|
||
{
|
||
vec3d points[4];
|
||
olc::vf2d tile;
|
||
};
|
||
|
||
struct sCell
|
||
{
|
||
bool wall = false;
|
||
olc::vi2d id[6]{ };
|
||
};
|
||
|
||
class World
|
||
{
|
||
public:
|
||
World()
|
||
{
|
||
|
||
}
|
||
|
||
void Create(int w, int h)
|
||
{
|
||
size = { w, h };
|
||
vCells.resize(w * h);
|
||
}
|
||
|
||
sCell& GetCell(const olc::vi2d& v)
|
||
{
|
||
if (v.x >= 0 && v.x < size.x && v.y >= 0 && v.y < size.y)
|
||
return vCells[v.y * size.x + v.x];
|
||
else
|
||
return NullCell;
|
||
}
|
||
|
||
public:
|
||
olc::vi2d size;
|
||
|
||
private:
|
||
std::vector<sCell> vCells;
|
||
sCell NullCell;
|
||
};
|
||
|
||
World world;
|
||
Renderable rendSelect;
|
||
Renderable rendAllWalls;
|
||
|
||
olc::vf2d vCameraPos = { 0.0f, 0.0f };
|
||
float fCameraAngle = 0.0f;
|
||
float fCameraAngleTarget = fCameraAngle;
|
||
float fCameraPitch = 5.5f;
|
||
float fCameraZoom = 16.0f;
|
||
|
||
bool bVisible[6];
|
||
|
||
olc::vi2d vCursor = { 0, 0 };
|
||
olc::vi2d vTileCursor = { 0,0 };
|
||
olc::vi2d vTileSize = { 32, 32 };
|
||
|
||
enum Face
|
||
{
|
||
Floor = 0,
|
||
North = 1,
|
||
East = 2,
|
||
South = 3,
|
||
West = 4,
|
||
Top = 5
|
||
};
|
||
|
||
public:
|
||
bool OnUserCreate() override
|
||
{
|
||
rendSelect.Load("./gfx/dng_select.png");
|
||
rendAllWalls.Load("./gfx/oldDungeon.png");
|
||
|
||
world.Create(64, 64);
|
||
|
||
for (int y=0; y<world.size.y; y++)
|
||
for(int x=0; x<world.size.x; x++)
|
||
{
|
||
world.GetCell({ x, y }).wall = false;
|
||
world.GetCell({ x, y }).id[Face::Floor] = olc::vi2d{ 3, 0 } * vTileSize;
|
||
world.GetCell({ x, y }).id[Face::Top] = olc::vi2d{ 1, 0 } * vTileSize;
|
||
world.GetCell({ x, y }).id[Face::North] = olc::vi2d{ 0, 6 } * vTileSize;
|
||
world.GetCell({ x, y }).id[Face::South] = olc::vi2d{ 0, 6 } * vTileSize;
|
||
world.GetCell({ x, y }).id[Face::West] = olc::vi2d{ 0, 6 } * vTileSize;
|
||
world.GetCell({ x, y }).id[Face::East] = olc::vi2d{ 0, 6 } * vTileSize;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
std::array<vec3d, 8> CreateCube(const olc::vi2d& vCell, const float fAngle, const float fPitch, const float fScale, const vec3d& vCamera)
|
||
{
|
||
// Unit Cube
|
||
std::array<vec3d, 8> unitCube, rotCube, worldCube, projCube;
|
||
unitCube[0] = { 0.0f, 0.0f, 0.0f };
|
||
unitCube[1] = { fScale, 0.0f, 0.0f };
|
||
unitCube[2] = { fScale, -fScale, 0.0f };
|
||
unitCube[3] = { 0.0f, -fScale, 0.0f };
|
||
unitCube[4] = { 0.0f, 0.0f, fScale };
|
||
unitCube[5] = { fScale, 0.0f, fScale };
|
||
unitCube[6] = { fScale, -fScale, fScale };
|
||
unitCube[7] = { 0.0f, -fScale, fScale };
|
||
|
||
// Translate Cube in X-Z Plane
|
||
for (int i = 0; i < 8; i++)
|
||
{
|
||
unitCube[i].x += (vCell.x * fScale - vCamera.x);
|
||
unitCube[i].y += -vCamera.y;
|
||
unitCube[i].z += (vCell.y * fScale - vCamera.z);
|
||
}
|
||
|
||
// Rotate Cube in Y-Axis around origin
|
||
float s = sin(fAngle);
|
||
float c = cos(fAngle);
|
||
for (int i = 0; i < 8; i++)
|
||
{
|
||
rotCube[i].x = unitCube[i].x * c + unitCube[i].z * s;
|
||
rotCube[i].y = unitCube[i].y;
|
||
rotCube[i].z = unitCube[i].x * -s + unitCube[i].z * c;
|
||
}
|
||
|
||
// Rotate Cube in X-Axis around origin (tilt slighly overhead)
|
||
s = sin(fPitch);
|
||
c = cos(fPitch);
|
||
for (int i = 0; i < 8; i++)
|
||
{
|
||
worldCube[i].x = rotCube[i].x;
|
||
worldCube[i].y = rotCube[i].y * c - rotCube[i].z * s;
|
||
worldCube[i].z = rotCube[i].y * s + rotCube[i].z * c;
|
||
}
|
||
|
||
// Project Cube Orthographically - Unit Cube Viewport
|
||
//float fLeft = -ScreenWidth() * 0.5f;
|
||
//float fRight = ScreenWidth() * 0.5f;
|
||
//float fTop = ScreenHeight() * 0.5f;
|
||
//float fBottom = -ScreenHeight() * 0.5f;
|
||
//float fNear = 0.1f;
|
||
//float fFar = 100.0f;*/
|
||
//for (int i = 0; i < 8; i++)
|
||
//{
|
||
// projCube[i].x = (2.0f / (fRight - fLeft)) * worldCube[i].x - ((fRight + fLeft) / (fRight - fLeft));
|
||
// projCube[i].y = (2.0f / (fTop - fBottom)) * worldCube[i].y - ((fTop + fBottom) / (fTop - fBottom));
|
||
// projCube[i].z = (2.0f / (fFar - fNear)) * worldCube[i].z - ((fFar + fNear) / (fFar - fNear));
|
||
// projCube[i].x *= -fRight;
|
||
// projCube[i].y *= -fTop;
|
||
// projCube[i].x += fRight;
|
||
// projCube[i].y += fTop;
|
||
//}
|
||
|
||
// Project Cube Orthographically - Full Screen Centered
|
||
for (int i = 0; i < 8; i++)
|
||
{
|
||
projCube[i].x = worldCube[i].x + ScreenWidth() * 0.5f;
|
||
projCube[i].y = worldCube[i].y + ScreenHeight() * 0.5f;
|
||
projCube[i].z = worldCube[i].z;
|
||
}
|
||
|
||
return projCube;
|
||
}
|
||
|
||
|
||
|
||
void CalculateVisibleFaces(std::array<vec3d, 8>& cube)
|
||
{
|
||
auto CheckNormal = [&](int v1, int v2, int v3)
|
||
{
|
||
olc::vf2d a = { cube[v1].x, cube[v1].y };
|
||
olc::vf2d b = { cube[v2].x, cube[v2].y };
|
||
olc::vf2d c = { cube[v3].x, cube[v3].y };
|
||
return (b - a).cross(c - a) > 0;
|
||
};
|
||
|
||
bVisible[Face::Floor] = CheckNormal(4, 0, 1);
|
||
bVisible[Face::South] = CheckNormal(3, 0, 1);
|
||
bVisible[Face::North] = CheckNormal(6, 5, 4);
|
||
bVisible[Face::East] = CheckNormal(7, 4, 0);
|
||
bVisible[Face::West] = CheckNormal(2, 1, 5);
|
||
bVisible[Face::Top] = CheckNormal(7, 3, 2);
|
||
}
|
||
|
||
void GetFaceQuads(const olc::vi2d& vCell, const float fAngle, const float fPitch, const float fScale, const vec3d& vCamera, std::vector<sQuad> &render)
|
||
{
|
||
std::array<vec3d, 8> projCube = CreateCube(vCell, fAngle, fPitch, fScale, vCamera);
|
||
|
||
auto& cell = world.GetCell(vCell);
|
||
|
||
auto MakeFace = [&](int v1, int v2, int v3, int v4, Face f)
|
||
{
|
||
render.push_back({ projCube[v1], projCube[v2], projCube[v3], projCube[v4], cell.id[f] });
|
||
};
|
||
|
||
if (!cell.wall)
|
||
{
|
||
if(bVisible[Face::Floor]) MakeFace(4, 0, 1, 5, Face::Floor);
|
||
}
|
||
else
|
||
{
|
||
if (bVisible[Face::South]) MakeFace(3, 0, 1, 2, Face::South);
|
||
if (bVisible[Face::North]) MakeFace(6, 5, 4, 7, Face::North);
|
||
if (bVisible[Face::East]) MakeFace(7, 4, 0, 3, Face::East);
|
||
if (bVisible[Face::West]) MakeFace(2, 1, 5, 6, Face::West);
|
||
if (bVisible[Face::Top]) MakeFace(7, 3, 2, 6, Face::Top);
|
||
}
|
||
}
|
||
|
||
|
||
bool OnUserUpdate(float fElapsedTime) override
|
||
{
|
||
// Grab mouse for convenience
|
||
olc::vi2d vMouse = { GetMouseX(), GetMouseY() };
|
||
|
||
// Edit mode - Selection from tile sprite sheet
|
||
if (GetKey(olc::Key::TAB).bHeld)
|
||
{
|
||
DrawSprite({ 0, 0 }, rendAllWalls.sprite);
|
||
DrawRect(vTileCursor * vTileSize, vTileSize);
|
||
if (GetMouse(0).bPressed) vTileCursor = vMouse / vTileSize;
|
||
return true;
|
||
}
|
||
|
||
// WS keys to tilt camera
|
||
if (GetKey(olc::Key::W).bHeld) fCameraPitch += 1.0f * fElapsedTime;
|
||
if (GetKey(olc::Key::S).bHeld) fCameraPitch -= 1.0f * fElapsedTime;
|
||
|
||
// DA Keys to manually rotate camera
|
||
if (GetKey(olc::Key::D).bHeld) fCameraAngleTarget += 1.0f * fElapsedTime;
|
||
if (GetKey(olc::Key::A).bHeld) fCameraAngleTarget -= 1.0f * fElapsedTime;
|
||
|
||
// QZ Keys to zoom in or out
|
||
if (GetKey(olc::Key::Q).bHeld) fCameraZoom += 5.0f * fElapsedTime;
|
||
if (GetKey(olc::Key::Z).bHeld) fCameraZoom -= 5.0f * fElapsedTime;
|
||
|
||
// Numpad keys used to rotate camera to fixed angles
|
||
if (GetKey(olc::Key::NP2).bPressed) fCameraAngleTarget = 3.14159f * 0.0f;
|
||
if (GetKey(olc::Key::NP1).bPressed) fCameraAngleTarget = 3.14159f * 0.25f;
|
||
if (GetKey(olc::Key::NP4).bPressed) fCameraAngleTarget = 3.14159f * 0.5f;
|
||
if (GetKey(olc::Key::NP7).bPressed) fCameraAngleTarget = 3.14159f * 0.75f;
|
||
if (GetKey(olc::Key::NP8).bPressed) fCameraAngleTarget = 3.14159f * 1.0f;
|
||
if (GetKey(olc::Key::NP9).bPressed) fCameraAngleTarget = 3.14159f * 1.25f;
|
||
if (GetKey(olc::Key::NP6).bPressed) fCameraAngleTarget = 3.14159f * 1.5f;
|
||
if (GetKey(olc::Key::NP3).bPressed) fCameraAngleTarget = 3.14159f * 1.75f;
|
||
|
||
// Numeric keys apply selected tile to specific face
|
||
if (GetKey(olc::Key::K1).bPressed) world.GetCell(vCursor).id[Face::North] = vTileCursor * vTileSize;
|
||
if (GetKey(olc::Key::K2).bPressed) world.GetCell(vCursor).id[Face::East] = vTileCursor * vTileSize;
|
||
if (GetKey(olc::Key::K3).bPressed) world.GetCell(vCursor).id[Face::South] = vTileCursor * vTileSize;
|
||
if (GetKey(olc::Key::K4).bPressed) world.GetCell(vCursor).id[Face::West] = vTileCursor * vTileSize;
|
||
if (GetKey(olc::Key::K5).bPressed) world.GetCell(vCursor).id[Face::Floor] = vTileCursor * vTileSize;
|
||
if (GetKey(olc::Key::K6).bPressed) world.GetCell(vCursor).id[Face::Top] = vTileCursor * vTileSize;
|
||
|
||
// Smooth camera
|
||
fCameraAngle += (fCameraAngleTarget - fCameraAngle) * 10.0f * fElapsedTime;
|
||
|
||
// Arrow keys to move the selection cursor around map (boundary checked)
|
||
if (GetKey(olc::Key::LEFT).bPressed) vCursor.x--;
|
||
if (GetKey(olc::Key::RIGHT).bPressed) vCursor.x++;
|
||
if (GetKey(olc::Key::UP).bPressed) vCursor.y--;
|
||
if (GetKey(olc::Key::DOWN).bPressed) vCursor.y++;
|
||
if (vCursor.x < 0) vCursor.x = 0;
|
||
if (vCursor.y < 0) vCursor.y = 0;
|
||
if (vCursor.x >= world.size.x) vCursor.x = world.size.x - 1;
|
||
if (vCursor.y >= world.size.y) vCursor.y = world.size.y - 1;
|
||
|
||
// Place block with space
|
||
if (GetKey(olc::Key::SPACE).bPressed)
|
||
{
|
||
world.GetCell(vCursor).wall = !world.GetCell(vCursor).wall;
|
||
}
|
||
|
||
// Position camera in world
|
||
vCameraPos = { vCursor.x + 0.5f, vCursor.y + 0.5f };
|
||
vCameraPos *= fCameraZoom;
|
||
|
||
// Rendering
|
||
|
||
// 1) Create dummy cube to extract visible face information
|
||
// Cull faces that cannot be seen
|
||
std::array<vec3d, 8> cullCube = CreateCube({ 0, 0 }, fCameraAngle, fCameraPitch, fCameraZoom, { vCameraPos.x, 0.0f, vCameraPos.y });
|
||
CalculateVisibleFaces(cullCube);
|
||
|
||
// 2) Get all visible sides of all visible "tile cubes"
|
||
std::vector<sQuad> vQuads;
|
||
for(int y = 0; y<world.size.y; y++)
|
||
for(int x=0; x<world.size.x; x++)
|
||
GetFaceQuads({ x, y }, fCameraAngle, fCameraPitch, fCameraZoom, { vCameraPos.x, 0.0f, vCameraPos.y }, vQuads);
|
||
|
||
// 3) Sort in order of depth, from farthest away to closest
|
||
std::sort(vQuads.begin(), vQuads.end(), [](const sQuad& q1, const sQuad& q2)
|
||
{
|
||
float z1 = (q1.points[0].z + q1.points[1].z + q1.points[2].z + q1.points[3].z) * 0.25f;
|
||
float z2 = (q2.points[0].z + q2.points[1].z + q2.points[2].z + q2.points[3].z) * 0.25f;
|
||
return z1 < z2;
|
||
});
|
||
|
||
// 4) Iterate through all "tile cubes" and draw their visible faces
|
||
Clear(olc::BLACK);
|
||
for (auto& q : vQuads)
|
||
DrawPartialWarpedDecal
|
||
(
|
||
rendAllWalls.decal,
|
||
{ {q.points[0].x, q.points[0].y}, {q.points[1].x, q.points[1].y}, {q.points[2].x, q.points[2].y}, {q.points[3].x, q.points[3].y} },
|
||
q.tile,
|
||
vTileSize
|
||
);
|
||
|
||
// 5) Draw current tile selection
|
||
DrawPartialDecal({ 10,10 }, rendAllWalls.decal, vTileCursor * vTileSize, vTileSize);
|
||
|
||
// 6) Draw selection "tile cube"
|
||
vQuads.clear();
|
||
GetFaceQuads(vCursor, fCameraAngle, fCameraPitch, fCameraZoom, { vCameraPos.x, 0.0f, vCameraPos.y }, vQuads);
|
||
for (auto& q : vQuads)
|
||
DrawWarpedDecal(rendSelect.decal, { {q.points[0].x, q.points[0].y}, {q.points[1].x, q.points[1].y}, {q.points[2].x, q.points[2].y}, {q.points[3].x, q.points[3].y} });
|
||
|
||
// 7) Draw some debug info
|
||
DrawStringDecal({ 0,0 }, "Cursor: " + std::to_string(vCursor.x) + ", " + std::to_string(vCursor.y), olc::YELLOW, { 0.5f, 0.5f });
|
||
DrawStringDecal({ 0,8 }, "Angle: " + std::to_string(fCameraAngle) + ", " + std::to_string(fCameraPitch), olc::YELLOW, { 0.5f, 0.5f });
|
||
|
||
// Graceful exit if user is in full screen mode
|
||
return !GetKey(olc::Key::ESCAPE).bPressed;
|
||
}
|
||
};
|
||
|
||
int main()
|
||
{
|
||
olcDungeon demo;
|
||
if (demo.Construct(640, 480, 2, 2, false))
|
||
demo.Start();
|
||
return 0;
|
||
} |