/* OneLoneCoder.com - Code-It-Yourself! Worms Part #2 "owch...." - @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 ~~~~~~~~~~ Worms is a classic game where several teams of worms use a variety of weaponry to elimiate each other from a randomly generated terrain. This code is the second part of a series that show how to make your own Worms game from scratch in C++! Author ~~~~~~ Twitter: @javidx9 Blog: www.onelonecoder.com Video: ~~~~~~ Part #1 https://youtu.be/EHlaJvQpW3U Part #2 https://youtu.be/pV2qYJjCdxM Last Updated: 03/12/2017 */ #include #include #include using namespace std; #include "olcConsoleGameEngine.h" class cPhysicsObject { public: float px = 0.0f; // Position float py = 0.0f; float vx = 0.0f; // Velocity float vy = 0.0f; float ax = 0.0f; // Acceleration float ay = 0.0f; float radius = 4.0f; // Bounding circle for collision bool bStable = false; // Has object stopped moving float fFriction = 0.8f; // Actually, a dampening factor is a more accurate name int nBounceBeforeDeath = -1; // How many time object can bounce before death // -1 = infinite bool bDead = false; // Flag to indicate object should be removed cPhysicsObject(float x = 0.0f, float y = 0.0f) { px = x; py = y; } // Make class abstract virtual void Draw(olcConsoleGameEngine *engine, float fOffsetX, float fOffsetY) = 0; virtual int BounceDeathAction() = 0; }; class cDummy : public cPhysicsObject // Does nothing, shows a marker that helps with physics debug and test { public: cDummy(float x = 0.0f, float y = 0.0f) : cPhysicsObject(x, y) { } virtual void Draw(olcConsoleGameEngine *engine, float fOffsetX, float fOffsetY) { engine->DrawWireFrameModel(vecModel, px - fOffsetX, py - fOffsetY, atan2f(vy, vx), radius, FG_WHITE); } virtual int BounceDeathAction() { return 0; // Nothing, just fade } private: static vector> vecModel; }; vector> DefineDummy() { // Defines a circle with a line fom center to edge vector> vecModel; vecModel.push_back({ 0.0f, 0.0f }); for (int i = 0; i < 10; i++) vecModel.push_back({ cosf(i / 9.0f * 2.0f * 3.14159f) , sinf(i / 9.0f * 2.0f * 3.14159f) }); return vecModel; } vector> cDummy::vecModel = DefineDummy(); class cDebris : public cPhysicsObject // a small rock that bounces { public: cDebris(float x = 0.0f, float y = 0.0f) : cPhysicsObject(x, y) { // Set velocity to random direction and size for "boom" effect vx = 10.0f * cosf(((float)rand() / (float)RAND_MAX) * 2.0f * 3.14159f); vy = 10.0f * sinf(((float)rand() / (float)RAND_MAX) * 2.0f * 3.14159f); radius = 1.0f; fFriction = 0.8f; nBounceBeforeDeath = 5; // After 5 bounces, dispose } virtual void Draw(olcConsoleGameEngine *engine, float fOffsetX, float fOffsetY) { engine->DrawWireFrameModel(vecModel, px - fOffsetX, py - fOffsetY, atan2f(vy, vx), radius, FG_DARK_GREEN); } virtual int BounceDeathAction() { return 0; // Nothing, just fade } private: static vector> vecModel; }; vector> DefineDebris() { // A small unit rectangle vector> vecModel; vecModel.push_back({ 0.0f, 0.0f }); vecModel.push_back({ 1.0f, 0.0f }); vecModel.push_back({ 1.0f, 1.0f }); vecModel.push_back({ 0.0f, 1.0f }); return vecModel; } vector> cDebris::vecModel = DefineDebris(); class cMissile : public cPhysicsObject // A projectile weapon { public: cMissile(float x = 0.0f, float y = 0.0f, float _vx = 0.0f, float _vy = 0.0f) : cPhysicsObject(x, y) { radius = 2.5f; fFriction = 0.5f; vx = _vx; vy = _vy; bDead = false; nBounceBeforeDeath = 1; } virtual void Draw(olcConsoleGameEngine *engine, float fOffsetX, float fOffsetY) { engine->DrawWireFrameModel(vecModel, px - fOffsetX, py - fOffsetY, atan2f(vy, vx), radius, FG_YELLOW); } virtual int BounceDeathAction() { return 20; // Explode Big } private: static vector> vecModel; }; vector> DefineMissile() { // Defines a rocket like shape vector> vecModel; vecModel.push_back({ 0.0f, 0.0f }); vecModel.push_back({ 1.0f, 1.0f }); vecModel.push_back({ 2.0f, 1.0f }); vecModel.push_back({ 2.5f, 0.0f }); vecModel.push_back({ 2.0f, -1.0f }); vecModel.push_back({ 1.0f, -1.0f }); vecModel.push_back({ 0.0f, 0.0f }); vecModel.push_back({ -1.0f, -1.0f }); vecModel.push_back({ -2.5f, -1.0f }); vecModel.push_back({ -2.0f, 0.0f }); vecModel.push_back({ -2.5f, 1.0f }); vecModel.push_back({ -1.0f, 1.0f }); // Scale points to make shape unit sized for (auto &v : vecModel) { v.first /= 2.5f; v.second /= 2.5f; } return vecModel; } vector> cMissile::vecModel = DefineMissile(); class cWorm : public cPhysicsObject // A unit, or worm { public: cWorm(float x = 0.0f, float y = 0.0f) : cPhysicsObject(x, y) { radius = 3.5f; fFriction = 0.2f; bDead = false; nBounceBeforeDeath = -1; // load sprite data from sprite file if (sprWorm == nullptr) sprWorm = new olcSprite(L"../spriteeditor/worms.spr"); } virtual void Draw(olcConsoleGameEngine *engine, float fOffsetX, float fOffsetY) { engine->DrawPartialSprite(px - fOffsetX - radius, py - fOffsetY - radius, sprWorm, 0, 0, 8, 8); } virtual int BounceDeathAction() { return 0; // Nothing } public: float fShootAngle = 0.0f; private: static olcSprite *sprWorm; }; olcSprite* cWorm::sprWorm = nullptr; // Main Game Engine Class class OneLoneCoder_Worms : public olcConsoleGameEngine { public: OneLoneCoder_Worms() { m_sAppName = L"Worms"; } private: // Terrain size int nMapWidth = 1024; int nMapHeight = 512; unsigned char *map = nullptr; // Camera coordinates float fCameraPosX = 0.0f; float fCameraPosY = 0.0f; float fCameraPosXTarget = 0.0f; float fCameraPosYTarget = 0.0f; enum GAME_STATE { GS_RESET = 0, GS_GENERATE_TERRAIN = 1, GS_GENERATING_TERRAIN, GS_ALLOCATE_UNITS, GS_ALLOCATING_UNITS, GS_START_PLAY, GS_CAMERA_MODE } nGameState, nNextState; bool bGameIsStable = false; bool bPlayerHasControl = false; bool bPlayerActionComplete = false; // list of things that exist in game world list> listObjects; cPhysicsObject* pObjectUnderControl = nullptr; cPhysicsObject* pCameraTrackingObject = nullptr; bool bEnergising = false; float fEnergyLevel = 0.0f; bool bFireWeapon = false; virtual bool OnUserCreate() { // Create Map map = new unsigned char[nMapWidth * nMapHeight]; memset(map, 0, nMapWidth*nMapHeight * sizeof(unsigned char)); //CreateMap(); nGameState = GS_RESET; nNextState = GS_RESET; return true; } virtual bool OnUserUpdate(float fElapsedTime) { // Press 'M' key to regenerate map //if (m_keys[L'M'].bReleased) // CreateMap(); // Left click to cause small explosion if (m_mouse[0].bReleased) Boom(m_mousePosX + fCameraPosX, m_mousePosY + fCameraPosY, 10.0f); // Right click to drop missile if (m_mouse[1].bReleased) listObjects.push_back(unique_ptr(new cMissile(m_mousePosX + fCameraPosX, m_mousePosY + fCameraPosY))); // Middle click to spawn worm/unit if (m_mouse[2].bReleased) { cWorm* worm = new cWorm(m_mousePosX + fCameraPosX, m_mousePosY + fCameraPosY); pObjectUnderControl = worm; pCameraTrackingObject = worm; listObjects.push_back(unique_ptr(worm)); } // Mouse Edge Map Scroll float fMapScrollSpeed = 400.0f; if (m_mousePosX < 5) fCameraPosX -= fMapScrollSpeed * fElapsedTime; if (m_mousePosX > ScreenWidth() - 5) fCameraPosX += fMapScrollSpeed * fElapsedTime; if (m_mousePosY < 5) fCameraPosY -= fMapScrollSpeed * fElapsedTime; if (m_mousePosY > ScreenHeight() - 5) fCameraPosY += fMapScrollSpeed * fElapsedTime; // Control Supervisor switch (nGameState) { case GS_RESET: // Set game variables to know state { bGameIsStable = false; bPlayerActionComplete = false; bPlayerHasControl = false; nNextState = GS_GENERATE_TERRAIN; } break; case GS_GENERATE_TERRAIN: // Create a new terrain { bPlayerHasControl = false; CreateMap(); nNextState = GS_GENERATING_TERRAIN; } break; case GS_GENERATING_TERRAIN: // Does nothing, for now ;) { bPlayerHasControl = false; nNextState = GS_ALLOCATE_UNITS; } break; case GS_ALLOCATE_UNITS: // Add a unit to the top of the screen { bPlayerHasControl = false; cWorm *worm = new cWorm(32.0f, 1.0f); listObjects.push_back(unique_ptr(worm)); pObjectUnderControl = worm; pCameraTrackingObject = pObjectUnderControl; nNextState = GS_ALLOCATING_UNITS; } break; case GS_ALLOCATING_UNITS: // Stay in this state whilst units are deploying { bPlayerHasControl = false; if (bGameIsStable) // Can only leave state once game is stable { bPlayerActionComplete = false; nNextState = GS_START_PLAY; } } break; case GS_START_PLAY: // Player is in control of unit { bPlayerHasControl = true; if (bPlayerActionComplete) // Can only leave state when the player action has completed nNextState = GS_CAMERA_MODE; } break; case GS_CAMERA_MODE: // Camera is tracking on-screen action { bPlayerHasControl = false; bPlayerActionComplete = false; if (bGameIsStable) // Can only leave state when action has finished, and engine is stable { pCameraTrackingObject = pObjectUnderControl; nNextState = GS_START_PLAY; } } break; } // Handle User Input if (bPlayerHasControl) { if (pObjectUnderControl != nullptr) { if (pObjectUnderControl->bStable) { if (m_keys[L'Z'].bPressed) // Jump in direction of cursor { float a = ((cWorm*)pObjectUnderControl)->fShootAngle; pObjectUnderControl->vx = 4.0f * cosf(a); pObjectUnderControl->vy = 8.0f * sinf(a); pObjectUnderControl->bStable = false; } if (m_keys[L'A'].bHeld) // Rotate cursor CCW { cWorm* worm = (cWorm*)pObjectUnderControl; worm->fShootAngle -= 1.0f * fElapsedTime; if (worm->fShootAngle < -3.14159f) worm->fShootAngle += 3.14159f * 2.0f; } if (m_keys[L'S'].bHeld) // Rotate cursor CW { cWorm* worm = (cWorm*)pObjectUnderControl; worm->fShootAngle += 1.0f * fElapsedTime; if (worm->fShootAngle > 3.14159f) worm->fShootAngle -= 3.14159f * 2.0f; } if (m_keys[VK_SPACE].bPressed) // Start to charge weapon { bEnergising = true; bFireWeapon = false; fEnergyLevel = 0.0f; } if (m_keys[VK_SPACE].bHeld) // Weapon is charging { if (bEnergising) { fEnergyLevel += 0.75f * fElapsedTime; if (fEnergyLevel >= 1.0f) // If it maxes out, Fire! { fEnergyLevel = 1.0f; bFireWeapon = true; } } } if (m_keys[VK_SPACE].bReleased) // If it is released before maxing out, Fire! { if (bEnergising) { bFireWeapon = true; } bEnergising = false; } } if (bFireWeapon) { cWorm* worm = (cWorm*)pObjectUnderControl; // Get Weapon Origin float ox = worm->px; float oy = worm->py; // Get Weapon Direction float dx = cosf(worm->fShootAngle); float dy = sinf(worm->fShootAngle); // Create Weapon Object cMissile *m = new cMissile(ox, oy, dx * 40.0f * fEnergyLevel, dy * 40.0f * fEnergyLevel); listObjects.push_back(unique_ptr(m)); pCameraTrackingObject = m; // Reset flags involved with firing weapon bFireWeapon = false; fEnergyLevel = 0.0f; bEnergising = false; // Indicate the player has completed their action for this unit bPlayerActionComplete = true; } } } if (pCameraTrackingObject != nullptr) { //fCameraPosX = pCameraTrackingObject->px - ScreenWidth() / 2; //fCameraPosY = pCameraTrackingObject->py - ScreenHeight() / 2; fCameraPosXTarget = pCameraTrackingObject->px - ScreenWidth() / 2; fCameraPosYTarget = pCameraTrackingObject->py - ScreenHeight() / 2; fCameraPosX += (fCameraPosXTarget - fCameraPosX) * 5.0f * fElapsedTime; fCameraPosY += (fCameraPosYTarget - fCameraPosY) * 5.0f * fElapsedTime; } // Clamp map boundaries if (fCameraPosX < 0) fCameraPosX = 0; if (fCameraPosX >= nMapWidth - ScreenWidth()) fCameraPosX = nMapWidth - ScreenWidth(); if (fCameraPosY < 0) fCameraPosY = 0; if (fCameraPosY >= nMapHeight - ScreenHeight()) fCameraPosY = nMapHeight - ScreenHeight(); // Do 10 physics iterations per frame - this allows smaller physics steps // giving rise to more accurate and controllable calculations for (int z = 0; z < 10; z++) { // Update physics of all physical objects for (auto &p : listObjects) { // Apply Gravity p->ay += 2.0f; // Update Velocity p->vx += p->ax * fElapsedTime; p->vy += p->ay * fElapsedTime; // Update Position float fPotentialX = p->px + p->vx * fElapsedTime; float fPotentialY = p->py + p->vy * fElapsedTime; // Reset Acceleration p->ax = 0.0f; p->ay = 0.0f; p->bStable = false; // Collision Check With Map float fAngle = atan2f(p->vy, p->vx); float fResponseX = 0; float fResponseY = 0; bool bCollision = false; // Iterate through semicircle of objects radius rotated to direction of travel for (float r = fAngle - 3.14159f / 2.0f; r < fAngle + 3.14159f / 2.0f; r += 3.14159f / 8.0f) { // Calculate test point on circumference of circle float fTestPosX = (p->radius) * cosf(r) + fPotentialX; float fTestPosY = (p->radius) * sinf(r) + fPotentialY; // Constrain to test within map boundary if (fTestPosX >= nMapWidth) fTestPosX = nMapWidth - 1; if (fTestPosY >= nMapHeight) fTestPosY = nMapHeight - 1; if (fTestPosX < 0) fTestPosX = 0; if (fTestPosY < 0) fTestPosY = 0; // Test if any points on semicircle intersect with terrain if (map[(int)fTestPosY * nMapWidth + (int)fTestPosX] != 0) { // Accumulate collision points to give an escape response vector // Effectively, normal to the areas of contact fResponseX += fPotentialX - fTestPosX; fResponseY += fPotentialY - fTestPosY; bCollision = true; } } // Calculate magnitudes of response and velocity vectors float fMagVelocity = sqrtf(p->vx*p->vx + p->vy*p->vy); float fMagResponse = sqrtf(fResponseX*fResponseX + fResponseY*fResponseY); // Collision occurred if (bCollision) { // Force object to be stable, this stops the object penetrating the terrain p->bStable = true; // Calculate reflection vector of objects velocity vector, using response vector as normal float dot = p->vx * (fResponseX / fMagResponse) + p->vy * (fResponseY / fMagResponse); // Use friction coefficient to dampen response (approximating energy loss) p->vx = p->fFriction * (-2.0f * dot * (fResponseX / fMagResponse) + p->vx); p->vy = p->fFriction * (-2.0f * dot * (fResponseY / fMagResponse) + p->vy); //Some objects will "die" after several bounces if (p->nBounceBeforeDeath > 0) { p->nBounceBeforeDeath--; p->bDead = p->nBounceBeforeDeath == 0; // If object died, work out what to do next if (p->bDead) { // Action upon object death // = 0 Nothing // > 0 Explosion int nResponse = p->BounceDeathAction(); if (nResponse > 0) { Boom(p->px, p->py, nResponse); // Dead objects can no lobger be tracked by the camera pCameraTrackingObject = nullptr; } } } } else { // No collision so update objects position p->px = fPotentialX; p->py = fPotentialY; } // Turn off movement when tiny if (fMagVelocity < 0.1f) p->bStable = true; } // Remove dead objects from the list, so they are not processed further. As the object // is a unique pointer, it will go out of scope too, deleting the object automatically. Nice :-) listObjects.remove_if([](unique_ptr &o) {return o->bDead; }); } // Draw Landscape for (int x = 0; x < ScreenWidth(); x++) for (int y = 0; y < ScreenHeight(); y++) { // Offset screen coordinates into world coordinates switch (map[(y + (int)fCameraPosY)*nMapWidth + (x + (int)fCameraPosX)]) { case 0: Draw(x, y, PIXEL_SOLID, FG_CYAN); // Sky break; case 1: Draw(x, y, PIXEL_SOLID, FG_DARK_GREEN); // Land break; } } // Draw Objects for (auto &p : listObjects) { p->Draw(this, fCameraPosX, fCameraPosY); cWorm* worm = (cWorm*)pObjectUnderControl; if (p.get() == worm) { float cx = worm->px + 8.0f * cosf(worm->fShootAngle) - fCameraPosX; float cy = worm->py + 8.0f * sinf(worm->fShootAngle) - fCameraPosY; // Draw "+" symbol Draw(cx, cy, PIXEL_SOLID, FG_BLACK); Draw(cx + 1, cy, PIXEL_SOLID, FG_BLACK); Draw(cx - 1, cy, PIXEL_SOLID, FG_BLACK); Draw(cx, cy + 1, PIXEL_SOLID, FG_BLACK); Draw(cx, cy - 1, PIXEL_SOLID, FG_BLACK); // Draws an Energy Bar, indicating how much energy should the weapon be // fired with for (int i = 0; i < 11 * fEnergyLevel; i++) { Draw(worm->px - 5 + i - fCameraPosX, worm->py - 12 - fCameraPosY, PIXEL_SOLID, FG_GREEN); Draw(worm->px - 5 + i - fCameraPosX, worm->py - 11 - fCameraPosY, PIXEL_SOLID, FG_RED); } } } // Check For game state stability bGameIsStable = true; for (auto &p : listObjects) if (!p->bStable) { bGameIsStable = false; break; } // DEBUG Feature: Indicate Game Stability if (bGameIsStable) Fill(2, 2, 6, 6, PIXEL_SOLID, FG_RED); // Update State Machine nGameState = nNextState; return true; } // Explosion Function void Boom(float fWorldX, float fWorldY, float fRadius) { auto CircleBresenham = [&](int xc, int yc, int r) { // Taken from wikipedia int x = 0; int y = r; int p = 3 - 2 * r; if (!r) return; auto drawline = [&](int sx, int ex, int ny) { for (int i = sx; i < ex; i++) if (ny >= 0 && ny < nMapHeight && i >= 0 && i < nMapWidth) map[ny*nMapWidth + i] = 0; }; while (y >= x) { // Modified to draw scan-lines instead of edges drawline(xc - x, xc + x, yc - y); drawline(xc - y, xc + y, yc - x); drawline(xc - x, xc + x, yc + y); drawline(xc - y, xc + y, yc + x); if (p < 0) p += 4 * x++ + 6; else p += 4 * (x++ - y--) + 10; } }; // Erase Terrain to form crater CircleBresenham(fWorldX, fWorldY, fRadius); // Shockwave other entities in range for (auto &p : listObjects) { // Work out distance between explosion origin and object float dx = p->px - fWorldX; float dy = p->py - fWorldY; float fDist = sqrt(dx*dx + dy*dy); if (fDist < 0.0001f) fDist = 0.0001f; // If within blast radius if (fDist < fRadius) { // Set velocity proportional and away from boom origin p->vx = (dx / fDist) * fRadius; p->vy = (dy / fDist) * fRadius; p->bStable = false; } } // Launch debris proportional to blast size for (int i = 0; i < (int)fRadius; i++) listObjects.push_back(unique_ptr(new cDebris(fWorldX, fWorldY))); } void CreateMap() { // Used 1D Perlin Noise float *fSurface = new float[nMapWidth]; float *fNoiseSeed = new float[nMapWidth]; // Populate with noise for (int i = 0; i < nMapWidth; i++) fNoiseSeed[i] = (float)rand() / (float)RAND_MAX; // Clamp noise to half way up screen fNoiseSeed[0] = 0.5f; // Generate 1D map PerlinNoise1D(nMapWidth, fNoiseSeed, 8, 2.0f, fSurface); // Fill 2D map based on adjacent 1D map for (int x = 0; x < nMapWidth; x++) for (int y = 0; y < nMapHeight; y++) { if (y >= fSurface[x] * nMapHeight) map[y * nMapWidth + x] = 1; else map[y * nMapWidth + x] = 0; } // Clean up! delete[] fSurface; delete[] fNoiseSeed; } // Taken from Perlin Noise Video https://youtu.be/6-0UaeJBumA void PerlinNoise1D(int nCount, float *fSeed, int nOctaves, float fBias, float *fOutput) { // Used 1D Perlin Noise for (int x = 0; x < nCount; x++) { float fNoise = 0.0f; float fScaleAcc = 0.0f; float fScale = 1.0f; for (int o = 0; o < nOctaves; o++) { int nPitch = nCount >> o; int nSample1 = (x / nPitch) * nPitch; int nSample2 = (nSample1 + nPitch) % nCount; float fBlend = (float)(x - nSample1) / (float)nPitch; float fSample = (1.0f - fBlend) * fSeed[nSample1] + fBlend * fSeed[nSample2]; fScaleAcc += fScale; fNoise += fSample * fScale; fScale = fScale / fBias; } // Scale to seed range fOutput[x] = fNoise / fScaleAcc; } } }; int main() { OneLoneCoder_Worms game; game.ConstructConsole(256, 160, 6, 6); game.Start(); return 0; }