Skip to content

Instantly share code, notes, and snippets.

@JohnEarnest
Created January 8, 2013 01:19
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JohnEarnest/4480242 to your computer and use it in GitHub Desktop.
Save JohnEarnest/4480242 to your computer and use it in GitHub Desktop.
A prototype game exploring the idea of a bullet hell space-shooter in which the player takes an entirely defensive role and must protect others in addition to themselves.
/**
* Stop
*
* A prototype game exploring the idea of a
* bullet hell space-shooter in which the
* player takes an entirely defensive role
* and must protect others in addition to
* themselves.
*
* Known issues/ things to ponder:
*
* - What is your motivation for NOT having the shield up at all times?
* - make it run out of juice while taking hits/ recharge while retracted?
* - make the player more mobile somehow when not shielding?
* - tried keeping shield angle fixed while deployed- testers didn't like it.
* - Why/How does the player empathize with the things they are saving?
*
* TODO:
*
* - enlarge wave counter for constrast with timer
* - periodically spawn more people to defend
* - indicate oncoming enemies?
*
**/
import java.util.*;
import java.util.List; // awt is dumb.
import java.awt.*;
import java.awt.geom.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;
// just for the screen capture feature:
import javax.imageio.*;
import java.io.*;
public class Stop extends JPanel implements KeyListener {
/**
* Rendering engine bits and utilities:
**/
private static final int WIDTH = 160;
private static final int HEIGHT = 120;
private static final int GOAL_FPS = 50;
static final BufferedImage buff = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB);
public static void main(String[] args) {
JFrame window = new JFrame("Stop");
Stop app = new Stop();
app.setPreferredSize(new Dimension(320*3, 240*3));
window.setCursor(window.getToolkit().createCustomCursor(
new BufferedImage(1,1, BufferedImage.TYPE_INT_ARGB),
new Point(0, 0),
"invisible"
));
window.addKeyListener(app);
window.add(app);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setResizable(false);
window.pack();
window.setVisible(true);
app.newgame();
while(true) {
long start = System.nanoTime();
synchronized(buff) { app.tick(); }
app.repaint();
long total = System.nanoTime() - start;
if ((total / 1000000) < (1000 / GOAL_FPS)) {
try { Thread.sleep((1000 / GOAL_FPS) - (total / 1000000)); }
catch(InterruptedException e) {}
}
}
}
public void keyTyped(KeyEvent e) {}
public void keyPressed(KeyEvent e) {}
private int screenshot = 0;
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_S) {
synchronized(buff) {
try { ImageIO.write(buff, "PNG", new File("shot"+(screenshot++)+".png")); }
catch(IOException ioe) {}
}
}
}
public void paint(Graphics g) {
synchronized(buff) {
draw((Graphics2D)buff.getGraphics());
g.drawImage(
buff,
0, 0, getWidth(), getHeight(),
0, 0, WIDTH, HEIGHT,
this
);
}
}
void outline(Graphics2D g, Shape s) {
g.setColor(Color.WHITE);
g.fill(s);
g.setColor(Color.BLACK);
g.draw(s);
}
private double mx(MouseEvent e) { return ((double)e.getX()) / (getWidth() / WIDTH ); }
private double my(MouseEvent e) { return ((double)e.getY()) / (getHeight() / HEIGHT); }
private double dist(double x1, double y1, double x2, double y2) {
double dx = x1 - x2;
double dy = y1 - y2;
return Math.sqrt((dx*dx) + (dy*dy));
}
private double dist(Entity a, Entity b) {
return dist(a.x, a.y, b.x, b.y);
}
private <E extends Entity> E closest(List<E> sources, Entity target) {
E best = sources.get(0);
double bestd = Double.POSITIVE_INFINITY;
for(E e : sources) {
double dist = dist(target, e);
if (dist < bestd) {
bestd = dist;
best = e;
}
}
return best;
}
private Shape trans(GeneralPath p, double x, double y, double r, double s) {
AffineTransform a = new AffineTransform();
a.translate(x, y);
a.rotate(r);
a.scale(s, s);
return p.createTransformedShape(a);
}
private void tickAndCull(Collection<? extends Entity> entities, boolean boundCheck) {
Set<Entity> killed = new HashSet<Entity>();
for(Entity e : entities) {
e.tick();
if (boundCheck && (
(e.x < -10) || (e.x > WIDTH + 10) ||
(e.y < -10) || (e.y > HEIGHT + 10))) {
e.dead = true;
}
if (e.dead) {
killed.add(e);
}
}
entities.removeAll(killed);
}
private void drawNumber(Graphics g, int x, int y, long num) {
do {
g.drawImage(Data.nums[(int)(num % 10)], x, y, null);
x -= 4;
num /= 10;
} while(num > 0);
}
abstract class Entity implements MouseListener, MouseMotionListener {
public void mouseEntered (MouseEvent e) {}
public void mouseExited (MouseEvent e) {}
public void mouseClicked (MouseEvent e) {}
public void mousePressed (MouseEvent e) { mouseDown(e.getButton() == MouseEvent.BUTTON1); }
public void mouseReleased(MouseEvent e) { mouseUp (e.getButton() == MouseEvent.BUTTON1); }
public void mouseDragged (MouseEvent e) { mouseMoved(mx(e), my(e)); }
public void mouseMoved (MouseEvent e) { mouseMoved(mx(e), my(e)); }
void mouseDown(boolean primary) {}
void mouseUp (boolean primary) {}
void mouseMoved(double x, double y) {}
boolean collide() { return false; }
void move(double dx, double dy) {
double dist = dist(0, 0, dx, dy);
int steps = (int)(dist / 2);
if (dist < .001) { return; }
double ux = dx / steps;
double uy = dy / steps;
// do a zig-zag linear interpolation:
int ax = steps;
int ay = steps;
while(!dead && (ax > 0 || ay > 0)) {
if (ax > 0) {
x += ux;
if (collide()) { ax=0; x-= ux; }
else { ax--; }
}
if (ay > 0) {
y += uy;
if (collide()) { ay=0; y-= uy; }
else { ay--; }
}
}
}
void moveAngle(double angle, double mag) {
move(mag * Math.cos(angle), mag * Math.sin(angle));
}
double face(Entity e) { return Math.atan2(e.y - y, e.x - x); }
double skew(double width) { return Math.random() * width - (width / 2); }
double x;
double y;
boolean dead = false;
abstract void tick();
abstract void draw(Graphics2D g);
}
/**
* Game logic and global state:
**/
int mode = 0;
Ship player = null;
Shape shield = null;
boolean hit = false;
int hits = 30;
long time = 0;
int waveno = 0;
Random sequence = null;
List<Bullet> bullets = new ArrayList<Bullet>();
List<Particle> particles = new ArrayList<Particle>();
List<Enemy> enemies = new ArrayList<Enemy>();
List<Person> people = new ArrayList<Person>();
List<Shape> walls = new ArrayList<Shape>(); {
walls.add(new Rectangle(WIDTH / 4 * 1 - 8, HEIGHT / 4 * 1 - 8, 16, 16));
walls.add(new Rectangle(WIDTH / 4 * 3 - 8, HEIGHT / 4 * 1 - 8, 16, 16));
walls.add(new Rectangle(WIDTH / 4 * 1 - 8, HEIGHT / 4 * 3 - 8, 16, 16));
walls.add(new Rectangle(WIDTH / 4 * 3 - 8, HEIGHT / 4 * 3 - 8, 16, 16));
}
void newgame() {
mode = 0;
shield = null;
hit = false;
hits = 30;
time = 0;
waveno = 0;
bullets .clear();
particles.clear();
enemies .clear();
people .clear();
people.add(new Person( 50, 50));
people.add(new Person(100, 50));
sequence = new Random(0xDEFACED);
if (player != null) {
removeMouseListener(player);
removeMouseListener(player);
}
player = new Ship();
addMouseListener(player);
addMouseMotionListener(player);
}
void spawn(int type) {
switch(type) {
case 0 : enemies.add(new Hex()); break;
case 1 : enemies.add(new Square(100)); break;
case 2 : enemies.add(new Square(200)); break;
case 3 : enemies.add(new Square(300)); break;
case 4 : enemies.add(new Pent(0)); break;
case 5 : enemies.add(new Pent(1)); break;
case 6 : enemies.add(new Pent(2)); break;
case 7 : enemies.add(new Pent(3)); break;
}
}
void spawnWave() {
while(enemies.size() < (waveno / 5) + 1) {
switch(sequence.nextInt(19)) {
case 0 : spawn(0); break;
case 1 : spawn(1); break;
case 2 : spawn(1); spawn(2); break;
case 3 : spawn(1); spawn(2); spawn(3); break;
case 4 : spawn(4); break;
case 5 : spawn(5); break;
case 6 : spawn(6); break;
case 7 : spawn(7); break;
case 8 : spawn(4); spawn(4); break;
case 9 : spawn(5); spawn(5); break;
case 10 : spawn(6); spawn(6); break;
case 11 : spawn(7); spawn(7); break;
case 12 : spawn(0); spawn(0); break;
case 13 : spawn(0); spawn(2); spawn(3); break;
case 14 : spawn(4); spawn(4); break;
case 15 : spawn(5); spawn(5); break;
case 16 : spawn(6); spawn(6); break;
case 17 : spawn(7); spawn(7); break;
case 18 : spawn(0); break;
}
}
waveno++;
}
void tick() {
if (mode == 0) {
player.tick();
for(Person p : people) { p.tick(); }
tickAndCull(particles, true);
tickAndCull(bullets, true);
tickAndCull(enemies, false);
if (hit == true) { hits--; }
if (hits < 1) { mode = 50; }
if (enemies.size() < 1) { spawnWave(); }
time++;
}
else {
if (mode > 1) { mode--; }
}
}
void draw(Graphics2D g) {
g.setColor(Color.WHITE);
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(Color.BLACK);
{
Graphics2D t = (Graphics2D)g.create();
t.setPaint(Data.diagonal);
for(Shape s : walls) { t.fill(s); }
}
for(Person p : people ) { p.draw(g); }
for(Bullet b : bullets) { b.draw(g); }
g.setXORMode(Color.WHITE);
for(Particle p : particles) { p.draw(g); }
g.setPaintMode();
for(Enemy e : enemies) { e.draw(g); }
player.draw((Graphics2D)g.create());
g.setXORMode(Color.WHITE);
g.setColor(Color.BLACK);
for(int z = 0; z < hits; z++) {
g.fill(new Rectangle(
(int)((WIDTH / 2) - (hits / 2 * 3) + (z * 3)),
(int)(HEIGHT - 4),
1,
3
));
}
drawNumber(g, WIDTH - 4, 1, time);
drawNumber(g, WIDTH - 4, 6, waveno);
if (hit) {
g.fillRect(0, 0, getWidth(), getHeight());
hit = false;
}
}
/**
* Game entities:
**/
class Ship extends Entity {
Shape bounds = Data.arrow;
Muscle shieldup = new Muscle(0.1, 1.0, 1.5);
Muscle gather = new Muscle(1.0, 1.2, 1.0);
Person picked = null;
boolean hit = false;
double dir;
double ox, oy;
Ship() {
x = -100;
y = -100;
}
void tick() {
shieldup.step(0.1);
gather .step(0.1);
if (gather.active()) {
if (picked == null) {
picked = closest(people, this);
if (dist(picked, this) < 50) {
picked.pick();
}
else {
picked = null;
}
}
else {
double dist = dist(picked, this);
if (dist > 10) {
picked.move(
(x - picked.x) * .125,
(y - picked.y) * .125
);
picked.dir = picked.face(player);
}
}
}
else {
if (picked != null) { picked.picked = false; }
picked = null;
}
}
void draw(Graphics2D g) {
g.setColor(Color.BLACK);
if (shieldup.active()) {
shield = trans(Data.shield, x, y, dir, shieldup.v());
if (hit) {
outline(g, shield);
hit = false;
}
else {
g.setXORMode(Color.WHITE);
g.fill(shield);
g.setPaintMode();
}
}
else {
shield = null;
}
bounds = trans(Data.arrow, x, y, dir, gather.v());
if (gather.active()) {
outline(g, bounds);
}
else {
g.fill(bounds);
}
}
void mouseDown(boolean p) {
if (mode == 0) {
(p ? shieldup : gather).forward = true;
}
else {
newgame();
}
}
void mouseUp (boolean p) {
if (mode == 0) {
(p ? shieldup : gather).forward = false;
}
}
void mouseMoved(double mx, double my) {
x = mx;
y = my;
if (dist(x, y, ox, oy) < 2) { return; }
dir = Math.atan2(y - oy, x - ox);
ox = (x + ox)/2;
oy = (y + oy)/2;
}
}
class Person extends Entity {
Shape bounds = Data.arrow;
Muscle breathe = new Muscle(0.7, 0.9, 3);
Muscle blink = new Muscle(0.9, 2.0, 1);
double dir;
boolean picked = false;
Person(double x, double y) {
this.x = x;
this.y = y;
}
void tick() {
if (blink.active() && blink.done()) {
blink.forward = false;
blink.i = 0;
}
blink.step(0.1);
if (breathe.done()) { breathe.forward = !breathe.forward; }
breathe.step(0.1);
for(Person p : people) {
if (p == this) { continue; }
if (dist(this, p) < 8) {
p.moveAngle(face(p), 2.5);
}
}
if (!picked) {
double goaldir = face(player);
if (dir < goaldir) { dir += .05; }
if (dir > goaldir) { dir -= .05; }
dir %= Math.PI * 2;
}
}
boolean collide() {
for(Shape s : walls) {
if (s.contains(x, y)) { return true; }
}
return (x <= 5) || (x >= WIDTH-5) || (y <= 5) || (y >= HEIGHT-5);
}
void draw(Graphics2D g) {
if (blink.active()) {
outline(g, trans(Data.arrow, x, y, dir, blink.v()));
}
bounds = trans(Data.arrow, x, y, dir, breathe.v());
outline(g, bounds);
}
void pick() {
picked = true;
blink .forward = true;
breathe.forward = false;
}
}
class Bullet extends Entity {
double a; // angle
double v; // velocity (pixels/tick)
Bullet(double x, double y, double a, double v) {
this.x = x;
this.y = y;
this.a = a;
this.v = v;
}
boolean collide() {
if (shield != null && shield.contains(x, y)) {
player.hit = true;
player.shieldup.step(-.05);
dead = true;
return true;
}
for(Shape s : walls) {
if (s.contains(x, y)) {
dead = true;
return true;
}
}
for(Person p : people) {
if (p.bounds.contains(x, y)) {
p.moveAngle(a + skew(Math.PI / 4), 1.5 * v);
p.blink.forward = true;
hit = true;
dead = true;
return true;
}
}
if (player.bounds.contains(x, y)) {
hit = true;
dead = true;
return true;
}
return false;
}
void tick() {
moveAngle(a, v);
if (dead) {
if (Math.random() > .9) {
dead = false;
v = -v;
a += skew(Math.PI / 8);
}
else {
for(int z = (int)(5 * Math.random()); z >= 0; z--) {
particles.add(new Particle(
x + 2 * Math.random() - 1,
y + 2 * Math.random() - 1,
a + skew(Math.PI / 4),
-(v * 0.50 + (0.50 * Math.random())),
5
));
}
}
}
}
void draw(Graphics2D g) {
g.setColor(Color.BLACK);
g.drawLine(
(int)x,
(int)y,
(int)(-v * Math.cos(a) + x),
(int)(-v * Math.sin(a) + y)
);
}
}
class Particle extends Bullet {
int timer;
Particle(double x, double y, double a, double v, int t) {
super(x, y, a, v);
this.timer = t;
}
boolean collide() {
return false;
}
void tick() {
moveAngle(a, v);
timer--;
dead = timer < 1;
}
}
abstract class Enemy extends Entity {
Muscle fire = new Muscle(0.8, 1.2, 1);
double dir;
double gx;
double gy;
Enemy() {
dir = Math.random() * 2 * Math.PI;
pickOrigin();
this.x = gx;
this.y = gy;
pickGoal();
}
void pickOrigin() {
double ang = Math.random() * 2 * Math.PI;
gx = (WIDTH / 2) + (WIDTH * Math.cos(ang));
gy = (HEIGHT / 2) + (HEIGHT * Math.sin(ang));
}
void pickGoal() {
gx = Math.random() * (WIDTH * .8) + (WIDTH * .1);
gy = Math.random() * (HEIGHT * .8) + (HEIGHT * .1);
}
void tick() {
fire.step(.1);
if (fire.done() && fire.forward) { fire.forward = false; }
if (dist(x, y, gx, gy) < 10) { pickGoal(); }
x += (gx - x) * .0125;
y += (gy - y) * .0125;
}
void draw(Graphics2D g, GeneralPath s, boolean blink) {
Shape t = trans(s, x, y, dir, fire.v());
if (blink) { outline(g, t); }
else { g.fill(trans(s, x, y, dir, fire.v())); }
}
}
class Square extends Enemy {
double vx;
double vy;
Square(double dist) {
dir = Math.random() * 2 * Math.PI;
this.x = (WIDTH / 2) - dist * Math.cos(dir);
this.y = (HEIGHT / 2) - dist * Math.sin(dir);
this.gx = (WIDTH / 2) + dist * Math.cos(dir);
this.gy = (HEIGHT / 2) + dist * Math.sin(dir);
this.vx = 1.2 * Math.cos(dir);
this.vy = 1.2 * Math.sin(dir);
}
void tick() {
fire.step(.1);
if (fire.done() && fire.forward) { fire.forward = false; }
if (Math.random() > .95) {
bullets.add(new Bullet(x, y, face(player), 2.2));
fire.forward = true;
}
dir += .3;
x += vx;
y += vy;
if (dist(x, y, gx, gy) < 10) { dead = true; }
}
void draw(Graphics2D g) {
draw(g, Data.square, true);
}
}
class Pent extends Enemy {
int timer = 80;
int pattern;
// pattern 0 - fireburst
// pattern 1 - cone
// pattern 2 - spiral
// pattern 3 - double spiral
Pent(int pattern) {
fire.a = 1.0;
fire.b = 0.5;
this.pattern = pattern;
}
void explode() {
for(int z = 0; z < 100; z++) {
bullets.add(new Bullet(x, y, Math.PI * 2 / 100 * z, 3.0));
}
}
void cone() {
double target = face(closest(people, this));
for(int z = -10; z < 10; z++) {
bullets.add(new Bullet(x, y, target + (z * Math.PI / 40), 2.3));
bullets.add(new Bullet(x, y, target + (z * Math.PI / 40), 2.5));
bullets.add(new Bullet(x, y, target + (z * Math.PI / 40), 3.0));
}
}
void fire() {
bullets.add(new Bullet(x, y, dir, 2.5));
if (pattern == 3) {
bullets.add(new Bullet(x, y, dir + Math.PI, 2.5));
}
}
void tick() {
if (dist(x, y, gx, gy) < 10) {
dir += .05;
if (pattern == 2 || pattern == 3) { fire(); }
if (timer < 20) {
dir += .05;
}
if (timer < 10) {
fire.forward = true;
fire.step(.1);
}
if (timer < 1) {
if (pattern == 0) { explode(); }
if (pattern == 1) { cone(); }
dead = true;
}
else { timer--; }
}
else {
super.tick();
}
}
void draw(Graphics2D g) {
boolean blink = ((timer < 40) ? (timer / 2) : (timer / 4)) % 2 == 0;
draw(g, Data.pent, blink);
}
}
class Hex extends Enemy {
int hops = 6;
void pickGoal() {
super.pickGoal();
hops--;
if (hops == 1) {
pickOrigin();
}
if (hops == 0) {
dead = true;
}
}
void tick() {
super.tick();
Person p = closest(people, this);
if (Math.random() > .8) {
bullets.add(new Bullet(
x, y,
face(p) + skew(Math.PI / 8),
4.0
));
fire.forward = true;
}
dir += .05;
}
void draw(Graphics2D g) {
draw(g, Data.hex, true);
}
}
}
/**
* Game data:
**/
class Data {
static GeneralPath ngon(int sides, double radius) {
Polygon p = new Polygon();
for(int z = 0; z < sides; z++) {
p.addPoint(
(int)(radius * Math.cos(z * Math.PI * 2 / sides)),
(int)(radius * Math.sin(z * Math.PI * 2 / sides))
);
}
return new GeneralPath(p);
}
static BufferedImage render(int w, int h, int... data) {
BufferedImage ret = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics g = ret.getGraphics();
for(int z = 0; z < w*h; z++) {
g.setColor(data[z] == 0 ? Color.WHITE : Color.BLACK);
g.drawLine(z%w,z/w,z%w,z/w);
}
return ret;
}
static final GeneralPath arrow = new GeneralPath(new Polygon(
new int[] { 4,-4,-4},
new int[] { 0, 3,-3},
3
));
static final GeneralPath hex = ngon(6, 7.0);
static final GeneralPath pent = ngon(5, 6.5);
static final GeneralPath square = ngon(4, 5.0);
static final GeneralPath shield; static {
Polygon p = new Polygon();
for(int z = -5; z <= 5; z++) {
p.addPoint(
(int)(16 * Math.cos(z * Math.PI/16)),
(int)(16 * Math.sin(z * Math.PI/16))
);
}
for(int z = 5; z >= -5; z--) {
p.addPoint(
(int)(12 * Math.cos(z * Math.PI/16)),
(int)(12 * Math.sin(z * Math.PI/16))
);
}
shield = new GeneralPath(p);
}
static final Image[] nums = {
render(3,4, 1,1,1, 1,0,1, 1,0,1, 1,1,1 ),
render(3,4, 0,1,0, 0,1,0, 0,1,0, 0,1,0 ),
render(3,4, 1,1,1, 0,0,1, 0,1,0, 1,1,1 ),
render(3,4, 1,1,1, 0,1,1, 0,0,1, 1,1,1 ),
render(3,4, 1,0,1, 1,0,1, 1,1,1, 0,0,1 ),
render(3,4, 1,1,1, 1,1,0, 0,0,1, 1,1,0 ),
render(3,4, 1,0,0, 1,1,1, 1,0,1, 1,1,1 ),
render(3,4, 1,1,1, 0,0,1, 0,1,0, 0,1,0 ),
render(3,4, 1,1,1, 1,0,1, 1,1,1, 1,1,1 ),
render(3,4, 1,1,1, 1,0,1, 1,1,1, 0,0,1 ),
};
static final Paint diagonal = new TexturePaint(
render(4,4, 1,0,0,1, 1,1,0,0, 0,1,1,0, 0,0,1,1 ),
new Rectangle(4,4)
);
}
class Muscle {
double a; // start position
double b; // end position
double t; // tween duration
double i; // tween index
boolean forward = false; // moving forward?
Muscle(double start, double end, double time) {
a = start;
b = end;
t = time;
}
void step(double time) {
i = forward ? (i + time) : (i - time);
i = Math.max(i, 0);
i = Math.min(i, t);
}
boolean active() {
return !done() || forward;
}
boolean done() {
return forward ? (i == t) : (i == 0);
}
double v() {
return a * tween(1 - (i/t)) +
b * tween( (i/t));
}
double tween(double index) {
// a hand-tuned sigmoid eased tween:
return 1/(1 + Math.pow(Math.E, -12*(index-.5)));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment