You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
346 lines
10 KiB
346 lines
10 KiB
/*
|
|
OneLoneCoder.com - Code-It-Yourself! Asteroids at the command prompt (quick and simple c++)
|
|
"This one just kept growing..." - @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
|
|
~~~~~~~~~~
|
|
I needed a quick video to keep up with my upload schedule, and thought "oh asteroids
|
|
must be simple", and indeed it was, but it was a bit rubbish, and didn't really
|
|
offer anything educational, until I made the decision to ditch sprites and use
|
|
vector models like the original game. This made the game considerably more sophisticated,
|
|
has opened up new possibilities for the game engine, and just goes to show, jus 'cos
|
|
it's old, doesn't mean it's irrelevant.
|
|
|
|
I ended up with a demonstration of physics, wireframe graphics, 2D matrix transformations,
|
|
toroidal space mapping and vector manipulation.
|
|
|
|
Controls
|
|
~~~~~~~~
|
|
Left and Right arrow keys steer the ship. Up arrow is thrust. There is no reverse thrust.
|
|
Space bar fires bullets. Get the highest score by surviving waves of asteroids.
|
|
|
|
Author
|
|
~~~~~~
|
|
Twitter: @javidx9
|
|
Blog: www.onelonecoder.com
|
|
YouTube: www.youtube.com/javidx9
|
|
|
|
Video:
|
|
~~~~~~
|
|
https://youtu.be/QgDR8LrRZhk
|
|
|
|
Last Updated: 30/08/2017
|
|
*/
|
|
|
|
#include <iostream>
|
|
#include <string>
|
|
#include <algorithm>
|
|
using namespace std;
|
|
|
|
#include "olcConsoleGameEngine.h"
|
|
|
|
class OneLoneCoder_Asteroids : public olcConsoleGameEngine
|
|
{
|
|
public:
|
|
OneLoneCoder_Asteroids()
|
|
{
|
|
m_sAppName = L"Asteroids";
|
|
}
|
|
|
|
private:
|
|
struct sSpaceObject
|
|
{
|
|
int nSize;
|
|
float x;
|
|
float y;
|
|
float dx;
|
|
float dy;
|
|
float angle;
|
|
};
|
|
|
|
vector<sSpaceObject> vecAsteroids;
|
|
vector<sSpaceObject> vecBullets;
|
|
sSpaceObject player;
|
|
bool bDead = false;
|
|
int nScore = 0;
|
|
|
|
vector<pair<float, float>> vecModelShip;
|
|
vector<pair<float, float>> vecModelAsteroid;
|
|
|
|
protected:
|
|
// Called by olcConsoleGameEngine
|
|
virtual bool OnUserCreate()
|
|
{
|
|
vecModelShip =
|
|
{
|
|
{ 0.0f, -5.0f},
|
|
{-2.5f, +2.5f},
|
|
{+2.5f, +2.5f}
|
|
}; // A simple Isoceles Triangle
|
|
|
|
// Create a "jagged" circle for the asteroid. It's important it remains
|
|
// mostly circular, as we do a simple collision check against a perfect
|
|
// circle.
|
|
int verts = 20;
|
|
for (int i = 0; i < verts; i++)
|
|
{
|
|
float noise = (float)rand() / (float)RAND_MAX * 0.4f + 0.8f;
|
|
vecModelAsteroid.push_back(make_pair(noise * sinf(((float)i / (float)verts) * 6.28318f),
|
|
noise * cosf(((float)i / (float)verts) * 6.28318f)));
|
|
}
|
|
|
|
ResetGame();
|
|
return true;
|
|
}
|
|
|
|
void ResetGame()
|
|
{
|
|
// Initialise Player Position
|
|
player.x = ScreenWidth() / 2.0f;
|
|
player.y = ScreenHeight() / 2.0f;
|
|
player.dx = 0.0f;
|
|
player.dy = 0.0f;
|
|
player.angle = 0.0f;
|
|
|
|
vecBullets.clear();
|
|
vecAsteroids.clear();
|
|
|
|
// Put in two asteroids
|
|
vecAsteroids.push_back({ (int)16, 20.0f, 20.0f, 8.0f, -6.0f, 0.0f });
|
|
vecAsteroids.push_back({ (int)16, 100.0f, 20.0f, -5.0f, 3.0f, 0.0f });
|
|
|
|
// Reset game
|
|
bDead = false;
|
|
nScore = false;
|
|
}
|
|
|
|
// Implements "wrap around" for various in-game sytems
|
|
void WrapCoordinates(float ix, float iy, float &ox, float &oy)
|
|
{
|
|
ox = ix;
|
|
oy = iy;
|
|
if (ix < 0.0f) ox = ix + (float)ScreenWidth();
|
|
if (ix >= (float)ScreenWidth()) ox = ix - (float)ScreenWidth();
|
|
if (iy < 0.0f) oy = iy + (float)ScreenHeight();
|
|
if (iy >= (float)ScreenHeight()) oy = iy - (float)ScreenHeight();
|
|
}
|
|
|
|
// Overriden to handle toroidal drawing routines
|
|
virtual void Draw(int x, int y, wchar_t c = 0x2588, short col = 0x000F)
|
|
{
|
|
float fx, fy;
|
|
WrapCoordinates(x, y, fx, fy);
|
|
olcConsoleGameEngine::Draw(fx, fy, c, col);
|
|
}
|
|
|
|
bool IsPointInsideCircle(float cx, float cy, float radius, float x, float y)
|
|
{
|
|
return sqrt((x-cx)*(x-cx) + (y-cy)*(y-cy)) < radius;
|
|
}
|
|
|
|
// Called by olcConsoleGameEngine
|
|
virtual bool OnUserUpdate(float fElapsedTime)
|
|
{
|
|
if (bDead)
|
|
ResetGame();
|
|
|
|
// Clear Screen
|
|
Fill(0, 0, ScreenWidth(), ScreenHeight(), PIXEL_SOLID, 0);
|
|
|
|
// Steer Ship
|
|
if (m_keys[VK_LEFT].bHeld)
|
|
player.angle -= 5.0f * fElapsedTime;
|
|
if (m_keys[VK_RIGHT].bHeld)
|
|
player.angle += 5.0f * fElapsedTime;
|
|
|
|
// Thrust? Apply ACCELERATION
|
|
if (m_keys[VK_UP].bHeld)
|
|
{
|
|
// ACCELERATION changes VELOCITY (with respect to time)
|
|
player.dx += sin(player.angle) * 20.0f * fElapsedTime;
|
|
player.dy += -cos(player.angle) * 20.0f * fElapsedTime;
|
|
}
|
|
|
|
// VELOCITY changes POSITION (with respect to time)
|
|
player.x += player.dx * fElapsedTime;
|
|
player.y += player.dy * fElapsedTime;
|
|
|
|
// Keep ship in gamespace
|
|
WrapCoordinates(player.x, player.y, player.x, player.y);
|
|
|
|
// Check ship collision with asteroids
|
|
for (auto &a : vecAsteroids)
|
|
if (IsPointInsideCircle(a.x, a.y, a.nSize, player.x, player.y))
|
|
bDead = true; // Uh oh...
|
|
|
|
// Fire Bullet in direction of player
|
|
if (m_keys[VK_SPACE].bReleased)
|
|
vecBullets.push_back({ 0, player.x, player.y, 50.0f * sinf(player.angle), -50.0f * cosf(player.angle), 100.0f });
|
|
|
|
// Update and draw asteroids
|
|
for (auto &a : vecAsteroids)
|
|
{
|
|
// VELOCITY changes POSITION (with respect to time)
|
|
a.x += a.dx * fElapsedTime;
|
|
a.y += a.dy * fElapsedTime;
|
|
a.angle += 0.5f * fElapsedTime; // Add swanky rotation :)
|
|
|
|
// Asteroid coordinates are kept in game space (toroidal mapping)
|
|
WrapCoordinates(a.x, a.y, a.x, a.y);
|
|
|
|
// Draw Asteroids
|
|
DrawWireFrameModel(vecModelAsteroid, a.x, a.y, a.angle, (float)a.nSize, FG_YELLOW);
|
|
}
|
|
|
|
// Any new asteroids created after collision detection are stored
|
|
// in a temporary vector, so we don't interfere with the asteroids
|
|
// vector iterator in the for(auto)
|
|
vector<sSpaceObject> newAsteroids;
|
|
|
|
// Update Bullets
|
|
for (auto &b : vecBullets)
|
|
{
|
|
b.x += b.dx * fElapsedTime;
|
|
b.y += b.dy * fElapsedTime;
|
|
WrapCoordinates(b.x, b.y, b.x, b.y);
|
|
b.angle -= 1.0f * fElapsedTime;
|
|
|
|
// Check collision with asteroids
|
|
for (auto &a : vecAsteroids)
|
|
{
|
|
//if (IsPointInsideRectangle(a.x, a.y, a.x + a.nSize, a.y + a.nSize, b.x, b.y))
|
|
if(IsPointInsideCircle(a.x, a.y, a.nSize, b.x, b.y))
|
|
{
|
|
// Asteroid Hit - Remove bullet
|
|
// We've already updated the bullets, so force bullet to be offscreen
|
|
// so it is cleaned up by the removal algorithm.
|
|
b.x = -100;
|
|
|
|
// Create child asteroids
|
|
if (a.nSize > 4)
|
|
{
|
|
float angle1 = ((float)rand() / (float)RAND_MAX) * 6.283185f;
|
|
float angle2 = ((float)rand() / (float)RAND_MAX) * 6.283185f;
|
|
newAsteroids.push_back({ (int)a.nSize >> 1 ,a.x, a.y, 10.0f * sinf(angle1), 10.0f * cosf(angle1), 0.0f });
|
|
newAsteroids.push_back({ (int)a.nSize >> 1 ,a.x, a.y, 10.0f * sinf(angle2), 10.0f * cosf(angle2), 0.0f });
|
|
}
|
|
|
|
// Remove asteroid - Same approach as bullets
|
|
a.x = -100;
|
|
nScore += 100; // Small score increase for hitting asteroid
|
|
}
|
|
}
|
|
}
|
|
|
|
// Append new asteroids to existing vector
|
|
for(auto a:newAsteroids)
|
|
vecAsteroids.push_back(a);
|
|
|
|
// Clear up dead objects - They are out of game space
|
|
|
|
// Remove asteroids that have been blown up
|
|
if (vecAsteroids.size() > 0)
|
|
{
|
|
auto i = remove_if(vecAsteroids.begin(), vecAsteroids.end(), [&](sSpaceObject o) { return (o.x < 0); });
|
|
if (i != vecAsteroids.end())
|
|
vecAsteroids.erase(i);
|
|
}
|
|
|
|
if (vecAsteroids.empty()) // If no asteroids, level complete! :) - you win MORE asteroids!
|
|
{
|
|
// Level Clear
|
|
nScore += 1000; // Large score for level progression
|
|
vecAsteroids.clear();
|
|
vecBullets.clear();
|
|
|
|
// Add two new asteroids, but in a place where the player is not, we'll simply
|
|
// add them 90 degrees left and right to the player, their coordinates will
|
|
// be wrapped by th enext asteroid update
|
|
vecAsteroids.push_back({ (int)16, 30.0f * sinf(player.angle - 3.14159f/2.0f) + player.x,
|
|
30.0f * cosf(player.angle - 3.14159f/2.0f) + player.y,
|
|
10.0f * sinf(player.angle), 10.0f*cosf(player.angle), 0.0f });
|
|
|
|
vecAsteroids.push_back({ (int)16, 30.0f * sinf(player.angle + 3.14159f/2.0f) + player.x,
|
|
30.0f * cosf(player.angle + 3.14159f/2.0f) + player.y,
|
|
10.0f * sinf(-player.angle), 10.0f*cosf(-player.angle), 0.0f });
|
|
}
|
|
|
|
// Remove bullets that have gone off screen
|
|
if (vecBullets.size() > 0)
|
|
{
|
|
auto i = remove_if(vecBullets.begin(), vecBullets.end(), [&](sSpaceObject o) { return (o.x < 1 || o.y < 1 || o.x >= ScreenWidth() - 1 || o.y >= ScreenHeight() - 1); });
|
|
if (i != vecBullets.end())
|
|
vecBullets.erase(i);
|
|
}
|
|
|
|
// Draw Bullets
|
|
for (auto b : vecBullets)
|
|
Draw(b.x, b.y);
|
|
|
|
// Draw Ship
|
|
DrawWireFrameModel(vecModelShip, player.x, player.y, player.angle);
|
|
|
|
// Draw Score
|
|
DrawString(2, 2, L"SCORE: " + to_wstring(nScore));
|
|
return true;
|
|
}
|
|
|
|
void DrawWireFrameModel(const vector<pair<float, float>> &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<pair<float, float>> 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(vecTransformedCoordinates[i % verts].first, vecTransformedCoordinates[i % verts].second,
|
|
vecTransformedCoordinates[j % verts].first, vecTransformedCoordinates[j % verts].second, PIXEL_SOLID, col);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
int main()
|
|
{
|
|
// Use olcConsoleGameEngine derived app
|
|
OneLoneCoder_Asteroids game;
|
|
game.ConstructConsole(160, 100, 8, 8);
|
|
game.Start();
|
|
return 0;
|
|
} |