diff --git a/olcConsoleGameEngine.h b/olcConsoleGameEngine.h index 16ef2ba..fa2982d 100644 --- a/olcConsoleGameEngine.h +++ b/olcConsoleGameEngine.h @@ -1,804 +1,776 @@ -/* -OneLoneCoder.com - Command Line Game Engine -"Who needs a frame buffer?" - @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 -~~~~~~~~~~ -If you've seen any of my videos - I like to do things using the windows console. It's quick -and easy, and allows you to focus on just the code that matters - ideal when you're -experimenting. Thing is, I have to keep doing the same initialisation and display code -each time, so this class wraps that up. - -Author -~~~~~~ -Twitter: @javidx9 http://twitter.com/javidx9 -Blog: http://www.onelonecoder.com -YouTube: http://www.youtube.com/javidx9 - -Videos: -~~~~~~ -Original: https://youtu.be/cWc0hgYwZyc -Added mouse support: https://youtu.be/tdqc9hZhHxM -Beginners Guide: https://youtu.be/u5BhrA8ED0o - -Shout Outs! -~~~~~~~~~~~ -Thanks to cool people who helped with testing, bug-finding and fixing! - YouTube: wowLinh, JavaJack59 - -Last Updated: 05/11/2017 - -Usage: -~~~~~~ -This class is abstract, so you must inherit from it. Override the OnUserCreate() function -with all the stuff you need for your application (for thready reasons it's best to do -this in this function and not your class constructor). Override the OnUserUpdate(float fElapsedTime) -function with the good stuff, it gives you the elapsed time since the last call so you -can modify your stuff dynamically. Both functions should return true, unless you need -the application to close. - - int main() - { - // Use olcConsoleGameEngine derived app - OneLoneCoder_Example game; - - // Create a console with resolution 160x100 characters - // Each character occupies 8x8 pixels - game.ConstructConsole(160, 100, 8, 8); - - // Start the engine! - game.Start(); - - return 0; - } - -Input is also handled for you - interrogate the m_keys[] array with the virtual -keycode you want to know about. bPressed is set for the frame the key is pressed down -in, bHeld is set if the key is held down, bReleased is set for the frame the key -is released in. The same applies to mouse! m_mousePosX and Y can be used to get -the current cursor position, and m_mouse[1..5] returns the mouse buttons. - -The draw routines treat characters like pixels. By default they are set to white solid -blocks - but you can draw any unicode character, using any of the colours listed below. - -There may be bugs! - -See my other videos for examples! -http://www.youtube.com/javidx9 - -*/ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -using namespace std; - -#include - -enum COLOUR -{ - FG_BLACK = 0x0000, - FG_DARK_BLUE = 0x0001, - FG_DARK_GREEN = 0x0002, - FG_DARK_CYAN = 0x0003, - FG_DARK_RED = 0x0004, - FG_DARK_MAGENTA = 0x0005, - FG_DARK_YELLOW = 0x0006, - FG_GREY = 0x0007, // Thanks MS :-/ - FG_DARK_GREY = 0x0008, - FG_BLUE = 0x0009, - FG_GREEN = 0x000A, - FG_CYAN = 0x000B, - FG_RED = 0x000C, - FG_MAGENTA = 0x000D, - FG_YELLOW = 0x000E, - FG_WHITE = 0x000F, - BG_BLACK = 0x0000, - BG_DARK_BLUE = 0x0010, - BG_DARK_GREEN = 0x0020, - BG_DARK_CYAN = 0x0030, - BG_DARK_RED = 0x0040, - BG_DARK_MAGENTA = 0x0050, - BG_DARK_YELLOW = 0x0060, - BG_GREY = 0x0070, - BG_DARK_GREY = 0x0080, - BG_BLUE = 0x0090, - BG_GREEN = 0x00A0, - BG_CYAN = 0x00B0, - BG_RED = 0x00C0, - BG_MAGENTA = 0x00D0, - BG_YELLOW = 0x00E0, - BG_WHITE = 0x00F0, -}; - -enum PIXEL_TYPE -{ - PIXEL_SOLID = 0x2588, - PIXEL_THREEQUARTERS = 0x2593, - PIXEL_HALF = 0x2592, - PIXEL_QUARTER = 0x2591, -}; - -class olcSprite -{ -public: - olcSprite() - { - - } - - olcSprite(int w, int h) - { - Create(w, h); - } - - olcSprite(wstring sFile) - { - if (!Load(sFile)) - Create(8, 8); - } - - int nWidth = 0; - int nHeight = 0; - -private: - wchar_t *m_Glyphs = nullptr; - short *m_Colours = nullptr; - - void Create(int w, int h) - { - nWidth = w; - nHeight = h; - m_Glyphs = new wchar_t[w*h]; - m_Colours = new short[w*h]; - for (int i = 0; i < w*h; i++) - { - m_Glyphs[i] = L' '; - m_Colours[i] = FG_BLACK; - } - } - -public: - void SetGlyph(int x, int y, wchar_t c) - { - if (x <0 || x > nWidth || y < 0 || y > nHeight) - return; - else - m_Glyphs[y * nWidth + x] = c; - } - - void SetColour(int x, int y, short c) - { - if (x <0 || x > nWidth || y < 0 || y > nHeight) - return; - else - m_Colours[y * nWidth + x] = c; - } - - wchar_t GetGlyph(int x, int y) - { - if (x <0 || x > nWidth || y < 0 || y > nHeight) - return L' '; - else - return m_Glyphs[y * nWidth + x]; - } - - short GetColour(int x, int y) - { - if (x <0 || x > nWidth || y < 0 || y > nHeight) - return FG_BLACK; - else - return m_Colours[y * nWidth + x]; - } - - wchar_t SampleGlyph(float x, float y) - { - int sx = x * (float)nWidth; - int sy = y * (float)nHeight-1; - if (sx <0 || sx > nWidth || sy < 0 || sy > nHeight) - return L' '; - else - return m_Glyphs[sy * nWidth + sx]; - } - - short SampleColour(float x, float y) - { - int sx = x * (float)nWidth; - int sy = y * (float)nHeight-1; - if (sx <0 || sx > nWidth || sy < 0 || sy > nHeight) - return FG_BLACK; - else - return m_Colours[sy * nWidth + sx]; - } - - bool Save(wstring sFile) - { - FILE *f = nullptr; - _wfopen_s(&f, sFile.c_str(), L"wb"); - if (f == nullptr) - return false; - - fwrite(&nWidth, sizeof(int), 1, f); - fwrite(&nHeight, sizeof(int), 1, f); - fwrite(m_Colours, sizeof(short), nWidth * nHeight, f); - fwrite(m_Glyphs, sizeof(wchar_t), nWidth * nHeight, f); - - fclose(f); - - return true; - } - - bool Load(wstring sFile) - { - delete[] m_Glyphs; - delete[] m_Colours; - nWidth = 0; - nHeight = 0; - - FILE *f = nullptr; - _wfopen_s(&f, sFile.c_str(), L"rb"); - if (f == nullptr) - return false; - - fread(&nWidth, sizeof(int), 1, f); - fread(&nHeight, sizeof(int), 1, f); - - Create(nWidth, nHeight); - - fread(m_Colours, sizeof(short), nWidth * nHeight, f); - fread(m_Glyphs, sizeof(wchar_t), nWidth * nHeight, f); - - fclose(f); - return true; - } - -}; - - -class olcConsoleGameEngine -{ -public: - olcConsoleGameEngine() - { - m_nScreenWidth = 80; - m_nScreenHeight = 30; - - m_hConsole = GetStdHandle(STD_OUTPUT_HANDLE); - m_hConsoleIn = GetStdHandle(STD_INPUT_HANDLE); - - m_keyNewState = new short[256]; - m_keyOldState = new short[256]; - memset(m_keyNewState, 0, 256 * sizeof(short)); - memset(m_keyOldState, 0, 256 * sizeof(short)); - memset(m_keys, 0, 256 * sizeof(sKeyState)); - - m_mousePosX = 0; - m_mousePosY = 0; - - m_sAppName = L"Default"; - } - - // Update 14/09/2017 - Below is the original implementation of CreateConsole(). This works - // on many, but not all systems. A revised version is used below. This will be removed - // once it is established the revision is stable. Jx9 - - /*int ConstructConsole(int width, int height, int fontw = 12, int fonth = 12) - { - m_nScreenWidth = width; - m_nScreenHeight = height; - - CONSOLE_FONT_INFOEX cfi; - cfi.cbSize = sizeof(cfi); - cfi.nFont = 0; - cfi.dwFontSize.X = fontw; - cfi.dwFontSize.Y = fonth; - cfi.FontFamily = FF_DONTCARE; - cfi.FontWeight = FW_NORMAL; - wcscpy_s(cfi.FaceName, L"Consolas"); - - if (!SetCurrentConsoleFontEx(m_hConsole, false, &cfi)) - return Error(L"SetCurrentConsoleFontEx"); - - COORD coordLargest = GetLargestConsoleWindowSize(m_hConsole); - if (m_nScreenHeight > coordLargest.Y) - return Error(L"Game Height Too Big"); - if (m_nScreenWidth > coordLargest.X) - return Error(L"Game Width Too Big"); - - COORD buffer = { (short)m_nScreenWidth, (short)m_nScreenHeight }; - if (!SetConsoleScreenBufferSize(m_hConsole, buffer)) - return Error(L"SetConsoleScreenBufferSize"); - - m_rectWindow = { 0, 0, (short)m_nScreenWidth - 1, (short)m_nScreenHeight - 1 }; - if (!SetConsoleWindowInfo(m_hConsole, TRUE, &m_rectWindow)) - return Error(L"SetConsoleWindowInfo"); - - // Set flags to allow mouse input - if (!SetConsoleMode(m_hConsoleIn, ENABLE_EXTENDED_FLAGS | ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT)) - return Error(L"SetConsoleMode"); - - m_bufScreen = new CHAR_INFO[m_nScreenWidth*m_nScreenHeight]; - - return 1; - }*/ - - int olcConsoleGameEngine::ConstructConsole(int width, int height, int fontw, int fonth) - { - if (m_hConsole == INVALID_HANDLE_VALUE) - return Error(L"Bad Handle"); - - m_nScreenWidth = width; - m_nScreenHeight = height; - - // Update 13/09/2017 - It seems that the console behaves differently on some systems - // and I'm unsure why this is. It could be to do with windows default settings, or - // screen resolutions, or system languages. Unfortunately, MSDN does not offer much - // by way of useful information, and so the resulting sequence is the reult of experiment - // that seems to work in multiple cases. - // - // The problem seems to be that the SetConsoleXXX functions are somewhat circular and - // fail depending on the state of the current console properties, i.e. you can't set - // the buffer size until you set the screen size, but you can't change the screen size - // until the buffer size is correct. This coupled with a precise ordering of calls - // makes this procedure seem a little mystical :-P. Thanks to wowLinh for helping - Jx9 - - // Change console visual size to a minimum so ScreenBuffer can shrink - // below the actual visual size - m_rectWindow = { 0, 0, 1, 1 }; - SetConsoleWindowInfo(m_hConsole, TRUE, &m_rectWindow); - - // Set the size of the screen buffer - COORD coord = { (short)m_nScreenWidth, (short)m_nScreenHeight }; - if (!SetConsoleScreenBufferSize(m_hConsole, coord)) - Error(L"SetConsoleScreenBufferSize"); - - // Assign screen buffer to the console - if (!SetConsoleActiveScreenBuffer(m_hConsole)) - return Error(L"SetConsoleActiveScreenBuffer"); - - // Set the font size now that the screen buffer has been assigned to the console - CONSOLE_FONT_INFOEX cfi; - cfi.cbSize = sizeof(cfi); - cfi.nFont = 0; - cfi.dwFontSize.X = fontw; - cfi.dwFontSize.Y = fonth; - cfi.FontFamily = FF_DONTCARE; - cfi.FontWeight = FW_NORMAL; - wcscpy_s(cfi.FaceName, L"Consolas"); - if (!SetCurrentConsoleFontEx(m_hConsole, false, &cfi)) - return Error(L"SetCurrentConsoleFontEx"); - - // Get screen buffer info and check the maximum allowed window size. Return - // error if exceeded, so user knows their dimensions/fontsize are too large - CONSOLE_SCREEN_BUFFER_INFO csbi; - if (!GetConsoleScreenBufferInfo(m_hConsole, &csbi)) - return Error(L"GetConsoleScreenBufferInfo"); - if (m_nScreenHeight > csbi.dwMaximumWindowSize.Y) - return Error(L"Screen Height / Font Height Too Big"); - if (m_nScreenWidth > csbi.dwMaximumWindowSize.X) - return Error(L"Screen Width / Font Width Too Big"); - - // Set Physical Console Window Size - m_rectWindow = { 0, 0, (short)m_nScreenWidth - 1, (short)m_nScreenHeight - 1 }; - if (!SetConsoleWindowInfo(m_hConsole, TRUE, &m_rectWindow)) - return Error(L"SetConsoleWindowInfo"); - - // Set flags to allow mouse input - if (!SetConsoleMode(m_hConsoleIn, ENABLE_EXTENDED_FLAGS | ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT)) - return Error(L"SetConsoleMode"); - - // Allocate memory for screen buffer - m_bufScreen = new CHAR_INFO[m_nScreenWidth*m_nScreenHeight]; - memset(m_bufScreen, 0, sizeof(CHAR_INFO) * m_nScreenWidth * m_nScreenHeight); - - return 1; - } - - virtual void Draw(int x, int y, wchar_t c = 0x2588, short col = 0x000F) - { - if (x >= 0 && x < m_nScreenWidth && y >= 0 && y < m_nScreenHeight) - { - m_bufScreen[y * m_nScreenWidth + x].Char.UnicodeChar = c; - m_bufScreen[y * m_nScreenWidth + x].Attributes = col; - } - } - - void Fill(int x1, int y1, int x2, int y2, wchar_t c = 0x2588, short col = 0x000F) - { - Clip(x1, y1); - Clip(x2, y2); - for (int x = x1; x < x2; x++) - for (int y = y1; y < y2; y++) - Draw(x, y, c, col); - } - - void DrawString(int x, int y, wstring c, short col = 0x000F) - { - for (size_t i = 0; i < c.size(); i++) - { - m_bufScreen[y * m_nScreenWidth + x + i].Char.UnicodeChar = c[i]; - m_bufScreen[y * m_nScreenWidth + x + i].Attributes = col; - } - } - - void DrawStringAlpha(int x, int y, wstring c, short col = 0x000F) - { - for (size_t i = 0; i < c.size(); i++) - { - if (c[i] != L' ') - { - m_bufScreen[y * m_nScreenWidth + x + i].Char.UnicodeChar = c[i]; - m_bufScreen[y * m_nScreenWidth + x + i].Attributes = col; - } - } - } - - void Clip(int &x, int &y) - { - if (x < 0) x = 0; - if (x >= m_nScreenWidth) x = m_nScreenWidth; - if (y < 0) y = 0; - if (y >= m_nScreenHeight) y = m_nScreenHeight; - } - - void DrawLine(int x1, int y1, int x2, int y2, wchar_t c = 0x2588, short col = 0x000F) - { - int x, y, dx, dy, dx1, dy1, px, py, xe, ye, i; - dx = x2 - x1; - dy = y2 - y1; - dx1 = abs(dx); - dy1 = abs(dy); - px = 2 * dy1 - dx1; - py = 2 * dx1 - dy1; - if (dy1 <= dx1) - { - if (dx >= 0) - { - x = x1; - y = y1; - xe = x2; - } - else - { - x = x2; - y = y2; - xe = x1; - } - Draw(x, y, c, col); - for (i = 0; x0 && dy>0)) - y = y + 1; - else - y = y - 1; - px = px + 2 * (dy1 - dx1); - } - Draw(x, y, c, col); - } - } - else - { - if (dy >= 0) - { - x = x1; - y = y1; - ye = y2; - } - else - { - x = x2; - y = y2; - ye = y1; - } - Draw(x, y, c, col); - for (i = 0; y0 && dy>0)) - x = x + 1; - else - x = x - 1; - py = py + 2 * (dx1 - dy1); - } - Draw(x, y, c, col); - } - } - } - - void DrawSprite(int x, int y, olcSprite *sprite) - { - if (sprite == nullptr) - return; - - for (int i = 0; i < sprite->nWidth; i++) - { - for (int j = 0; j < sprite->nHeight; j++) - { - if (sprite->GetGlyph(i, j) != L' ') - Draw(x + i, y + j, sprite->GetGlyph(i, j), sprite->GetColour(i, j)); - } - } - } - - void DrawPartialSprite(int x, int y, olcSprite *sprite, int ox, int oy, int w, int h) - { - if (sprite == nullptr) - return; - - for (int i = 0; i < w; i++) - { - for (int j = 0; j < h; j++) - { - if (sprite->GetGlyph(i+ox, j+oy) != L' ') - Draw(x + i, y + j, sprite->GetGlyph(i+ox, j+oy), sprite->GetColour(i+ox, j+oy)); - } - } - } - - void DrawWireFrameModel(const vector> &vecModelCoordinates, float x, float y, float r = 0.0f, float s = 1.0f, short col = FG_WHITE) - { - // pair.first = x coordinate - // pair.second = y coordinate - - // Create translated model vector of coordinate pairs - vector> vecTransformedCoordinates; - int verts = vecModelCoordinates.size(); - vecTransformedCoordinates.resize(verts); - - // Rotate - for (int i = 0; i < verts; i++) - { - vecTransformedCoordinates[i].first = vecModelCoordinates[i].first * cosf(r) - vecModelCoordinates[i].second * sinf(r); - vecTransformedCoordinates[i].second = vecModelCoordinates[i].first * sinf(r) + vecModelCoordinates[i].second * cosf(r); - } - - // Scale - for (int i = 0; i < verts; i++) - { - vecTransformedCoordinates[i].first = vecTransformedCoordinates[i].first * s; - vecTransformedCoordinates[i].second = vecTransformedCoordinates[i].second * s; - } - - // Translate - for (int i = 0; i < verts; i++) - { - vecTransformedCoordinates[i].first = vecTransformedCoordinates[i].first + x; - vecTransformedCoordinates[i].second = vecTransformedCoordinates[i].second + y; - } - - // Draw Closed Polygon - for (int i = 0; i < verts + 1; i++) - { - int j = (i + 1); - DrawLine((int)vecTransformedCoordinates[i % verts].first, (int)vecTransformedCoordinates[i % verts].second, - (int)vecTransformedCoordinates[j % verts].first, (int)vecTransformedCoordinates[j % verts].second, PIXEL_SOLID, col); - } - } - - ~olcConsoleGameEngine() - { - SetConsoleActiveScreenBuffer(m_hOriginalConsole); - delete[] m_bufScreen; - } - -public: - void Start() - { - m_bAtomActive = true; - - // Star the thread - thread t = thread(&olcConsoleGameEngine::GameThread, this); - - // Wait for thread to be exited - m_cvGameFinished.wait(unique_lock(m_muxGame)); - - // Tidy up - t.join(); - } - - int ScreenWidth() - { - return m_nScreenWidth; - } - - int ScreenHeight() - { - return m_nScreenHeight; - } - -private: - void GameThread() - { - // Create user resources as part of this thread - if (!OnUserCreate()) - m_bAtomActive = false; - - auto tp1 = chrono::system_clock::now(); - auto tp2 = chrono::system_clock::now(); - - // Run as fast as possible - while (m_bAtomActive) - { - // Handle Timing - tp2 = chrono::system_clock::now(); - chrono::duration elapsedTime = tp2 - tp1; - tp1 = tp2; - float fElapsedTime = elapsedTime.count(); - - // Handle Keyboard Input - for (int i = 0; i < 256; i++) - { - m_keyNewState[i] = GetAsyncKeyState(i); - - m_keys[i].bPressed = false; - m_keys[i].bReleased = false; - - if (m_keyNewState[i] != m_keyOldState[i]) - { - if (m_keyNewState[i] & 0x8000) - { - m_keys[i].bPressed = !m_keys[i].bHeld; - m_keys[i].bHeld = true; - } - else - { - m_keys[i].bReleased = true; - m_keys[i].bHeld = false; - } - } - - m_keyOldState[i] = m_keyNewState[i]; - } - - // Handle Mouse Input - Check for window events - INPUT_RECORD inBuf[32]; - DWORD events = 0; - GetNumberOfConsoleInputEvents(m_hConsoleIn, &events); - if(events > 0) - ReadConsoleInput(m_hConsoleIn, inBuf, events, &events); - - // Handle events - we only care about mouse clicks and movement - // for now - for (DWORD i = 0; i < events; i++) - { - switch (inBuf[i].EventType) - { - case MOUSE_EVENT: - { - switch (inBuf[i].Event.MouseEvent.dwEventFlags) - { - case MOUSE_MOVED: - { - m_mousePosX = inBuf[i].Event.MouseEvent.dwMousePosition.X; - m_mousePosY = inBuf[i].Event.MouseEvent.dwMousePosition.Y; - } - break; - - case 0: - { - for (int m = 0; m < 5; m++) - m_mouseNewState[m] = (inBuf[i].Event.MouseEvent.dwButtonState & (1 << m)) > 0; - - } - break; - - default: - break; - } - } - break; - - default: - break; - // We don't care just at the moment - } - } - - for (int m = 0; m < 5; m++) - { - m_mouse[m].bPressed = false; - m_mouse[m].bReleased = false; - - if (m_mouseNewState[m] != m_mouseOldState[m]) - { - if (m_mouseNewState[m]) - { - m_mouse[m].bPressed = true; - m_mouse[m].bHeld = true; - } - else - { - m_mouse[m].bReleased = true; - m_mouse[m].bHeld = false; - } - } - - m_mouseOldState[m] = m_mouseNewState[m]; - } - - - // Handle Frame Update - if (!OnUserUpdate(fElapsedTime)) - m_bAtomActive = false; - - // Update Title & Present Screen Buffer - wchar_t s[256]; - swprintf_s(s, 256, L"OneLoneCoder.com - Console Game Engine - %s - FPS: %3.2f - %d ", m_sAppName.c_str(), 1.0f / fElapsedTime, events); - SetConsoleTitle(s); - WriteConsoleOutput(m_hConsole, m_bufScreen, { (short)m_nScreenWidth, (short)m_nScreenHeight }, { 0,0 }, &m_rectWindow); - } - - m_cvGameFinished.notify_one(); - } - -public: - // User MUST OVERRIDE THESE!! - virtual bool OnUserCreate() = 0; - virtual bool OnUserUpdate(float fElapsedTime) = 0; - - -protected: - int m_nScreenWidth; - int m_nScreenHeight; - CHAR_INFO *m_bufScreen; - atomic m_bAtomActive; - condition_variable m_cvGameFinished; - mutex m_muxGame; - wstring m_sAppName; - - struct sKeyState - { - bool bPressed; - bool bReleased; - bool bHeld; - } m_keys[256], m_mouse[5]; - - int m_mousePosX; - int m_mousePosY; - - -protected: - int Error(wchar_t *msg) - { - wchar_t buf[256]; - FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), buf, 256, NULL); - SetConsoleActiveScreenBuffer(m_hOriginalConsole); - wprintf(L"ERROR: %s\n\t%s\n", msg, buf); - return -1; - } - -private: - HANDLE m_hOriginalConsole; - CONSOLE_SCREEN_BUFFER_INFO m_OriginalConsoleInfo; - HANDLE m_hConsole; - HANDLE m_hConsoleIn; - SMALL_RECT m_rectWindow; - short *m_keyOldState; - short *m_keyNewState; - bool m_mouseOldState[5]; - bool m_mouseNewState[5]; -}; +/* +OneLoneCoder.com - Command Line Game Engine +"Who needs a frame buffer?" - @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 +~~~~~~~~~~ +If you've seen any of my videos - I like to do things using the windows console. It's quick +and easy, and allows you to focus on just the code that matters - ideal when you're +experimenting. Thing is, I have to keep doing the same initialisation and display code +each time, so this class wraps that up. + +Author +~~~~~~ +Twitter: @javidx9 http://twitter.com/javidx9 +Blog: http://www.onelonecoder.com +YouTube: http://www.youtube.com/javidx9 + +Videos: +~~~~~~ +Original: https://youtu.be/cWc0hgYwZyc +Added mouse support: https://youtu.be/tdqc9hZhHxM +Beginners Guide: https://youtu.be/u5BhrA8ED0o + +Shout Outs! +~~~~~~~~~~~ +Thanks to cool people who helped with testing, bug-finding and fixing! + YouTube: wowLinh, JavaJack59, idkwid + +Last Updated: 07/12/2017 + +Usage: +~~~~~~ +This class is abstract, so you must inherit from it. Override the OnUserCreate() function +with all the stuff you need for your application (for thready reasons it's best to do +this in this function and not your class constructor). Override the OnUserUpdate(float fElapsedTime) +function with the good stuff, it gives you the elapsed time since the last call so you +can modify your stuff dynamically. Both functions should return true, unless you need +the application to close. + + int main() + { + // Use olcConsoleGameEngine derived app + OneLoneCoder_Example game; + + // Create a console with resolution 160x100 characters + // Each character occupies 8x8 pixels + game.ConstructConsole(160, 100, 8, 8); + + // Start the engine! + game.Start(); + + return 0; + } + +Input is also handled for you - interrogate the m_keys[] array with the virtual +keycode you want to know about. bPressed is set for the frame the key is pressed down +in, bHeld is set if the key is held down, bReleased is set for the frame the key +is released in. The same applies to mouse! m_mousePosX and Y can be used to get +the current cursor position, and m_mouse[1..5] returns the mouse buttons. + +The draw routines treat characters like pixels. By default they are set to white solid +blocks - but you can draw any unicode character, using any of the colours listed below. + +There may be bugs! + +See my other videos for examples! +http://www.youtube.com/javidx9 + +Lots of programs to try: +http://www.github.com/OneLoneCoder/videos + +Chat on the Discord server: +https://discord.gg/WhwHUMV + +Be bored by Twitch: +http://www.twitch.tv/javidx9 + +*/ + +#pragma once + +#ifndef UNICODE +#error Please enable UNICODE for your compiler! VS: Project Properties -> General -> \ +Character Set -> Use Unicode. In Code::Blocks, include 'UNICODE' and '_UNICODE' as \ +pre-processor directives. Thanks! - Javidx9 +#endif + +#include +#include +#include +#include +#include +#include +#include +using namespace std; + +#include + +enum COLOUR +{ + FG_BLACK = 0x0000, + FG_DARK_BLUE = 0x0001, + FG_DARK_GREEN = 0x0002, + FG_DARK_CYAN = 0x0003, + FG_DARK_RED = 0x0004, + FG_DARK_MAGENTA = 0x0005, + FG_DARK_YELLOW = 0x0006, + FG_GREY = 0x0007, // Thanks MS :-/ + FG_DARK_GREY = 0x0008, + FG_BLUE = 0x0009, + FG_GREEN = 0x000A, + FG_CYAN = 0x000B, + FG_RED = 0x000C, + FG_MAGENTA = 0x000D, + FG_YELLOW = 0x000E, + FG_WHITE = 0x000F, + BG_BLACK = 0x0000, + BG_DARK_BLUE = 0x0010, + BG_DARK_GREEN = 0x0020, + BG_DARK_CYAN = 0x0030, + BG_DARK_RED = 0x0040, + BG_DARK_MAGENTA = 0x0050, + BG_DARK_YELLOW = 0x0060, + BG_GREY = 0x0070, + BG_DARK_GREY = 0x0080, + BG_BLUE = 0x0090, + BG_GREEN = 0x00A0, + BG_CYAN = 0x00B0, + BG_RED = 0x00C0, + BG_MAGENTA = 0x00D0, + BG_YELLOW = 0x00E0, + BG_WHITE = 0x00F0, +}; + +enum PIXEL_TYPE +{ + PIXEL_SOLID = 0x2588, + PIXEL_THREEQUARTERS = 0x2593, + PIXEL_HALF = 0x2592, + PIXEL_QUARTER = 0x2591, +}; + +class olcSprite +{ +public: + olcSprite() + { + + } + + olcSprite(int w, int h) + { + Create(w, h); + } + + olcSprite(wstring sFile) + { + if (!Load(sFile)) + Create(8, 8); + } + + int nWidth = 0; + int nHeight = 0; + +private: + wchar_t *m_Glyphs = nullptr; + short *m_Colours = nullptr; + + void Create(int w, int h) + { + nWidth = w; + nHeight = h; + m_Glyphs = new wchar_t[w*h]; + m_Colours = new short[w*h]; + for (int i = 0; i < w*h; i++) + { + m_Glyphs[i] = L' '; + m_Colours[i] = FG_BLACK; + } + } + +public: + void SetGlyph(int x, int y, wchar_t c) + { + if (x <0 || x > nWidth || y < 0 || y > nHeight) + return; + else + m_Glyphs[y * nWidth + x] = c; + } + + void SetColour(int x, int y, short c) + { + if (x <0 || x > nWidth || y < 0 || y > nHeight) + return; + else + m_Colours[y * nWidth + x] = c; + } + + wchar_t GetGlyph(int x, int y) + { + if (x <0 || x > nWidth || y < 0 || y > nHeight) + return L' '; + else + return m_Glyphs[y * nWidth + x]; + } + + short GetColour(int x, int y) + { + if (x <0 || x > nWidth || y < 0 || y > nHeight) + return FG_BLACK; + else + return m_Colours[y * nWidth + x]; + } + + wchar_t SampleGlyph(float x, float y) + { + int sx = (int)(x * (float)nWidth); + int sy = (int)(y * (float)nHeight-1.0f); + if (sx <0 || sx > nWidth || sy < 0 || sy > nHeight) + return L' '; + else + return m_Glyphs[sy * nWidth + sx]; + } + + short SampleColour(float x, float y) + { + int sx = (int)(x * (float)nWidth); + int sy = (int)(y * (float)nHeight-1.0f); + if (sx <0 || sx > nWidth || sy < 0 || sy > nHeight) + return FG_BLACK; + else + return m_Colours[sy * nWidth + sx]; + } + + bool Save(wstring sFile) + { + FILE *f = nullptr; + _wfopen_s(&f, sFile.c_str(), L"wb"); + if (f == nullptr) + return false; + + fwrite(&nWidth, sizeof(int), 1, f); + fwrite(&nHeight, sizeof(int), 1, f); + fwrite(m_Colours, sizeof(short), nWidth * nHeight, f); + fwrite(m_Glyphs, sizeof(wchar_t), nWidth * nHeight, f); + + fclose(f); + + return true; + } + + bool Load(wstring sFile) + { + delete[] m_Glyphs; + delete[] m_Colours; + nWidth = 0; + nHeight = 0; + + FILE *f = nullptr; + _wfopen_s(&f, sFile.c_str(), L"rb"); + if (f == nullptr) + return false; + + fread(&nWidth, sizeof(int), 1, f); + fread(&nHeight, sizeof(int), 1, f); + + Create(nWidth, nHeight); + + fread(m_Colours, sizeof(short), nWidth * nHeight, f); + fread(m_Glyphs, sizeof(wchar_t), nWidth * nHeight, f); + + fclose(f); + return true; + } + +}; + + +class olcConsoleGameEngine +{ +public: + olcConsoleGameEngine() + { + m_nScreenWidth = 80; + m_nScreenHeight = 30; + + m_hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + m_hConsoleIn = GetStdHandle(STD_INPUT_HANDLE); + + m_keyNewState = new short[256]; + m_keyOldState = new short[256]; + memset(m_keyNewState, 0, 256 * sizeof(short)); + memset(m_keyOldState, 0, 256 * sizeof(short)); + memset(m_keys, 0, 256 * sizeof(sKeyState)); + + m_mousePosX = 0; + m_mousePosY = 0; + + m_sAppName = L"Default"; + } + + int ConstructConsole(int width, int height, int fontw, int fonth) + { + if (m_hConsole == INVALID_HANDLE_VALUE) + return Error(L"Bad Handle"); + + m_nScreenWidth = width; + m_nScreenHeight = height; + + // Update 13/09/2017 - It seems that the console behaves differently on some systems + // and I'm unsure why this is. It could be to do with windows default settings, or + // screen resolutions, or system languages. Unfortunately, MSDN does not offer much + // by way of useful information, and so the resulting sequence is the reult of experiment + // that seems to work in multiple cases. + // + // The problem seems to be that the SetConsoleXXX functions are somewhat circular and + // fail depending on the state of the current console properties, i.e. you can't set + // the buffer size until you set the screen size, but you can't change the screen size + // until the buffer size is correct. This coupled with a precise ordering of calls + // makes this procedure seem a little mystical :-P. Thanks to wowLinh for helping - Jx9 + + // Change console visual size to a minimum so ScreenBuffer can shrink + // below the actual visual size + m_rectWindow = { 0, 0, 1, 1 }; + SetConsoleWindowInfo(m_hConsole, TRUE, &m_rectWindow); + + // Set the size of the screen buffer + COORD coord = { (short)m_nScreenWidth, (short)m_nScreenHeight }; + if (!SetConsoleScreenBufferSize(m_hConsole, coord)) + Error(L"SetConsoleScreenBufferSize"); + + // Assign screen buffer to the console + if (!SetConsoleActiveScreenBuffer(m_hConsole)) + return Error(L"SetConsoleActiveScreenBuffer"); + + // Set the font size now that the screen buffer has been assigned to the console + CONSOLE_FONT_INFOEX cfi; + cfi.cbSize = sizeof(cfi); + cfi.nFont = 0; + cfi.dwFontSize.X = fontw; + cfi.dwFontSize.Y = fonth; + cfi.FontFamily = FF_DONTCARE; + cfi.FontWeight = FW_NORMAL; + wcscpy_s(cfi.FaceName, L"Consolas"); + if (!SetCurrentConsoleFontEx(m_hConsole, false, &cfi)) + return Error(L"SetCurrentConsoleFontEx"); + + // Get screen buffer info and check the maximum allowed window size. Return + // error if exceeded, so user knows their dimensions/fontsize are too large + CONSOLE_SCREEN_BUFFER_INFO csbi; + if (!GetConsoleScreenBufferInfo(m_hConsole, &csbi)) + return Error(L"GetConsoleScreenBufferInfo"); + if (m_nScreenHeight > csbi.dwMaximumWindowSize.Y) + return Error(L"Screen Height / Font Height Too Big"); + if (m_nScreenWidth > csbi.dwMaximumWindowSize.X) + return Error(L"Screen Width / Font Width Too Big"); + + // Set Physical Console Window Size + m_rectWindow = { 0, 0, (short)m_nScreenWidth - 1, (short)m_nScreenHeight - 1 }; + if (!SetConsoleWindowInfo(m_hConsole, TRUE, &m_rectWindow)) + return Error(L"SetConsoleWindowInfo"); + + // Set flags to allow mouse input + if (!SetConsoleMode(m_hConsoleIn, ENABLE_EXTENDED_FLAGS | ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT)) + return Error(L"SetConsoleMode"); + + // Allocate memory for screen buffer + m_bufScreen = new CHAR_INFO[m_nScreenWidth*m_nScreenHeight]; + memset(m_bufScreen, 0, sizeof(CHAR_INFO) * m_nScreenWidth * m_nScreenHeight); + + return 1; + } + + virtual void Draw(int x, int y, wchar_t c = 0x2588, short col = 0x000F) + { + if (x >= 0 && x < m_nScreenWidth && y >= 0 && y < m_nScreenHeight) + { + m_bufScreen[y * m_nScreenWidth + x].Char.UnicodeChar = c; + m_bufScreen[y * m_nScreenWidth + x].Attributes = col; + } + } + + void Fill(int x1, int y1, int x2, int y2, wchar_t c = 0x2588, short col = 0x000F) + { + Clip(x1, y1); + Clip(x2, y2); + for (int x = x1; x < x2; x++) + for (int y = y1; y < y2; y++) + Draw(x, y, c, col); + } + + void DrawString(int x, int y, wstring c, short col = 0x000F) + { + for (size_t i = 0; i < c.size(); i++) + { + m_bufScreen[y * m_nScreenWidth + x + i].Char.UnicodeChar = c[i]; + m_bufScreen[y * m_nScreenWidth + x + i].Attributes = col; + } + } + + void DrawStringAlpha(int x, int y, wstring c, short col = 0x000F) + { + for (size_t i = 0; i < c.size(); i++) + { + if (c[i] != L' ') + { + m_bufScreen[y * m_nScreenWidth + x + i].Char.UnicodeChar = c[i]; + m_bufScreen[y * m_nScreenWidth + x + i].Attributes = col; + } + } + } + + void Clip(int &x, int &y) + { + if (x < 0) x = 0; + if (x >= m_nScreenWidth) x = m_nScreenWidth; + if (y < 0) y = 0; + if (y >= m_nScreenHeight) y = m_nScreenHeight; + } + + void DrawLine(int x1, int y1, int x2, int y2, wchar_t c = 0x2588, short col = 0x000F) + { + int x, y, dx, dy, dx1, dy1, px, py, xe, ye, i; + dx = x2 - x1; + dy = y2 - y1; + dx1 = abs(dx); + dy1 = abs(dy); + px = 2 * dy1 - dx1; + py = 2 * dx1 - dy1; + if (dy1 <= dx1) + { + if (dx >= 0) + { + x = x1; + y = y1; + xe = x2; + } + else + { + x = x2; + y = y2; + xe = x1; + } + Draw(x, y, c, col); + for (i = 0; x0 && dy>0)) + y = y + 1; + else + y = y - 1; + px = px + 2 * (dy1 - dx1); + } + Draw(x, y, c, col); + } + } + else + { + if (dy >= 0) + { + x = x1; + y = y1; + ye = y2; + } + else + { + x = x2; + y = y2; + ye = y1; + } + Draw(x, y, c, col); + for (i = 0; y0 && dy>0)) + x = x + 1; + else + x = x - 1; + py = py + 2 * (dx1 - dy1); + } + Draw(x, y, c, col); + } + } + } + + void DrawSprite(int x, int y, olcSprite *sprite) + { + if (sprite == nullptr) + return; + + for (int i = 0; i < sprite->nWidth; i++) + { + for (int j = 0; j < sprite->nHeight; j++) + { + if (sprite->GetGlyph(i, j) != L' ') + Draw(x + i, y + j, sprite->GetGlyph(i, j), sprite->GetColour(i, j)); + } + } + } + + void DrawPartialSprite(int x, int y, olcSprite *sprite, int ox, int oy, int w, int h) + { + if (sprite == nullptr) + return; + + for (int i = 0; i < w; i++) + { + for (int j = 0; j < h; j++) + { + if (sprite->GetGlyph(i+ox, j+oy) != L' ') + Draw(x + i, y + j, sprite->GetGlyph(i+ox, j+oy), sprite->GetColour(i+ox, j+oy)); + } + } + } + + void DrawWireFrameModel(const vector> &vecModelCoordinates, float x, float y, float r = 0.0f, float s = 1.0f, short col = FG_WHITE) + { + // pair.first = x coordinate + // pair.second = y coordinate + + // Create translated model vector of coordinate pairs + vector> vecTransformedCoordinates; + int verts = vecModelCoordinates.size(); + vecTransformedCoordinates.resize(verts); + + // Rotate + for (int i = 0; i < verts; i++) + { + vecTransformedCoordinates[i].first = vecModelCoordinates[i].first * cosf(r) - vecModelCoordinates[i].second * sinf(r); + vecTransformedCoordinates[i].second = vecModelCoordinates[i].first * sinf(r) + vecModelCoordinates[i].second * cosf(r); + } + + // Scale + for (int i = 0; i < verts; i++) + { + vecTransformedCoordinates[i].first = vecTransformedCoordinates[i].first * s; + vecTransformedCoordinates[i].second = vecTransformedCoordinates[i].second * s; + } + + // Translate + for (int i = 0; i < verts; i++) + { + vecTransformedCoordinates[i].first = vecTransformedCoordinates[i].first + x; + vecTransformedCoordinates[i].second = vecTransformedCoordinates[i].second + y; + } + + // Draw Closed Polygon + for (int i = 0; i < verts + 1; i++) + { + int j = (i + 1); + DrawLine((int)vecTransformedCoordinates[i % verts].first, (int)vecTransformedCoordinates[i % verts].second, + (int)vecTransformedCoordinates[j % verts].first, (int)vecTransformedCoordinates[j % verts].second, PIXEL_SOLID, col); + } + } + + ~olcConsoleGameEngine() + { + SetConsoleActiveScreenBuffer(m_hOriginalConsole); + delete[] m_bufScreen; + } + +public: + void Start() + { + m_bAtomActive = true; + + // Star the thread + thread t = thread(&olcConsoleGameEngine::GameThread, this); + + // Wait for thread to be exited + unique_lock ul(m_muxGame); + m_cvGameFinished.wait(ul); + + // Tidy up + t.join(); + } + + int ScreenWidth() + { + return m_nScreenWidth; + } + + int ScreenHeight() + { + return m_nScreenHeight; + } + +private: + void GameThread() + { + // Create user resources as part of this thread + if (!OnUserCreate()) + m_bAtomActive = false; + + auto tp1 = chrono::system_clock::now(); + auto tp2 = chrono::system_clock::now(); + + // Run as fast as possible + while (m_bAtomActive) + { + // Handle Timing + tp2 = chrono::system_clock::now(); + chrono::duration elapsedTime = tp2 - tp1; + tp1 = tp2; + float fElapsedTime = elapsedTime.count(); + + // Handle Keyboard Input + for (int i = 0; i < 256; i++) + { + m_keyNewState[i] = GetAsyncKeyState(i); + + m_keys[i].bPressed = false; + m_keys[i].bReleased = false; + + if (m_keyNewState[i] != m_keyOldState[i]) + { + if (m_keyNewState[i] & 0x8000) + { + m_keys[i].bPressed = !m_keys[i].bHeld; + m_keys[i].bHeld = true; + } + else + { + m_keys[i].bReleased = true; + m_keys[i].bHeld = false; + } + } + + m_keyOldState[i] = m_keyNewState[i]; + } + + // Handle Mouse Input - Check for window events + INPUT_RECORD inBuf[32]; + DWORD events = 0; + GetNumberOfConsoleInputEvents(m_hConsoleIn, &events); + if(events > 0) + ReadConsoleInput(m_hConsoleIn, inBuf, events, &events); + + // Handle events - we only care about mouse clicks and movement + // for now + for (DWORD i = 0; i < events; i++) + { + switch (inBuf[i].EventType) + { + case MOUSE_EVENT: + { + switch (inBuf[i].Event.MouseEvent.dwEventFlags) + { + case MOUSE_MOVED: + { + m_mousePosX = inBuf[i].Event.MouseEvent.dwMousePosition.X; + m_mousePosY = inBuf[i].Event.MouseEvent.dwMousePosition.Y; + } + break; + + case 0: + { + for (int m = 0; m < 5; m++) + m_mouseNewState[m] = (inBuf[i].Event.MouseEvent.dwButtonState & (1 << m)) > 0; + + } + break; + + default: + break; + } + } + break; + + default: + break; + // We don't care just at the moment + } + } + + for (int m = 0; m < 5; m++) + { + m_mouse[m].bPressed = false; + m_mouse[m].bReleased = false; + + if (m_mouseNewState[m] != m_mouseOldState[m]) + { + if (m_mouseNewState[m]) + { + m_mouse[m].bPressed = true; + m_mouse[m].bHeld = true; + } + else + { + m_mouse[m].bReleased = true; + m_mouse[m].bHeld = false; + } + } + + m_mouseOldState[m] = m_mouseNewState[m]; + } + + + // Handle Frame Update + if (!OnUserUpdate(fElapsedTime)) + m_bAtomActive = false; + + // Update Title & Present Screen Buffer + wchar_t s[256]; + swprintf_s(s, 256, L"OneLoneCoder.com - Console Game Engine - %s - FPS: %3.2f - %d ", m_sAppName.c_str(), 1.0f / fElapsedTime, events); + SetConsoleTitle(s); + WriteConsoleOutput(m_hConsole, m_bufScreen, { (short)m_nScreenWidth, (short)m_nScreenHeight }, { 0,0 }, &m_rectWindow); + } + + m_cvGameFinished.notify_one(); + } + +public: + // User MUST OVERRIDE THESE!! + virtual bool OnUserCreate() = 0; + virtual bool OnUserUpdate(float fElapsedTime) = 0; + + +protected: + int m_nScreenWidth; + int m_nScreenHeight; + CHAR_INFO *m_bufScreen; + atomic m_bAtomActive; + condition_variable m_cvGameFinished; + mutex m_muxGame; + wstring m_sAppName; + + struct sKeyState + { + bool bPressed; + bool bReleased; + bool bHeld; + } m_keys[256], m_mouse[5]; + + int m_mousePosX; + int m_mousePosY; + + +protected: + int Error(const wchar_t *msg) + { + wchar_t buf[256]; + FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), buf, 256, NULL); + SetConsoleActiveScreenBuffer(m_hOriginalConsole); + wprintf(L"ERROR: %s\n\t%s\n", msg, buf); + return -1; + } + +private: + HANDLE m_hOriginalConsole; + CONSOLE_SCREEN_BUFFER_INFO m_OriginalConsoleInfo; + HANDLE m_hConsole; + HANDLE m_hConsoleIn; + SMALL_RECT m_rectWindow; + short *m_keyOldState; + short *m_keyNewState; + bool m_mouseOldState[5]; + bool m_mouseNewState[5]; +}; \ No newline at end of file