diff --git a/README.md b/README.md index 14f3e0f..8c50465 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # CommandLineFPS + A First Person Shooter at the command line? Yup... Please see the source file on how to configure your command line before running. -This is designed for MS Windows +Windows and Linux versions are available. See the relevant source directory for details. diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..ca15818 --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.5) # CMake version check +project(CommandLineFPS) # Create project "CommandLineFPS" +set(CMAKE_CXX_STANDARD 11) # Enable c++11 standard + +# Include our FindNcursesw.cmake file +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_MODULE_PATH}) + +# Find Ncursesw +find_package( Ncursesw REQUIRED ) +include_directories( ${CURSES_INCLUDE_DIRS} ) + +# Add CommandLineFPS.cpp file of project root directory as source file +set(SOURCE_FILES CommandLineFPS.cpp) + +# Add executable target with source files listed in SOURCE_FILES variable +add_executable(CommandLineFPS ${SOURCE_FILES}) + +# Link ncursesw library +target_link_libraries( CommandLineFPS ${CURSES_LIBRARIES} ) diff --git a/linux/CommandLineFPS.cpp b/linux/CommandLineFPS.cpp new file mode 100644 index 0000000..b3c298f --- /dev/null +++ b/linux/CommandLineFPS.cpp @@ -0,0 +1,306 @@ +/* + OneLoneCoder.com - Command Line First Person Shooter (FPS) Engine + "Why were games not done like this is 1990?" - @Javidx9 + Disclaimer + ~~~~~~~~~~ + I don't care what you use this for. It's intended to be educational, and perhaps + to the oddly minded - a little bit of fun. Please hack this, change it and use it + in any way you see fit. BUT, you acknowledge that I am not responsible for anything + bad that happens as a result of your actions. However, if good stuff happens, I + would appreciate a shout out, or at least give the blog some publicity for me. + Cheers! + Background + ~~~~~~~~~~ + Whilst waiting for TheMexicanRunner to start the finale of his NesMania project, + his Twitch stream had a counter counting down for a couple of hours until it started. + With some time on my hands, I thought it might be fun to see what the graphical + capabilities of the console are. Turns out, not very much, but hey, it's nice to think + Wolfenstein could have existed a few years earlier, and in just ~200 lines of code. + IMPORTANT!!!! + ~~~~~~~~~~~~~ + READ ME BEFORE RUNNING!!! This program expects the console dimensions to be set to + 120 Columns by 40 Rows. I recommend a small font "Consolas" at size 16. You can do this + by running the program, and right clicking on the console title bar, and specifying + the properties. You can also choose to default to them in the future. + + Controls: A = Turn Left, D = Turn Right, W = Walk Forwards, S = Walk Backwards + Future Modifications + ~~~~~~~~~~~~~~~~~~~~ + 1) Shade block segments based on angle from player, i.e. less light reflected off + walls at side of player. Walls straight on are brightest. + 2) Find an interesting and optimised ray-tracing method. I'm sure one must exist + to more optimally search the map space + 3) Add bullets! + 4) Add bad guys! + Author + ~~~~~~ + Twitter: @javidx9 + Blog: www.onelonecoder.com + Linux Port + ~~~~~~~~~~ + Author: Rohan Liston + GitHub: github.com/rohanliston + Video: + ~~~~~~ + https://youtu.be/xW8skO7MFYw + Last Updated: 27/02/2017 +*/ + +#include +#include +#include +#include +#include +#include + +using namespace std; + +int nScreenWidth = 120; // Console Screen Size X (columns) +int nScreenHeight = 40; // Console Screen Size Y (rows) +int nMapWidth = 16; // World Dimensions +int nMapHeight = 16; + +float fPlayerX = 14.7f; // Player Start Position +float fPlayerY = 5.09f; +float fPlayerA = 0.0f; // Player Start Rotation +float fFOV = 3.14159f / 4.0f; // Field of View +float fDepth = 16.0f; // Maximum rendering distance +float fSpeed = 150.0f; // Walking Speed + +int main() +{ + // NCurses setup + setlocale(LC_ALL, ""); // Set locale for UTF-8 support + initscr(); // Initialise NCurses screen + noecho(); // Don't echo input to screen + curs_set(0); // Don't show terminal cursor + nodelay(stdscr, true); // Don't halt program while waiting for input + cbreak(); // Make input characters immediately available to the program + + // Create Screen Buffer + auto *screen = new wchar_t[nScreenWidth * nScreenHeight]; + + // Create Map of world space # = wall block, . = space + wstring map; + map += L"#########......."; + map += L"#..............."; + map += L"#.......########"; + map += L"#..............#"; + map += L"#......##......#"; + map += L"#......##......#"; + map += L"#..............#"; + map += L"###............#"; + map += L"##.............#"; + map += L"#......####..###"; + map += L"#......#.......#"; + map += L"#......#.......#"; + map += L"#..............#"; + map += L"#......#########"; + map += L"#..............#"; + map += L"################"; + + auto tp1 = chrono::system_clock::now(); + auto tp2 = chrono::system_clock::now(); + + bool finished = false; + while (!finished) + { + // We'll need time differential per frame to calculate modification + // to movement speeds, to ensure consistant movement, as ray-tracing + // is non-deterministic + tp2 = chrono::system_clock::now(); + chrono::duration elapsedTime = tp2 - tp1; + tp1 = tp2; + float fElapsedTime = elapsedTime.count(); + + // Measure terminal size + int terminalWidth; + int terminalHeight; + bool cleared = false; + getmaxyx(stdscr, terminalHeight, terminalWidth); + + // Ensure terminal size is OK + while (terminalHeight != nScreenHeight || terminalWidth != nScreenWidth) + { + if (!cleared) + { + clear(); + cleared = true; + } + + ostringstream out; + out << "Resize your terminal to (" << nScreenWidth << ", " << nScreenHeight << ") - current size is (" << terminalWidth << ", " << terminalHeight << ")"; + mvaddstr(0, 0, out.str().c_str()); + refresh(); + getmaxyx(stdscr, terminalHeight, terminalWidth); + } + + int key = getch(); + switch (key) { + case 'a': + // CCW Rotation + fPlayerA -= (fSpeed * 0.75f) * fElapsedTime; + break; + case 'd': + // CW Rotation + fPlayerA += (fSpeed * 0.75f) * fElapsedTime; + break; + case 'w': + // Forward movement + fPlayerX += sinf(fPlayerA) * fSpeed * fElapsedTime;; + fPlayerY += cosf(fPlayerA) * fSpeed * fElapsedTime;; + if (map.c_str()[(int) fPlayerX * nMapWidth + (int) fPlayerY] == '#') { + fPlayerX -= sinf(fPlayerA) * fSpeed * fElapsedTime;; + fPlayerY -= cosf(fPlayerA) * fSpeed * fElapsedTime;; + } + break; + case 's': + // Backward movement + fPlayerX -= sinf(fPlayerA) * fSpeed * fElapsedTime;; + fPlayerY -= cosf(fPlayerA) * fSpeed * fElapsedTime;; + if (map.c_str()[(int) fPlayerX * nMapWidth + (int) fPlayerY] == '#') { + fPlayerX += sinf(fPlayerA) * fSpeed * fElapsedTime;; + fPlayerY += cosf(fPlayerA) * fSpeed * fElapsedTime;; + } + break; + case 'q': + // Quit + finished = true; + break; + default: + break; + } + + for (int x = 0; x < nScreenWidth; x++) + { + // For each column, calculate the projected ray angle into world space + float fRayAngle = (fPlayerA - fFOV/2.0f) + ((float)x / (float)nScreenWidth) * fFOV; + + // Find distance to wall + float fStepSize = 0.1f; // Increment size for ray casting, decrease to increase resolution + float fDistanceToWall = 0.0f; + + bool bHitWall = false; // Set when ray hits wall block + bool bBoundary = false; // Set when ray hits boundary between two wall blocks + + float fEyeX = sinf(fRayAngle); // Unit vector for ray in player space + float fEyeY = cosf(fRayAngle); + + // Incrementally cast ray from player, along ray angle, testing for + // intersection with a block + while (!bHitWall && fDistanceToWall < fDepth) + { + fDistanceToWall += fStepSize; + int nTestX = (int)(fPlayerX + fEyeX * fDistanceToWall); + int nTestY = (int)(fPlayerY + fEyeY * fDistanceToWall); + + // Test if ray is out of bounds + if (nTestX < 0 || nTestX >= nMapWidth || nTestY < 0 || nTestY >= nMapHeight) + { + bHitWall = true; // Just set distance to maximum depth + fDistanceToWall = fDepth; + } + else + { + // Ray is inbounds so test to see if the ray cell is a wall block + if (map.c_str()[nTestX * nMapWidth + nTestY] == '#') + { + // Ray has hit wall + bHitWall = true; + + // To highlight tile boundaries, cast a ray from each corner + // of the tile, to the player. The more coincident this ray + // is to the rendering ray, the closer we are to a tile + // boundary, which we'll shade to add detail to the walls + vector> p; + + // Test each corner of hit tile, storing the distance from + // the player, and the calculated dot product of the two rays + for (int tx = 0; tx < 2; tx++) + for (int ty = 0; ty < 2; ty++) + { + // Angle of corner to eye + float vy = (float)nTestY + ty - fPlayerY; + float vx = (float)nTestX + tx - fPlayerX; + float d = sqrt(vx*vx + vy*vy); + float dot = (fEyeX * vx / d) + (fEyeY * vy / d); + p.push_back(make_pair(d, dot)); + } + + // Sort Pairs from closest to farthest + sort(p.begin(), p.end(), [](const pair &left, const pair &right) {return left.first < right.first; }); + + // First two/three are closest (we will never see all four) + float fBound = 0.005; + if (acos(p.at(0).second) < fBound) bBoundary = true; + if (acos(p.at(1).second) < fBound) bBoundary = true; + if (acos(p.at(2).second) < fBound) bBoundary = true; + } + } + } + + // Calculate distance to ceiling and floor + int nCeiling = (int)((float)(nScreenHeight/2.0) - nScreenHeight / ((float)fDistanceToWall)); + int nFloor = nScreenHeight - nCeiling; + + // Shader walls based on distance + short nShade = ' '; + if (fDistanceToWall <= fDepth / 4.0f) nShade = 0x2588; // Very close + else if (fDistanceToWall < fDepth / 3.0f) nShade = 0x2593; + else if (fDistanceToWall < fDepth / 2.0f) nShade = 0x2592; + else if (fDistanceToWall < fDepth) nShade = 0x2591; + else nShade = ' '; // Too far away + + if (bBoundary) + nShade = ' '; // Black it out + + for (int y = 0; y < nScreenHeight; y++) + { + // Each Row + if(y <= nCeiling) + screen[y*nScreenWidth + x] = ' '; + else if(y > nCeiling && y <= nFloor) + screen[y*nScreenWidth + x] = nShade; + else // Floor + { + // Shade floor based on distance + float b = 1.0f - (((float)y -nScreenHeight/2.0f) / ((float)nScreenHeight / 2.0f)); + if (b < 0.25) nShade = '#'; + else if (b < 0.5) nShade = 'x'; + else if (b < 0.75) nShade = '.'; + else if (b < 0.9) nShade = '-'; + else nShade = ' '; + screen[y*nScreenWidth + x] = nShade; + } + } + } + + // Display Frame + mvaddwstr(0, 0, screen); + + // Display Stats + wchar_t stats[40]; + swprintf(stats, 40, L"X=%3.2f, Y=%3.2f, A=%3.2f FPS=%3.2f ", fPlayerX, fPlayerY, fPlayerA, 1.0f/fElapsedTime); + mvaddwstr(0, 0, stats); + + // Display Map + for (int nx = 0; nx < nMapWidth; nx++) + { + for (int ny = 0; ny < nMapWidth; ny++) + { + mvaddch(ny+1, nx, (chtype)map[ny * nMapWidth + nx]); + } + } + + // Display Player + mvaddch((int)fPlayerX+1, ((int)fPlayerY), 'P'); + + // Draw screen + refresh(); + } + + endwin(); + return 0; +} + +// That's It!! - Jx9 diff --git a/linux/FindNcursesw.cmake b/linux/FindNcursesw.cmake new file mode 100644 index 0000000..e572c70 --- /dev/null +++ b/linux/FindNcursesw.cmake @@ -0,0 +1,204 @@ +#.rst: +# FindNcursesw +# ------------ +# +# Find the ncursesw (wide ncurses) include file and library. +# +# Based on FindCurses.cmake which comes with CMake. +# +# Checks for ncursesw first. If not found, it then executes the +# regular old FindCurses.cmake to look for for ncurses (or curses). +# +# +# Result Variables +# ^^^^^^^^^^^^^^^^ +# +# This module defines the following variables: +# +# ``CURSES_FOUND`` +# True if curses is found. +# ``NCURSESW_FOUND`` +# True if ncursesw is found. +# ``CURSES_INCLUDE_DIRS`` +# The include directories needed to use Curses. +# ``CURSES_LIBRARIES`` +# The libraries needed to use Curses. +# ``CURSES_HAVE_CURSES_H`` +# True if curses.h is available. +# ``CURSES_HAVE_NCURSES_H`` +# True if ncurses.h is available. +# ``CURSES_HAVE_NCURSES_NCURSES_H`` +# True if ``ncurses/ncurses.h`` is available. +# ``CURSES_HAVE_NCURSES_CURSES_H`` +# True if ``ncurses/curses.h`` is available. +# ``CURSES_HAVE_NCURSESW_NCURSES_H`` +# True if ``ncursesw/ncurses.h`` is available. +# ``CURSES_HAVE_NCURSESW_CURSES_H`` +# True if ``ncursesw/curses.h`` is available. +# +# Set ``CURSES_NEED_NCURSES`` to ``TRUE`` before the +# ``find_package(Ncursesw)`` call if NCurses functionality is required. +# +#============================================================================= +# Copyright 2001-2014 Kitware, Inc. +# modifications: Copyright 2015 kahrl +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the names of Kitware, Inc., the Insight Software Consortium, +# nor the names of their 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. +# +# ------------------------------------------------------------------------------ +# +# The above copyright and license notice applies to distributions of +# CMake in source and binary form. Some source files contain additional +# notices of original copyright by their contributors; see each source +# for details. Third-party software packages supplied with CMake under +# compatible licenses provide their own copyright notices documented in +# corresponding subdirectories. +# +# ------------------------------------------------------------------------------ +# +# CMake was initially developed by Kitware with the following sponsorship: +# +# * National Library of Medicine at the National Institutes of Health +# as part of the Insight Segmentation and Registration Toolkit (ITK). +# +# * US National Labs (Los Alamos, Livermore, Sandia) ASC Parallel +# Visualization Initiative. +# +# * National Alliance for Medical Image Computing (NAMIC) is funded by the +# National Institutes of Health through the NIH Roadmap for Medical Research, +# Grant U54 EB005149. +# +# * Kitware, Inc. +#============================================================================= + +include(CheckLibraryExists) + +find_library(CURSES_NCURSESW_LIBRARY NAMES ncursesw + DOC "Path to libncursesw.so or .lib or .a") + +set(CURSES_USE_NCURSES FALSE) +set(CURSES_USE_NCURSESW FALSE) + +if(CURSES_NCURSESW_LIBRARY) + set(CURSES_USE_NCURSES TRUE) + set(CURSES_USE_NCURSESW TRUE) +endif() + +if(CURSES_USE_NCURSESW) + get_filename_component(_cursesLibDir "${CURSES_NCURSESW_LIBRARY}" PATH) + get_filename_component(_cursesParentDir "${_cursesLibDir}" PATH) + + find_path(CURSES_INCLUDE_PATH + NAMES ncursesw/ncurses.h ncursesw/curses.h ncurses.h curses.h + HINTS "${_cursesParentDir}/include" + ) + + # Previous versions of FindCurses provided these values. + if(NOT DEFINED CURSES_LIBRARY) + set(CURSES_LIBRARY "${CURSES_NCURSESW_LIBRARY}") + endif() + + CHECK_LIBRARY_EXISTS("${CURSES_NCURSESW_LIBRARY}" + cbreak "" CURSES_NCURSESW_HAS_CBREAK) + if(NOT CURSES_NCURSESW_HAS_CBREAK) + find_library(CURSES_EXTRA_LIBRARY tinfo HINTS "${_cursesLibDir}" + DOC "Path to libtinfo.so or .lib or .a") + find_library(CURSES_EXTRA_LIBRARY tinfo ) + endif() + + # Report whether each possible header name exists in the include directory. + if(NOT DEFINED CURSES_HAVE_NCURSESW_NCURSES_H) + if(EXISTS "${CURSES_INCLUDE_PATH}/ncursesw/ncurses.h") + set(CURSES_HAVE_NCURSESW_NCURSES_H "${CURSES_INCLUDE_PATH}/ncursesw/ncurses.h") + else() + set(CURSES_HAVE_NCURSESW_NCURSES_H "CURSES_HAVE_NCURSESW_NCURSES_H-NOTFOUND") + endif() + endif() + if(NOT DEFINED CURSES_HAVE_NCURSESW_CURSES_H) + if(EXISTS "${CURSES_INCLUDE_PATH}/ncursesw/curses.h") + set(CURSES_HAVE_NCURSESW_CURSES_H "${CURSES_INCLUDE_PATH}/ncursesw/curses.h") + else() + set(CURSES_HAVE_NCURSESW_CURSES_H "CURSES_HAVE_NCURSESW_CURSES_H-NOTFOUND") + endif() + endif() + if(NOT DEFINED CURSES_HAVE_NCURSES_H) + if(EXISTS "${CURSES_INCLUDE_PATH}/ncurses.h") + set(CURSES_HAVE_NCURSES_H "${CURSES_INCLUDE_PATH}/ncurses.h") + else() + set(CURSES_HAVE_NCURSES_H "CURSES_HAVE_NCURSES_H-NOTFOUND") + endif() + endif() + if(NOT DEFINED CURSES_HAVE_CURSES_H) + if(EXISTS "${CURSES_INCLUDE_PATH}/curses.h") + set(CURSES_HAVE_CURSES_H "${CURSES_INCLUDE_PATH}/curses.h") + else() + set(CURSES_HAVE_CURSES_H "CURSES_HAVE_CURSES_H-NOTFOUND") + endif() + endif() + + + find_library(CURSES_FORM_LIBRARY form HINTS "${_cursesLibDir}" + DOC "Path to libform.so or .lib or .a") + find_library(CURSES_FORM_LIBRARY form ) + + # Need to provide the *_LIBRARIES + set(CURSES_LIBRARIES ${CURSES_LIBRARY}) + + if(CURSES_EXTRA_LIBRARY) + set(CURSES_LIBRARIES ${CURSES_LIBRARIES} ${CURSES_EXTRA_LIBRARY}) + endif() + + if(CURSES_FORM_LIBRARY) + set(CURSES_LIBRARIES ${CURSES_LIBRARIES} ${CURSES_FORM_LIBRARY}) + endif() + + # Provide the *_INCLUDE_DIRS result. + set(CURSES_INCLUDE_DIRS ${CURSES_INCLUDE_PATH}) + set(CURSES_INCLUDE_DIR ${CURSES_INCLUDE_PATH}) # compatibility + + # handle the QUIETLY and REQUIRED arguments and set CURSES_FOUND to TRUE if + # all listed variables are TRUE + include(FindPackageHandleStandardArgs) + FIND_PACKAGE_HANDLE_STANDARD_ARGS(Ncursesw DEFAULT_MSG + CURSES_LIBRARY CURSES_INCLUDE_PATH) + set(CURSES_FOUND ${NCURSESW_FOUND}) + +else() + find_package(Curses) + set(NCURSESW_FOUND FALSE) +endif() + +mark_as_advanced( + CURSES_INCLUDE_PATH + CURSES_CURSES_LIBRARY + CURSES_NCURSES_LIBRARY + CURSES_NCURSESW_LIBRARY + CURSES_EXTRA_LIBRARY + CURSES_FORM_LIBRARY + ) diff --git a/linux/README.md b/linux/README.md new file mode 100644 index 0000000..c90cd74 --- /dev/null +++ b/linux/README.md @@ -0,0 +1,27 @@ +# CommandLineFPS - Linux Version + +![Screenshot](screenshot.png) +## Prerequisites + +* CMake - C/C++ build system +* NCursesw - Library for interacting with the terminal + +### Installation + +* Ubuntu: `sudo apt-get install cmake libncursesw5-dev` +* Other distros: `¯\_(ツ)_/¯` + +## Build + +```sh +mkdir build +cd build +cmake .. +make +``` + +## Run + +`./CommandLineFPS` + + diff --git a/linux/screenshot.png b/linux/screenshot.png new file mode 100644 index 0000000..0bc1dff Binary files /dev/null and b/linux/screenshot.png differ diff --git a/CommandLineFPS.cpp b/windows/CommandLineFPS.cpp similarity index 100% rename from CommandLineFPS.cpp rename to windows/CommandLineFPS.cpp