From 893b8ad68e9e3254d08e723c0bf8aebaf5a146b9 Mon Sep 17 00:00:00 2001 From: Javidx9 Date: Sun, 27 May 2018 17:50:09 +0100 Subject: [PATCH] Added Racing Lines Video --- OneLoneCoder_RacingLines.cpp | 432 +++++++++++++++++++++++++++++++++++ olcConsoleGameEngine.h | 249 ++++++++++++++++---- 2 files changed, 639 insertions(+), 42 deletions(-) create mode 100644 OneLoneCoder_RacingLines.cpp diff --git a/OneLoneCoder_RacingLines.cpp b/OneLoneCoder_RacingLines.cpp new file mode 100644 index 0000000..2bd37fb --- /dev/null +++ b/OneLoneCoder_RacingLines.cpp @@ -0,0 +1,432 @@ +/* +OneLoneCoder.com - Programming Racing Lines +"Brake! Brake! Hard Left! " - @Javidx9 + +License +~~~~~~~ +One Lone Coder Console Game Engine Copyright (C) 2018 Javidx9 +This program comes with ABSOLUTELY NO WARRANTY. +This is free software, and you are welcome to redistribute it +under certain conditions; See license for details. +Original works located at: +https://www.github.com/onelonecoder +https://www.onelonecoder.com +https://www.youtube.com/javidx9 +GNU GPLv3 +https://github.com/OneLoneCoder/videos/blob/master/LICENSE + +From Javidx9 :) +~~~~~~~~~~~~~~~ +Hello! Ultimately 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. You acknowledge +that I am not responsible for anything bad that happens as a result of +your actions. However this code is protected by GNU GPLv3, see the license in the +github repo. This means you must attribute me if you use it. You can view this +license here: https://github.com/OneLoneCoder/videos/blob/master/LICENSE +Cheers! + +Background +~~~~~~~~~~ +Algorithmically generating a racing line is quite tricky. This simple framework +allows me to explore different methods. Use mouse to drag points, and A & S keys +to change the number of iterations. + +Author +~~~~~~ +Twitter: @javidx9 +Blog: http://www.onelonecoder.com +Discord: https://discord.gg/WhwHUMV + +Video: +~~~~~~ +https://youtu.be/FlieT66N9OM + +Last Updated: 27/05/2018 +*/ + +#include +#include +using namespace std; + +#include "olcConsoleGameEngine.h" + +// See Programming Splines! Videos +struct sPoint2D +{ + float x; + float y; + float length; +}; + +struct sSpline +{ + vector points; + float fTotalSplineLength = 0.0f; + bool bIsLooped = true; + + sPoint2D GetSplinePoint(float t) + { + int p0, p1, p2, p3; + if (!bIsLooped) + { + p1 = (int)t + 1; + p2 = p1 + 1; + p3 = p2 + 1; + p0 = p1 - 1; + } + else + { + p1 = ((int)t) % points.size(); + p2 = (p1 + 1) % points.size(); + p3 = (p2 + 1) % points.size(); + p0 = p1 >= 1 ? p1 - 1 : points.size() - 1; + } + + t = t - (int)t; + + float tt = t * t; + float ttt = tt * t; + + float q1 = -ttt + 2.0f*tt - t; + float q2 = 3.0f*ttt - 5.0f*tt + 2.0f; + float q3 = -3.0f*ttt + 4.0f*tt + t; + float q4 = ttt - tt; + + float tx = 0.5f * (points[p0].x * q1 + points[p1].x * q2 + points[p2].x * q3 + points[p3].x * q4); + float ty = 0.5f * (points[p0].y * q1 + points[p1].y * q2 + points[p2].y * q3 + points[p3].y * q4); + + return{ tx, ty }; + } + + sPoint2D GetSplineGradient(float t) + { + int p0, p1, p2, p3; + if (!bIsLooped) + { + p1 = (int)t + 1; + p2 = p1 + 1; + p3 = p2 + 1; + p0 = p1 - 1; + } + else + { + p1 = ((int)t) % points.size(); + p2 = (p1 + 1) % points.size(); + p3 = (p2 + 1) % points.size(); + p0 = p1 >= 1 ? p1 - 1 : points.size() - 1; + } + + t = t - (int)t; + + float tt = t * t; + float ttt = tt * t; + + float q1 = -3.0f * tt + 4.0f*t - 1.0f; + float q2 = 9.0f*tt - 10.0f*t; + float q3 = -9.0f*tt + 8.0f*t + 1.0f; + float q4 = 3.0f*tt - 2.0f*t; + + float tx = 0.5f * (points[p0].x * q1 + points[p1].x * q2 + points[p2].x * q3 + points[p3].x * q4); + float ty = 0.5f * (points[p0].y * q1 + points[p1].y * q2 + points[p2].y * q3 + points[p3].y * q4); + + return{ tx, ty }; + } + + float CalculateSegmentLength(int node) + { + float fLength = 0.0f; + float fStepSize = 0.1; + + sPoint2D old_point, new_point; + old_point = GetSplinePoint((float)node); + + for (float t = 0; t < 1.0f; t += fStepSize) + { + new_point = GetSplinePoint((float)node + t); + fLength += sqrtf((new_point.x - old_point.x)*(new_point.x - old_point.x) + + (new_point.y - old_point.y)*(new_point.y - old_point.y)); + old_point = new_point; + } + + return fLength; + } + + + float GetNormalisedOffset(float p) + { + // Which node is the base? + int i = 0; + while (p > points[i].length) + { + p -= points[i].length; + i++; + } + + // The fractional is the offset + return (float)i + (p / points[i].length); + } + + + void UpdateSplineProperties() + { + // Use to cache local spline lengths and overall spline length + fTotalSplineLength = 0.0f; + + if (bIsLooped) + { + // Each node has a succeeding length + for (int i = 0; i < points.size(); i++) + { + points[i].length = CalculateSegmentLength(i); + fTotalSplineLength += points[i].length; + } + } + else + { + for (int i = 1; i < points.size() - 2; i++) + { + points[i].length = CalculateSegmentLength(i); + fTotalSplineLength += points[i].length; + } + } + } + + void DrawSelf(olcConsoleGameEngine* gfx, float ox, float oy, wchar_t c = 0x2588, short col = 0x000F) + { + if (bIsLooped) + { + for (float t = 0; t < (float)points.size() - 0; t += 0.005f) + { + sPoint2D pos = GetSplinePoint(t); + gfx->Draw(pos.x, pos.y, c, col); + } + } + else // Not Looped + { + for (float t = 0; t < (float)points.size() - 3; t += 0.005f) + { + sPoint2D pos = GetSplinePoint(t); + gfx->Draw(pos.x, pos.y, c, col); + } + } + } + +}; + + + +//////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////// + +class OneLoneCoder_RacingLine : public olcConsoleGameEngine +{ +public: + OneLoneCoder_RacingLine() + { + m_sAppName = L"Racing Line"; + } + +private: + sSpline path, trackLeft, trackRight, racingLine; // Various splines + + int nNodes = 20; // Number of nodes in spline + + float fDisplacement[20]; // Displacement along spline node normal + + int nIterations = 1; + float fMarker = 1.0f; + int nSelectedNode = -1; + + vector> vecModelCar; + +protected: + // Called by olcConsoleGameEngine + virtual bool OnUserCreate() + { + for (int i = 0; i < nNodes; i++) + { + //path.points.push_back( + // { 30.0f * sinf((float)i / (float)nNodes * 3.14159f * 2.0f) + ScreenWidth() / 2, + // 30.0f * cosf((float)i / (float)nNodes * 3.14159f * 2.0f) + ScreenHeight() / 2 }); + + // Could use allocation functions for thes now, but just size via + // append + trackLeft.points.push_back({ 0.0f, 0.0f }); + trackRight.points.push_back({ 0.0f, 0.0f }); + racingLine.points.push_back({ 0.0f, 0.0f }); + } + + // A hand crafted track + path.points = { { 81.8f, 196.0f }, { 108.0f,210.0f }, { 152.0f,216.0f }, + { 182.0f,185.6f }, { 190.0f,159.0f }, { 198.0f,122.0f }, { 226.0f,93.0f }, + { 224.0f,41.0f }, { 204.0f,15.0f }, { 158.0f,24.0f }, { 146.0f,52.0f }, + { 157.0f,93.0f }, { 124.0f,129.0f }, { 83.0f,104.0f }, { 77.0f,62.0f }, + { 40.0f,57.0f }, { 21.0f,83.0f }, { 33.0f,145.0f }, { 30.0f,198.0f }, + { 48.0f,210.0f } }; + + vecModelCar = { { 2,0 },{ 0,-1 },{ 0,1 } }; + + path.UpdateSplineProperties(); + return true; + } + + // Called by olcConsoleGameEngine + virtual bool OnUserUpdate(float fElapsedTime) + { + // Clear Screen + Fill(0, 0, ScreenWidth(), ScreenHeight(), PIXEL_SOLID, FG_DARK_GREEN); + + // Handle iteration count + if (m_keys[L'A'].bHeld) nIterations++; + if (m_keys[L'S'].bHeld) nIterations--; + if (nIterations < 0) nIterations = 0; + + + // Check if node is selected with mouse + if (GetMouse(0).bPressed) + { + for (int i = 0; i < path.points.size(); i++) + { + float d = sqrtf(powf(path.points[i].x - GetMouseX(), 2) + powf(path.points[i].y - GetMouseY(), 2)); + if (d < 5.0f) + { + nSelectedNode = i; + break; + } + } + } + + if (GetMouse(0).bReleased) + nSelectedNode = -1; + + // Move selected node + if (GetMouse(0).bHeld && nSelectedNode >= 0) + { + path.points[nSelectedNode].x = GetMouseX(); + path.points[nSelectedNode].y = GetMouseY(); + path.UpdateSplineProperties(); + } + + // Move car around racing line + fMarker += 2.0f * fElapsedTime; + if (fMarker >= (float)racingLine.fTotalSplineLength) + fMarker -= (float)racingLine.fTotalSplineLength; + + // Calculate track boundary points + float fTrackWidth = 10.0f; + for (int i = 0; i < path.points.size(); i++) + { + sPoint2D p1 = path.GetSplinePoint(i); + sPoint2D g1 = path.GetSplineGradient(i); + float glen = sqrtf(g1.x*g1.x + g1.y*g1.y); + + trackLeft.points[i].x = p1.x + fTrackWidth * (-g1.y / glen); + trackLeft.points[i].y = p1.y + fTrackWidth * ( g1.x / glen); + trackRight.points[i].x = p1.x - fTrackWidth * (-g1.y / glen); + trackRight.points[i].y = p1.y - fTrackWidth * (g1.x / glen); + } + + // Draw Track + float fRes = 0.2f; + for (float t = 0.0f; t < path.points.size(); t += fRes) + { + sPoint2D pl1 = trackLeft.GetSplinePoint(t); + sPoint2D pr1 = trackRight.GetSplinePoint(t); + sPoint2D pl2 = trackLeft.GetSplinePoint(t + fRes); + sPoint2D pr2 = trackRight.GetSplinePoint(t + fRes); + + FillTriangle(pl1.x, pl1.y, pr1.x, pr1.y, pr2.x, pr2.y, PIXEL_SOLID, FG_GREY); + FillTriangle(pl1.x, pl1.y, pl2.x, pl2.y, pr2.x, pr2.y, PIXEL_SOLID, FG_GREY); + } + + // Reset racing line + for (int i = 0; i < racingLine.points.size(); i++) + { + racingLine.points[i] = path.points[i]; + fDisplacement[i] = 0; + } + racingLine.UpdateSplineProperties(); + + for (int n = 0; n < nIterations; n++) + { + for (int i = 0; i < racingLine.points.size(); i++) + { + // Get locations of neighbour nodes + sPoint2D pointRight = racingLine.points[(i + 1) % racingLine.points.size()]; + sPoint2D pointLeft = racingLine.points[(i + racingLine.points.size() - 1) % racingLine.points.size()]; + sPoint2D pointMiddle = racingLine.points[i]; + + // Create vectors to neighbours + sPoint2D vectorLeft = { pointLeft.x - pointMiddle.x, pointLeft.y - pointMiddle.y }; + sPoint2D vectorRight = { pointRight.x - pointMiddle.x, pointRight.y - pointMiddle.y }; + + // Normalise neighbours + float lengthLeft = sqrtf(vectorLeft.x*vectorLeft.x + vectorLeft.y*vectorLeft.y); + sPoint2D leftn = { vectorLeft.x / lengthLeft, vectorLeft.y / lengthLeft }; + float lengthRight = sqrtf(vectorRight.x*vectorRight.x + vectorRight.y*vectorRight.y); + sPoint2D rightn = { vectorRight.x / lengthRight, vectorRight.y / lengthRight }; + + // Add together to create bisector vector + sPoint2D vectorSum = { rightn.x + leftn.x, rightn.y + leftn.y }; + float len = sqrtf(vectorSum.x*vectorSum.x + vectorSum.y*vectorSum.y); + vectorSum.x /= len; vectorSum.y /= len; + + // Get point gradient and normalise + sPoint2D g = path.GetSplineGradient(i); + float glen = sqrtf(g.x*g.x + g.y*g.y); + g.x /= glen; g.y /= glen; + + // Project required correction onto point tangent to give displacment + float dp = -g.y*vectorSum.x + g.x * vectorSum.y; + + // Shortest path + fDisplacement[i] += (dp * 0.3f); + + // Curvature + //fDisplacement[(i + 1) % racingLine.points.size()] += dp * -0.2f; + //fDisplacement[(i - 1 + racingLine.points.size()) % racingLine.points.size()] += dp * -0.2f; + + } + + // Clamp displaced points to track width + for (int i = 0; i < racingLine.points.size(); i++) + { + if (fDisplacement[i] >= fTrackWidth) fDisplacement[i] = fTrackWidth; + if (fDisplacement[i] <= -fTrackWidth) fDisplacement[i] = -fTrackWidth; + + sPoint2D g = path.GetSplineGradient(i); + float glen = sqrtf(g.x*g.x + g.y*g.y); + g.x /= glen; g.y /= glen; + + racingLine.points[i].x = path.points[i].x + -g.y * fDisplacement[i]; + racingLine.points[i].y = path.points[i].y + g.x * fDisplacement[i]; + } + } + + path.DrawSelf(this, 0, 0); + //trackLeft.DrawSelf(this, 0, 0); + //trackRight.DrawSelf(this, 0, 0); + + racingLine.UpdateSplineProperties(); + racingLine.DrawSelf(this, 0, 0, PIXEL_SOLID, FG_BLUE); + + for (auto i : path.points) + Fill(i.x - 1, i.y - 1, i.x + 2, i.y + 2, PIXEL_SOLID, FG_RED); + + sPoint2D car_p = racingLine.GetSplinePoint(fMarker); + sPoint2D car_g = racingLine.GetSplineGradient(fMarker); + DrawWireFrameModel(vecModelCar, car_p.x, car_p.y, atan2f(car_g.y, car_g.x), 4.0f, FG_BLACK); + + return true; + } +}; + + +int main() +{ + OneLoneCoder_RacingLine demo; + demo.ConstructConsole(256, 240, 4, 4); + demo.Start(); + return 0; +} \ No newline at end of file diff --git a/olcConsoleGameEngine.h b/olcConsoleGameEngine.h index 4c9de7c..7c1936e 100644 --- a/olcConsoleGameEngine.h +++ b/olcConsoleGameEngine.h @@ -51,9 +51,9 @@ Beginners Guide: https://youtu.be/u5BhrA8ED0o Shout Outs! ~~~~~~~~~~~ Thanks to cool people who helped with testing, bug-finding and fixing! -wowLinh, JavaJack59, idkwid, kingtatgi, Return Null, CPP Guy +wowLinh, JavaJack59, idkwid, kingtatgi, Return Null, CPP Guy, MaGetzUb -Last Updated: 18/03/2018 +Last Updated: 27/05/2018 Usage: ~~~~~~ @@ -370,13 +370,13 @@ public: cfi.FontFamily = FF_DONTCARE; cfi.FontWeight = FW_NORMAL; - /*DWORD version = GetVersion(); + /* DWORD version = GetVersion(); DWORD major = (DWORD)(LOBYTE(LOWORD(version))); - DWORD minor = (DWORD)(HIBYTE(LOWORD(version))); + DWORD minor = (DWORD)(HIBYTE(LOWORD(version)));*/ - if ((major > 6) || ((major == 6) && (minor >= 2) && (minor < 4))) - wcscpy_s(cfi.FaceName, L"Raster"); // Windows 8 :( - else*/ + //if ((major > 6) || ((major == 6) && (minor >= 2) && (minor < 4))) + // wcscpy_s(cfi.FaceName, L"Raster"); // Windows 8 :( + //else wcscpy_s(cfi.FaceName, L"Lucida Console"); // Everything else :P //wcscpy_s(cfi.FaceName, L"Liberation Mono"); @@ -461,27 +461,18 @@ public: 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; + 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; - } + { x = x1; y = y1; xe = x2; } else - { - x = x2; - y = y2; - xe = x1; - } + { x = x2; y = y2; xe = x1;} + Draw(x, y, c, col); + for (i = 0; x0 && dy>0)) - y = y + 1; - else - y = y - 1; + if ((dx<0 && dy<0) || (dx>0 && dy>0)) y = y + 1; else y = y - 1; px = px + 2 * (dy1 - dx1); } Draw(x, y, c, col); @@ -501,18 +489,12 @@ public: else { if (dy >= 0) - { - x = x1; - y = y1; - ye = y2; - } + { x = x1; y = y1; ye = y2; } else - { - x = x2; - y = y2; - ye = y1; - } + { x = x2; y = y2; ye = y1; } + Draw(x, y, c, col); + for (i = 0; y0 && dy>0)) - x = x + 1; - else - x = x - 1; + if ((dx<0 && dy<0) || (dx>0 && dy>0)) x = x + 1; else x = x - 1; py = py + 2 * (dx1 - dy1); } Draw(x, y, c, col); @@ -531,6 +510,192 @@ public: } } + void DrawTriangle(int x1, int y1, int x2, int y2, int x3, int y3, wchar_t c = 0x2588, short col = 0x000F) + { + DrawLine(x1, y1, x2, y2, c, col); + DrawLine(x2, y2, x3, y3, c, col); + DrawLine(x3, y3, x1, y1, c, col); + } + + // https://www.avrfreaks.net/sites/default/files/triangles.c + void FillTriangle(int x1, int y1, int x2, int y2, int x3, int y3, wchar_t c = 0x2588, short col = 0x000F) + { + auto SWAP = [](int &x, int &y) { int t = x; x = y; y = t; }; + auto drawline = [&](int sx, int ex, int ny) { for (int i = sx; i <= ex; i++) Draw(i, ny, c, col); }; + + int t1x, t2x, y, minx, maxx, t1xp, t2xp; + bool changed1 = false; + bool changed2 = false; + int signx1, signx2, dx1, dy1, dx2, dy2; + int e1, e2; + // Sort vertices + if (y1>y2) { SWAP(y1, y2); SWAP(x1, x2); } + if (y1>y3) { SWAP(y1, y3); SWAP(x1, x3); } + if (y2>y3) { SWAP(y2, y3); SWAP(x2, x3); } + + t1x = t2x = x1; y = y1; // Starting points + dx1 = (int)(x2 - x1); if (dx1<0) { dx1 = -dx1; signx1 = -1; } + else signx1 = 1; + dy1 = (int)(y2 - y1); + + dx2 = (int)(x3 - x1); if (dx2<0) { dx2 = -dx2; signx2 = -1; } + else signx2 = 1; + dy2 = (int)(y3 - y1); + + if (dy1 > dx1) { // swap values + SWAP(dx1, dy1); + changed1 = true; + } + if (dy2 > dx2) { // swap values + SWAP(dy2, dx2); + changed2 = true; + } + + e2 = (int)(dx2 >> 1); + // Flat top, just process the second half + if (y1 == y2) goto next; + e1 = (int)(dx1 >> 1); + + for (int i = 0; i < dx1;) { + t1xp = 0; t2xp = 0; + if (t1x= dx1) { + e1 -= dx1; + if (changed1) t1xp = signx1;//t1x += signx1; + else goto next1; + } + if (changed1) break; + else t1x += signx1; + } + // Move line + next1: + // process second line until y value is about to change + while (1) { + e2 += dy2; + while (e2 >= dx2) { + e2 -= dx2; + if (changed2) t2xp = signx2;//t2x += signx2; + else goto next2; + } + if (changed2) break; + else t2x += signx2; + } + next2: + if (minx>t1x) minx = t1x; if (minx>t2x) minx = t2x; + if (maxx dx1) { // swap values + SWAP(dy1, dx1); + changed1 = true; + } + else changed1 = false; + + e1 = (int)(dx1 >> 1); + + for (int i = 0; i <= dx1; i++) { + t1xp = 0; t2xp = 0; + if (t1x= dx1) { + e1 -= dx1; + if (changed1) { t1xp = signx1; break; }//t1x += signx1; + else goto next3; + } + if (changed1) break; + else t1x += signx1; + if (i= dx2) { + e2 -= dx2; + if (changed2) t2xp = signx2; + else goto next4; + } + if (changed2) break; + else t2x += signx2; + } + next4: + + if (minx>t1x) minx = t1x; if (minx>t2x) minx = t2x; + if (maxxy3) return; + } + } + + // Non-bresenham method + /*void FillTriangle(int x1, int y1, int x2, int y2, int x3, int y3, wchar_t c = 0x2588, short col = 0x000F) + { + if ((x1 == x2 && x1 == x3) || (y1 == y2) && (y1 == y3)) + return; + + int tmp; + if (y2 < y1){ tmp = y1; y1 = y2; y2 = tmp; tmp = x1; x1 = x2; x2 = tmp; } + if (y3 < y1){ tmp = y1; y1 = y3; y3 = tmp; tmp = x1; x1 = x3; x3 = tmp; } + if (y3 < y2){ tmp = y2; y2 = y3; y3 = tmp; tmp = x2; x2 = x3; x3 = tmp; } + + int dy1 = y2 - y1, dx1 = x2 - x1; + int dy2 = y3 - y1, dx2 = x3 - x1; + + if (dy1) + { + for (int i = y1; i < y2; i++) + { + int ax = x1 + ((i - y1)*dx1) / dy1; + int bx = x1 + ((i - y1)*dx2) / dy2; + tmp = ax; + if (ax > bx) ax = bx, bx = tmp; + for (int j = ax; j < bx; j++) Draw(j, i, c, col); + } + } + + dy1 = y3 - y2; dx1 = x3 - x2; + + if (dy1) + { + for (int i = y2; i < y3; i++) + { + int ax = x2 + ((i - y2)*dx1) / dy1; + int bx = x1 + ((i - y1)*dx2) / dy2; + tmp = ax; + if (ax > bx) ax = bx, bx = tmp; + for (int j = ax; j < bx; j++) Draw(j, i, c, col); + } + } + }*/ + void DrawCircle(int xc, int yc, int r, wchar_t c = 0x2588, short col = 0x000F) { int x = 0; @@ -904,4 +1069,4 @@ protected: atomic olcConsoleGameEngine::m_bAtomActive(false); condition_variable olcConsoleGameEngine::m_cvGameFinished; -mutex olcConsoleGameEngine::m_muxGame; +mutex olcConsoleGameEngine::m_muxGame; \ No newline at end of file