sortix-mirror/games/asteroids.cpp

842 lines
18 KiB
C++
Raw Normal View History

#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;
}