1
0
Fork 0
mirror of https://gitlab.com/sortix/sortix.git synced 2023-02-13 20:55:38 -05:00
sortix--sortix/games/asteroids.cpp
Jonas 'Sortie' Termansen a1c106ce1c Early prototype of an asteroids game.
asteroids(1) now uses chvideomode(1) if no driver is active.

Made the asteroids game object oriented.

Added asteroids to ateroids(1).

uptime(2) in asteroids.
2012-09-09 13:34:48 +02:00

841 lines
18 KiB
C++

#include <sys/keycodes.h>
#include <sys/termmode.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <error.h>
// This define runs the game without actually setting the video mode and
// checking whether the frame was actually copied to the screen. useful for
// debugging the game since you can't see console output.
//#define HACK_DONT_CHECK_FB
// TODO: Hacks that should belong in libm or something.
extern "C" float sqrtf(float x)
{
float ret;
asm ("fsqrt" : "=t" (ret) : "0" (x));
return ret;
}
extern "C" void sincosf(float x, float* sinval, float* cosval)
{
asm ("fsincos" : "=t" (*cosval), "=u" (*sinval) : "0" (x));
}
const float PI = 3.1415926532f;
inline float RandomFloat()
{
return (float) rand() / 32768.0f;
}
inline float RandomFloat(float min, float max)
{
return min + RandomFloat() * (max - min);
}
inline float DegreeToRadian(float degree)
{
return degree / 180 * PI;
}
inline float RandomAngle()
{
return RandomFloat() * DegreeToRadian(360);
}
inline uint32_t MakeColor(uint8_t r, uint8_t g, uint8_t b)
{
return b << 0UL | g << 8UL | r << 16UL;
}
const size_t STARFIELD_WIDTH = 512UL;
const size_t STARFIELD_HEIGHT = 512UL;
uint32_t starfield[STARFIELD_WIDTH * STARFIELD_HEIGHT];
void GenerateStarfield(uint32_t* bitmap, size_t width, size_t height)
{
size_t numpixels = width * height;
for ( size_t i = 0; i < numpixels; i++ )
{
uint8_t color = 0;
int randval = rand() % 256;
bool isstar = randval == 5 || randval == 42 || randval == 101;
if ( isstar ) { color = rand(); }
bitmap[i] = MakeColor(color, color, color);
}
}
const size_t MAXKEYNUM = 512UL;
bool keysdown[MAXKEYNUM] = { false };
void FetchKeyboardInput()
{
// Read the keyboard input from the user.
const unsigned termmode = TERMMODE_KBKEY
| TERMMODE_UNICODE
| TERMMODE_SIGNAL
| TERMMODE_NONBLOCK;
if ( settermmode(0, termmode) ) { error(1, errno, "settermmode"); }
uint32_t codepoint;
ssize_t numbytes;
while ( 0 < (numbytes = read(0, &codepoint, sizeof(codepoint))) )
{
int kbkey = KBKEY_DECODE(codepoint);
int abskbkey = (kbkey < 0) ? -kbkey : kbkey;
if ( MAXKEYNUM <= abskbkey ) { continue; }
keysdown[abskbkey] = 0 < kbkey;
}
}
size_t xres;
size_t yres;
int fb;
size_t bpp;
size_t linesize;
size_t framesize;
uint32_t* buf;
bool gamerunning;
unsigned long framenum;
void DrawLine(uint32_t color, long x0, long y0, long x1, long y1)
{
long dx = labs(x1-x0);
long sx = x0 < x1 ? 1 : -1;
long dy = labs(y1-y0);
long sy = y0 < y1 ? 1 : -1;
long err = (dx>dy ? dx : -dy)/2L;
long e2;
while ( true )
{
if ( 0 <= x0 && x0 < xres && 0 <= y0 & y0 < yres )
{
size_t index = y0 * linesize + x0;
buf[index] = color;
}
if ( x0 == x1 && y0 == y1 ) { break; }
e2 = err;
if ( e2 > -dx ) { err -= dy; x0 += sx; }
if ( e2 < dy ) { err += dx; y0 += sy; }
}
}
class Vector
{
public:
float x;
float y;
public:
Vector(float x = 0.0f, float y = 0.0f) : x(x), y(y) { }
Vector& operator=(const Vector& rhs)
{
if ( this != &rhs ) { x = rhs.x; y = rhs.y; }
return *this;
}
Vector& operator+=(const Vector& rhs)
{
x += rhs.x;
y += rhs.y;
}
Vector& operator-=(const Vector& rhs)
{
x -= rhs.x;
y -= rhs.y;
}
Vector& operator*=(float scalar)
{
x *= scalar;
y *= scalar;
}
const Vector operator+(const Vector& other) const
{
Vector ret(*this); ret += other; return ret;
}
const Vector operator-(const Vector& other) const
{
Vector ret(*this); ret -= other; return ret;
}
const Vector operator*(float scalar) const
{
Vector ret(*this); ret *= scalar; return ret;
}
bool operator==(const Vector& other) const
{
return x == other.x && y == other.y;
}
bool operator!=(const Vector& other) const
{
return !(*this == other);
}
float Dot(const Vector& other) const
{
return x * other.x + y * other.y;
}
float SquaredSize() const
{
return x*x + y*y;
}
float Size() const
{
return sqrtf(SquaredSize());
}
float DistanceTo(const Vector& other) const
{
return (other - *this).Size();
}
const Vector Rotate(float radians) const
{
float sinr;
float cosr;
sincosf(radians, &sinr, &cosr);
float newx = x * cosr - y * sinr;
float newy = x * sinr + y * cosr;
return Vector(newx, newy);
}
const Vector RotateAround(float radians, const Vector& off) const
{
return Vector(*this - off).Rotate(radians) + off;
}
};
bool AboveLine(const Vector& a, const Vector& b, const Vector& p)
{
Vector ba = b - a;
Vector bahat = Vector(ba.y, -ba.x);
Vector bp = p - a;
return 0.0 <= bahat.Dot(bp);
}
bool InsideTriangle(const Vector& a, const Vector& b, const Vector& c,
const Vector& p)
{
return !AboveLine(a, b, p) && !AboveLine(b, c, p) && !AboveLine(c, a, p);
}
class Object;
class Actor;
class Spaceship;
Object* firstobject = NULL;
Object* lastobject = NULL;
Vector screenoff;
Spaceship* playership = NULL;
class Object
{
public:
Object()
{
gcborn = false;
gcdead = false;
if ( !firstobject )
{
firstobject = lastobject = this;
prevobj = nextobj = NULL;
}
else
{
lastobject->nextobj = this;
this->prevobj = lastobject;
this->nextobj = NULL;
lastobject = this;
}
};
virtual ~Object()
{
if ( !prevobj ) { firstobject = nextobj; }
else { prevobj->nextobj = nextobj; }
if ( !nextobj ) { lastobject = prevobj; }
else { nextobj->prevobj = prevobj; }
}
public:
virtual bool IsA(const char* classname)
{
return !strcmp(classname, "Object");
}
virtual void OnFrame(float deltatime) { }
virtual void Render() { }
private:
bool gcborn;
bool gcdead;
Object* prevobj;
Object* nextobj;
public:
bool GCIsBorn() const { return gcborn; }
bool GCIsDead() const { return gcdead; }
bool GCIsAlive() const { return GCIsBorn() && !GCIsDead(); }
void GCDie() { gcdead = true; }
void GCBirth() { gcborn = true; }
Object* NextObj() const { return nextobj; }
};
class Actor : public Object
{
public:
Actor() { }
virtual ~Actor() { }
public:
virtual void OnFrame(float deltatime)
{
Think(deltatime);
Move(deltatime);
}
virtual bool IsA(const char* classname)
{
return !strcmp(classname, "Actor") || Object::IsA(classname);
}
virtual void Move(float deltatime);
virtual void Think(float deltatime) { }
virtual void Render() { }
public:
Vector pos;
Vector vel;
Vector acc;
};
void Actor::Move(float deltatime)
{
vel += acc * deltatime;
pos += vel * deltatime;
}
class Asteroid : public Actor
{
public:
Asteroid(Vector pos, Vector vel, float size);
virtual ~Asteroid() { }
virtual bool IsA(const char* classname)
{
return !strcmp(classname, "Asteroid") || Actor::IsA(classname);
}
virtual void Move(float deltatime);
virtual void Render();
private:
Vector Point(size_t id);
public:
bool InsideMe(const Vector& p);
void OnHit();
private:
static const size_t MIN_POLYS = 5;
static const size_t MAX_POLYS = 12;
static const float MAX_TURN_SPEED = 50.0f;
size_t numpolygons;
float slice;
float polydists[MAX_POLYS+1];
float size;
float angle;
float turnspeed;
};
Asteroid::Asteroid(Vector pos, Vector vel, float size)
{
this->pos = pos;
this->vel = vel;
this->size = size;
angle = 0.0f;
turnspeed = DegreeToRadian(MAX_TURN_SPEED) * (RandomFloat() * 2.0f - 1.0f);
numpolygons = MIN_POLYS + rand() % (MAX_POLYS - MIN_POLYS);
slice = DegreeToRadian(360.0f) / (float) numpolygons;
for ( size_t i = 0; i < numpolygons; i++ )
{
polydists[i] = (RandomFloat() + 1.0) * size / 2.0;
}
polydists[numpolygons] = polydists[0];
}
void Asteroid::Move(float deltatime)
{
Actor::Move(deltatime);
angle += turnspeed * deltatime;
}
Vector Asteroid::Point(size_t i)
{
float rot = i * slice + angle;
return Vector(polydists[i], 0.0).Rotate(rot);
}
bool Asteroid::InsideMe(const Vector& p)
{
const Vector& center = pos;
for ( size_t i = 0; i < numpolygons; i++ )
{
Vector from = Point(i) + pos;
Vector to = Point(i+1) + pos;
if ( InsideTriangle(from, to, center, p) ) { return true; }
}
return false;
}
void Asteroid::Render()
{
Vector screenpos = pos - screenoff;
uint32_t color = MakeColor(200, 200, 200);
float slice = DegreeToRadian(360.0f) / (float) numpolygons;
for ( size_t i = 0; i < numpolygons; i++ )
{
Vector from = Point(i) + screenpos;
Vector to = Point(i+1) + screenpos;
DrawLine(color, from.x, from.y, to.x, to.y);
}
}
void Asteroid::OnHit()
{
if ( !GCIsAlive() ) { return; }
Vector axis = Vector(size/2.0f, 0.0f).Rotate(RandomAngle());
float sizea = RandomFloat(size*0.3, size*0.7);
float sizeb = RandomFloat(size*0.3, size*0.7);
const float MINIMUM_SIZE = 6.0;
const float MAX_ANGLE = DegreeToRadian(45);
if ( MINIMUM_SIZE <= sizea )
{
Vector astvel = vel.Rotate(RandomFloat(0.0, MAX_ANGLE)) * 1.2;
new Asteroid(pos + axis, astvel, sizea);
}
if ( MINIMUM_SIZE <= sizeb )
{
Vector astvel = vel.Rotate(RandomFloat(0.0, -MAX_ANGLE)) * 1.2;
new Asteroid(pos - axis, astvel, sizeb);
}
GCDie();
}
class AsteroidField : public Actor
{
public:
AsteroidField() { }
virtual ~AsteroidField() { }
virtual bool IsA(const char* classname)
{
return !strcmp(classname, "AsteroidField") || Actor::IsA(classname);
}
public:
virtual void Think(float deltatime);
};
void AsteroidField::Think(float deltatime)
{
float spawndist = 1500.0f;
float maxdist = 1.5 * spawndist;
size_t minimumasteroids = 200;
size_t numasteroids = 0;
Vector center = ((Actor*)playership)->pos;
for ( Object* obj = firstobject; obj; obj = obj->NextObj() )
{
if ( !obj->IsA("Asteroid") ) { continue; }
Asteroid* ast = (Asteroid*) obj;
numasteroids++;
float dist = ast->pos.DistanceTo(center);
if ( spawndist < dist ) { ast->GCDie(); }
}
for ( ; numasteroids < minimumasteroids; numasteroids++ )
{
float dist = RandomFloat(spawndist, maxdist);
Vector astpos = Vector(dist, 0.0f).Rotate(RandomAngle()) + center;
float minsize = 4.0;
float maxsize = 120.0f;
float maxspeed = 80.0f;
float size = RandomFloat(minsize, maxsize);
float speed = RandomFloat() * maxspeed;
Vector astvel = Vector(speed, 0.0).Rotate(RandomAngle());
new Asteroid(astpos, astvel, size);
}
}
class Missile : public Actor
{
public:
Missile(Vector pos, Vector vel, float ttl);
virtual bool IsA(const char* classname)
{
return !strcmp(classname, "Missile") || Actor::IsA(classname);
}
virtual void Think(float deltatime);
virtual void Render();
virtual ~Missile();
private:
float ttl;
};
Missile::Missile(Vector pos, Vector vel, float ttl)
{
this->pos = pos;
this->vel = vel;
this->ttl = ttl;
}
void Missile::Think(float deltatime)
{
ttl -= deltatime;
if ( ttl < 0 ) { GCDie(); }
for ( Object* obj = firstobject; obj; obj = obj->NextObj() )
{
if ( !obj->GCIsAlive() ) { continue; }
if ( !obj->IsA("Asteroid") ) { continue; }
Asteroid* ast = (Asteroid*) obj;
if ( !ast->InsideMe(pos) ) { continue; }
ast->OnHit();
GCDie();
}
}
void Missile::Render()
{
Vector screenpos = pos - screenoff;
uint32_t shipcolor = MakeColor(31, 255, 31);
const float MISSILE_LEN = 5.0f;
Vector from = screenpos;
Vector to = screenpos + vel * (MISSILE_LEN / vel.Size());
DrawLine(shipcolor, from.x, from.y, to.x, to.y);
}
Missile::~Missile()
{
}
class Spaceship : public Actor
{
public:
Spaceship(float shipangle,
Vector pos = Vector(0, 0),
Vector vel = Vector(0, 0),
Vector acc = Vector(0, 0));
virtual ~Spaceship();
public:
virtual bool IsA(const char* classname)
{
return !strcmp(classname, "Spaceship") || Actor::IsA(classname);
}
virtual void Think(float deltatime);
virtual void Render();
public:
void SetThrust(bool forward, bool backward);
void SetTurn(bool turnleft, bool turnright);
void SetFiring(bool firing);
private:
bool turnleft;
bool turnright;
bool moveforward;
bool movebackward;
bool firing;
float shipangle;
};
Spaceship::Spaceship(float shipangle, Vector pos, Vector vel, Vector acc)
{
this->shipangle = shipangle;
this->pos = pos;
this->vel = vel;
this->acc = acc;
turnleft = turnright = moveforward = movebackward = firing = false;
}
Spaceship::~Spaceship()
{
}
void Spaceship::Think(float deltatime)
{
for ( Object* obj = firstobject; obj; obj = obj->NextObj() )
{
if ( !obj->GCIsAlive() ) { continue; }
if ( !obj->IsA("Asteroid") ) { continue; }
Asteroid* ast = (Asteroid*) obj;
if ( !ast->InsideMe(pos) ) { continue; }
ast->OnHit();
pos.y = 16384 - pos.y;
break;
}
const float turnspeed = 100.0;
const float turnamount = turnspeed * deltatime;
if ( turnleft ) { shipangle -= DegreeToRadian(turnamount); }
if ( turnright ) { shipangle += DegreeToRadian(turnamount); }
float shipaccelamount = 15.0;
float shipaccel = 0.0;
if ( moveforward ) { shipaccel += shipaccelamount; }
if ( movebackward ) { shipaccel -= shipaccelamount; }
acc = Vector(shipaccel, 0.0).Rotate(shipangle);
float shipspeed = vel.Size();
float maxspeed = 50.0f;
if ( maxspeed < shipspeed ) { vel *= maxspeed / shipspeed; }
if ( firing )
{
float ttl = 8.0;
float speed = 120.0;
const Vector P3(16.0f, 0.0f);
Vector spawnpos = pos + P3.Rotate(shipangle) * 1.1;
Vector spawnvel = Vector(speed, 0.0).Rotate(shipangle);
new Missile(spawnpos, spawnvel, ttl);
}
}
void Spaceship::Render()
{
Vector screenpos = pos - screenoff;
// TODO: Ideally these should be global constants, but global constructors
// are _not_ called upon process initiazation on Sortix yet.
const Vector P1(-8.0f, 8.0f);
const Vector P2(-8.0f, -8.0f);
const Vector P3(16.0f, 0.0f);
Vector p1 = P1.Rotate(shipangle) + screenpos;
Vector p2 = P2.Rotate(shipangle) + screenpos;
Vector p3 = P3.Rotate(shipangle) + screenpos;
uint32_t shipcolor = MakeColor(255, 255, 255);
DrawLine(shipcolor, p1.x, p1.y, p2.x, p2.y);
DrawLine(shipcolor, p2.x, p2.y, p3.x, p3.y);
DrawLine(shipcolor, p1.x, p1.y, p3.x, p3.y);
}
void Spaceship::SetThrust(bool forward, bool backward)
{
this->moveforward = forward;
this->movebackward = backward;
}
void Spaceship::SetTurn(bool turnleft, bool turnright)
{
this->turnleft = turnleft;
this->turnright = turnright;
}
void Spaceship::SetFiring(bool firing)
{
this->firing = firing;
}
uintmax_t lastframeat;
void GameLogic()
{
uintmax_t now;
do uptime(&now);
while ( now == lastframeat);
unsigned long deltausecs = now - lastframeat;
lastframeat = now;
float deltatime = deltausecs / 1000000.0f;
float timescale = 3.0;
deltatime *= timescale;
Object* first = firstobject;
Object* obj;
for ( obj = first; obj; obj = obj->NextObj() ) { obj->GCBirth(); }
playership->SetThrust(keysdown[KBKEY_UP], keysdown[KBKEY_DOWN]);
playership->SetTurn(keysdown[KBKEY_LEFT], keysdown[KBKEY_RIGHT]);
playership->SetFiring(keysdown[KBKEY_SPACE]);
keysdown[KBKEY_SPACE] = false;
for ( obj = first; obj; obj = obj->NextObj() )
{
if ( !obj->GCIsBorn() ) { continue; }
obj->OnFrame(deltatime);
}
for ( obj = first; obj; )
{
Object* todelete = obj;
obj = obj->NextObj();
if ( !todelete->GCIsDead() ) { continue; }
delete todelete;
}
}
void Render()
{
screenoff = playership->pos - Vector(xres/2.0, yres/2.0);
size_t staroffx = (size_t) screenoff.x;
size_t staroffy = (size_t) screenoff.y;
for ( size_t y = 0; y < yres; y++ )
{
uint32_t* line = buf + y * linesize;
for ( size_t x = 0; x < xres; x++ )
{
size_t fieldx = (x+staroffx) % STARFIELD_WIDTH;
size_t fieldy = (y+staroffy) % STARFIELD_HEIGHT;
size_t fieldindex = fieldy * STARFIELD_HEIGHT + fieldx;
line[x] = starfield[fieldindex];
}
}
for ( Object* obj = firstobject; obj; obj = obj->NextObj() )
{
obj->Render();
}
}
void FlushBuffer()
{
lseek(fb, 0, SEEK_SET);
if ( writeall(fb, buf, framesize) < framesize )
{
#ifndef HACK_DONT_CHECK_FB
error(1, errno, "writing to framebuffer");
#endif
}
}
void RunFrame()
{
FetchKeyboardInput();
GameLogic();
Render();
FlushBuffer();
}
char* GetCurrentVideoMode()
{
FILE* fp = fopen("/dev/video/mode", "r");
if ( !fp ) { return NULL; }
char* mode = NULL;
size_t n = 0;
getline(&mode, &n, fp);
fclose(fp);
return mode;
}
// TODO: This should be in libc and not use libmaxsi.
#include <libmaxsi/platform.h>
#include <libmaxsi/error.h>
#include <libmaxsi/string.h>
using namespace Maxsi;
bool ReadParamString(const char* str, ...)
{
if ( String::Seek(str, '\n') ) { Error::Set(EINVAL); }
const char* keyname;
va_list args;
while ( *str )
{
size_t varlen = String::Reject(str, ",");
if ( !varlen ) { str++; continue; }
size_t namelen = String::Reject(str, "=");
if ( !namelen ) { Error::Set(EINVAL); goto cleanup; }
if ( !str[namelen] ) { Error::Set(EINVAL); goto cleanup; }
if ( varlen < namelen ) { Error::Set(EINVAL); goto cleanup; }
size_t valuelen = varlen - 1 /*=*/ - namelen;
char* name = String::Substring(str, 0, namelen);
if ( !name ) { goto cleanup; }
char* value = String::Substring(str, namelen+1, valuelen);
if ( !value ) { delete[] name; goto cleanup; }
va_start(args, str);
while ( (keyname = va_arg(args, const char*)) )
{
char** nameptr = va_arg(args, char**);
if ( String::Compare(keyname, name) ) { continue; }
*nameptr = value;
break;
}
va_end(args);
if ( !keyname ) { delete[] value; }
delete[] name;
str += varlen;
str += String::Accept(str, ",");
}
return true;
cleanup:
va_start(args, str);
while ( (keyname = va_arg(args, const char*)) )
{
char** nameptr = va_arg(args, char**);
delete[] *nameptr; *nameptr = NULL;
}
va_end(args);
return false;
}
int atoi_safe(const char* str)
{
if ( !str ) { return 0; }
return atoi(str);
}
void InitGame()
{
uptime(&lastframeat);
GenerateStarfield(starfield, STARFIELD_WIDTH, STARFIELD_HEIGHT);
playership = new Spaceship(0.0, Vector(0, 0), Vector(4.0f, 0));
new AsteroidField;
}
int main(int argc, char* argv[])
{
#ifndef HACK_DONT_CHECK_FB
char* vidmode = GetCurrentVideoMode();
if ( !vidmode ) { perror("Cannot detect current video mode"); exit(1); }
char* widthstr = NULL;
char* heightstr = NULL;
if ( !ReadParamString(vidmode, "width", &widthstr, "height", &heightstr, NULL) )
{
error(1, errno, "Can't parse video mode: %s", vidmode);
}
xres = atoi_safe(widthstr); delete[] widthstr; widthstr = NULL;
yres = atoi_safe(heightstr); delete[] heightstr; heightstr = NULL;
if ( !xres || !yres )
{
const char* chvideomode = "chvideomode";
execlp(chvideomode, chvideomode, argv[0], NULL);
perror(chvideomode);
exit(127);
}
#else
xres = 1280;
yres = 720;
#endif
fb = open("/dev/video/fb", O_WRONLY);
#ifndef HACK_DONT_CHECK_FB
if ( fb < 0 ) { error(1, errno, "open: /dev/video/fb"); }
#endif
bpp = sizeof(uint32_t);
linesize = xres;
framesize = yres * linesize * bpp;
buf = new uint32_t[framesize / sizeof(uint32_t)];
InitGame();
gamerunning = true;
for ( framenum = 0; gamerunning; framenum++ )
{
RunFrame();
}
close(fb);
return 0;
}