By the way in SpaceClasses.pde (I suppose, this is main file) use "size(400,400,P2d);"
instead of "size(400,400);"
/*
Body class
by blindfish
www.blindfish.co.uk
november 2009
Handles generics of bodies that might float in space - i.e. position velocity etc.
You can set gravityAffected or frictionAffected to false if you don't want an inheriting
class to be affected by these.
Note that all bodies are registered with Space. That way there's a central location to do
stuff like collision detection etc. and send messages back to the bodies.
See ActionScript Animation by Keith Peters for a great reference on scripted animation and
the simplified physics used here.
*/
public class Body {
float x;
float y;
float vx;
float vy;
boolean gravityAffected;
boolean frictionAffected;
Space space;
Body(float x, float y, Space space) {
this.x = x;
this.y = y;
this.vx = 0;
this.vy = 0;
this.space = space;
this.gravityAffected = true;
this.frictionAffected = true;
}
Body(float x, float y, float vx, float vy, Space space) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.space = space;
this.gravityAffected = true;
this.frictionAffected = true;
}
void move() {
if(space.playing) {
if(gravityAffected){
vy += space.gravity;
}
if(frictionAffected){
vx *= space.friction;
vy *= space.friction;
}
x += vx;
y += vy;
checkConstraints();
}
}
void checkConstraints() {
//constraints
if(x<0) {
x = width;
}
else if(x>width) {
x = 0;
}
if(y<0){
y=height;
}
else if (y>height) {
y=0;
}
}
}
/*
Bullet class
by blindfish
www.blindfish.co.uk
november 2009
Could maybe have re-used (i.e. extended) Particle here...
*/
public class Bullet extends Body {
float maxLife;
int life;
int speed;
// Should probably provide an overloaded constructor to allow setting of things like speed, life etc.
Bullet(float x, float y, float rotation, Space space) {
super(x,y,space);
maxLife = 45; // governs the distance a bullet can travel
life = (int)maxLife;
speed = 6;
float angle = radians(rotation);
vx = cos(angle) * speed;
vy = sin(angle) * speed;
frictionAffected = false;
registerDraw(this);
}
void draw() {
move();
if (life > 0) {
int g = (int)((255/maxLife) * life);
life --;
noStroke();
rectMode(CENTER);
fill(0,g,0);
rect(x,y,2,2);
}
else {
removeBullet();
}
}
// Could there be a utility method in Space to handle this?
void removeBullet() {
unregisterDraw(this);
int index = space.bullets.indexOf(this);
space.bullets.remove(index);
}
}
/*
Particle class
by blindfish
www.blindfish.co.uk
november 2009
A rather crude system. In theory I'm sure it would be much better
to keep an ArrayList of particle objects to be re-used as and when
necessary; rather than the rather innefficient method of removing
old particles and creating new ones as and when necessary adopted here...
*/
public class Particle extends Body {
int decay;
int decayFade;
int pAlpha;
int pSize;
color colour;
Particle(float x, float y, int decay, Space space) {
super(x,y,space);
colour = #00ff00;
this.vx = random(2)-1;
this.vy = random(2)-1;
this.decay = decay;
this.decayFade = 255/decay;
this.pAlpha = 255;
this.pSize = 1;
registerDraw(this);
}
Particle(float x, float y, int decay, float rotation, Space space) {
super(x,y,space);
colour = #00ff00;
float angle = radians(rotation);
vx = (cos(angle) * -3) + random(0.4)-0.2;
vy = (sin(angle) * -3) + random(0.4)-0.2;
this.decay = decay;
this.decayFade = 255/decay;
this.pAlpha = 255;
this.pSize = 1;
registerDraw(this);
}
Particle(float x, float y, int decay, int pSize, Space space) {
super(x,y,space);
colour = #00ff00;
this.vx = random(2)-1;
this.vy = random(2)-1;
this.decay = decay;
this.decayFade = 255/decay;
this.pAlpha = 255;
this.pSize = pSize;
registerDraw(this);
}
void draw() {
move();
if(space.playing){
if(decay>0){
noStroke();
rectMode(CENTER);
fill(0,pAlpha,0);
pAlpha -= decayFade;
rect(x,y,pSize,pSize);
decay --;
}
else {
remove();
}
}
}
// I'm sure it would be much more efficient to keep a store of particle objects
// and re-use as necessary; thereby avoiding the removal an addition of lots of objects...
void remove() {
unregisterDraw(this);
int index = space.particles.indexOf(this);
space.particles.remove(index);
}
}
/*
Ship class
by blindfish
www.blindfish.co.uk
november 2009
Since development was rather fluid I still need to do more work to make this generic.
For instance it may be that you prefer to allocate 'lives' and explode the ship each
time it is hit, rather than use the 'shields' approach used here.
I'm also wondering whether I should have added ship specific key handling here rather
than in space; though there is some logic of doing it all in one place to avoid conflicts.
*/
public class Ship extends Body {
float rotation; // current rotation of ship
float vr; // speed at which rotation takes place (maybe adjust as ship upgrades)
float thrust; // Again could adjust with upgrades...
float reverseThrust;
int coolingRate;
int cooling; // creates a delay between shots being fired.
// for key handling
boolean left;
boolean right;
boolean up;
boolean down;
boolean fire;
boolean shieldHit;
int shieldRadius;
int shieldRecharge;
int shieldRechargeRate;
int shields;
color colour = #00ff00;
// positions to send jet and bullets from
PVector base;
PVector tip;
Ship(float x, float y, Space space) {
super(x,y,0,0,space);
registerDraw(this);
init();
}
void init() {
x = width/2;
y = width/2;
vx = 0;
vy = 0;
vr = 2;
thrust = 0.05;
reverseThrust = 0.005;
coolingRate = 30;
cooling = coolingRate;
shields = 2;
shieldRadius = 26;
shieldRecharge = 60;
shieldRechargeRate = shieldRecharge;
rotation = -90;
left = false;
right = false;
up = false;
down = false;
fire = false;
}
void shipSetupOverride(float vr, float thrust, float reverseThrust, int collingRate) {
this.vr = vr;
this.thrust = thrust;
this.reverseThrust = reverseThrust;
this.coolingRate = collingRate;
}
void draw() {
handleKeys();
move();
// The points that make up the ship
tip = space.rotatePoint(10,0,rotation);
PVector leftCorner = space.rotatePoint(-10,-5,rotation);
PVector rightCorner = space.rotatePoint(-10,5,rotation);
base = space.rotatePoint(-5,0,rotation);
noFill();
strokeWeight(1);
stroke(colour);
if(shields >= 0){
beginShape();
vertex(x + tip.x,y + tip.y);
vertex(x + leftCorner.x, y + leftCorner.y);
vertex(x + base.x, y + base.y);
vertex(x + rightCorner.x, y + rightCorner.y);
vertex(x + tip.x,y + tip.y);
endShape();
}
if(shieldHit) {
if(shields>=0){
ellipseMode(CENTER);
stroke(colour,shieldRecharge*2);
ellipse(x,y,shieldRadius,shieldRadius);
}
else {
// unregisterDraw(this);
space.gameOver = true;
shieldHit = false;
}
// if shield has recharged
if(shieldRecharge<=0) {
shieldHit=false;
shieldRecharge = shieldRechargeRate;
}
shieldRecharge --;
}
// crude way to limit the rate of fire
if(cooling > 0) {
cooling --;
}
}// draw() ends...
// - - - - - - - - - - - - - - - - - - - - - - - - //
void shieldHit() {
// crude 'bounce'
vx *= -0.5;
vy *= -0.5;
shields --;
shieldHit = true;
}
void handleKeys() {
if(shields>-1){
if(up) {
float angle = radians(rotation);
float ax = cos(angle) * thrust;
float ay = sin(angle) * thrust;
vx += ax;
vy += ay;
jetStream();
}
else if(down) {
float angle = radians(rotation);
float ax = cos(angle) * reverseThrust;
float ay = sin(angle) * reverseThrust;
vx -= ax;
vy -= ay;
}
if(left) rotation -= vr;
if(right) rotation += vr;
if(fire) {
if(cooling <= 0){
cooling = coolingRate;
space.bullets.add(new Bullet(x,y,rotation,space));
}
}
}
}
// add some particles :)
void jetStream() {
for(int i= 0; i<3; i++) {
space.particles.add(new Particle(x+base.x,y+base.y,15,rotation,space));
}
}
}
/* Space class
by blindfish
www.blindfish.co.uk
november 2009
Space is where everything happens: that includes collision detection, key handling etc.
Since empty space would be a little boring, a ship is added automatically
when a Space object is added; but to make a game out of this you'd need to extend
Space and add containers for objects that the ship interacts with - e.g. aliens. You can
then iterate through these containers to perform collision detection etc.
For example with Asteroids I did the following:
public class AsteroidSpace extends Space {
ArrayList asteroids;
int numAsteroids;
AsteroidSpace() {
super();
this.asteroids = new ArrayList();
}
// etc...
}
Note that in order to register your bodies (e.g. aliens) with Space you then need to create a
variable of the extended type of space in the extended body:
public class Asteroid extends Body {
// PROPERTIES
AsteroidSpace space; // MUST override the type of space here since normal space doesn't contain asteroids... (seems like a hack to me)
// etc...
// CONSTRUCTORS
Asteroid(float x, float y, float vx, float vy, AsteroidSpace space) {
super(x,y,vx,vy,space);
this.space = space;
// etc...
}
// etc
}
That seems like an ugly hack to me, but it works :)
Note that you SHOULD NOT remove something directly from an ArrayList container when you're iterating over it in Space.
The approach I took was to find a way to set the Body as 'dead' and check against this in the Body's class
and remove the object from there (thinking about it I should perhaps have created a property and method specifically for this
purpose in Body). So for example here's the unoptimised collision detection for bullets from Asteroids:
void handleBulletCollisions() {
for(int i=0; i<bullets.size(); i++) {
// the problem happens here:
for(int j=0; j<asteroids.size(); j++) {
Bullet thisBullet = (Bullet) bullets.get(i);
Asteroid thisAsteroid = (Asteroid) asteroids.get(j);
float dx = thisBullet.x - thisAsteroid.x;
float dy = thisBullet.y - thisAsteroid.y;
float distSquared = dx*dx + dy*dy;
if(distSquared < (thisAsteroid.radius*thisAsteroid.radius)) {
// rather than directly removing the bullet we flag it to be removed:
thisBullet.life=0;
createExplosion(thisAsteroid.x,thisAsteroid.y,(int)(thisAsteroid.radius*0.75));
// the equivalent needs to be done when removing the asteroid... in this case in the explode method
thisAsteroid.explode();
score+=10;
}
}
}
}
*/
public class Space {
//PROPERTIES
float gravity;
float friction;
ArrayList bullets;
ArrayList particles;
Ship ship;
boolean playing;
boolean menu;
int score;
boolean gameOver;
int gameOverPause;
int pauseBetweenLevels;
PFont font;
// CONSTRUCTOR
Space() {
init();
}
Space(float gravity, float friction) {
this.gravity = gravity;
this.friction = 1 - friction;
init();
}
void init() {
registerDraw(this);
this.bullets = new ArrayList();
this.particles = new ArrayList();
ship = new Ship(width/2,height/2,this);
playing = false;
menu = true;
registerKeyEvent(this);
font = createFont("Arial", 18);
textFont(font);
}
void startGame() {
// useful for resetting variables to default values after previous game has finished
}
// METHODS
void draw() {
// smooth();
// noStroke();
// fill(0,45);
// rectMode(CORNER);
// rect(0,0,width,height); // this looks interesting :)
background(0); // but I suspect this performs better
if (playing) {
handleBulletCollisions();
handleShipCollision();
if(gameOver) {
if(gameOverPause == 240){
// would be good to get this into the ship class somehow...
createExplosion(ship.x,ship.y,30);
}
if(gameOverPause<0) {
space.playing = false;
space.menu = true;
gameOver = false;
}
else {
gameOverPause --;
}
}
else {
drawHUD();
}
checkForEndOfLevel();
}
else if (!menu) { // i.e. paused
drawPauseMenu();
}
else { // display menu
drawMainMenu();
}
}
// - - - - - - IN-GAME METHOD - - - - - - //
void handleBulletCollisions(){
// will need to be implemeted to check collisions with whatever can be shot...
}
void handleShipCollision() {
// will need to be implemeted to check collisions with whatever hit the ship...
}
// other collision methods may be necessary
void createExplosion(float x,float y, int numParticles) {
for(int i=0;i<numParticles; i++) {
particles.add(new Particle(x,y,45,2,this));
}
}
void checkForEndOfLevel(){
// e.g. are all aliens dead?
}
void gameOver() {
playing = false;
menu = true;
}
// - - - - - - MENUS - - - - - - //
void drawHUD() {
// e.g. lives, score etc...
}
void drawPauseMenu() {
fill(255);
textAlign(CENTER);
text("PAUSED", width/2,height/2);
}
void drawMainMenu() {
fill(255);
textAlign(CENTER);
//text("score: " + score, width/2, height/2-100);
text("press S to start", width/2,height/2-70);
}
// - - - - - - UTILITIES - - - - - - //
PVector rotatePoint(float x, float y, float rotation) {
PVector rotatedPoint = new PVector();
Float angle = radians(rotation);
rotatedPoint.x = cos(angle) * x - sin(angle) * y;
rotatedPoint.y = cos(angle) * y + sin(angle) * x;
return rotatedPoint;
}
void hide(Body body) {
unregisterDraw(body);
}
public void keyEvent(KeyEvent event){
if(playing) {
if(event.getID() == KeyEvent.KEY_PRESSED){
switch(event.getKeyCode()){
case KeyEvent.VK_UP:
if(!ship.up) ship.up = true;
break;
case KeyEvent.VK_DOWN:
if(!ship.down) ship.down = true;
break;
case KeyEvent.VK_LEFT:
if(!ship.left) ship.left = true;
break;
case KeyEvent.VK_RIGHT:
if(!ship.right) ship.right = true;
break;
case KeyEvent.VK_CONTROL:
case KeyEvent.VK_SPACE:
if(!ship.fire) ship.fire = true;
break;
}
}
else if(event.getID() == KeyEvent.KEY_RELEASED){
switch(event.getKeyCode()){
case KeyEvent.VK_UP:
if(ship.up)
ship.up = false;
break;
case KeyEvent.VK_DOWN:
if(ship.down)
ship.down = false;
break;
case KeyEvent.VK_LEFT:
if(ship.left)
ship.left = false;
break;
case KeyEvent.VK_RIGHT:
if(ship.right)
ship.right = false;
break;
case KeyEvent.VK_CONTROL:
case KeyEvent.VK_SPACE:
if(ship.fire) ship.fire = false;
break;
case KeyEvent.VK_P:
playing = false;
break;
}
}
}
// If paused...
else if (!playing && !menu) {
if(event.getID() == KeyEvent.KEY_RELEASED){
switch(event.getKeyCode()) {
case KeyEvent.VK_P:
playing = true;
break;
}
}
}
// menu controls
else {
if(event.getID() == KeyEvent.KEY_RELEASED){
switch(event.getKeyCode()) {
case KeyEvent.VK_S:
menu = false;
playing = true;
startGame();
break;
}
}
}
}
}
Space space;
void setup() {
size(400,400);
space = new Space(0,0);
// space = new Space(0.075,0.025); // with added gravity and friction
// space.ship.shipSetupOverride(3, 0.3, 0.05, 15); // override defaults to make ship navigable with gravity and friction... and speeded up rate of fire
}
void draw() {
// nothing to see here...
// I got a bit carried away implementing everything, including key-handling, in the classes.
// I suspect it would have been better/simpler to leave some things here...
// in the meantime Space is probably where you should start looking ;)
}
A set of classes I used to create an Asteroids game. Far from perfect but in theory could be re-used to make space-based games...
Arrow keys for control. CTRL or SPACE to fire, P to pause... But you'll need to add some aliens to make it interesting ;)