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