Source code for Mike Hall and Rudy Rucker's Asteroids
Alive Applet. |
/************************************************************************************************
Asteroids.java
Original code by Mike Hall, www.brainjar.com, 1998.
Altered by Rudy Rucker, rucker@mathcs.sjsu.edu, 1999.
Code last revised December 23, 1999, by Rudy Rucker.
This is the code for a combined Application and Applet named Asteroids.class.
The applet code is pure Java 1.0, except for some optional code for loading
sounds from a jar file in the Asteroids loudSound function. Also we have
to switch between using "inside" or "contains" Polygon metods. We
build the
1.0 multiple files version and the 1.1 jar file version for Web deployment.
An annoying thing about Java's lack of #ifdef is that this means we have to
manually comment some code in and out. The Java 1.0 "method not found"
errors or the Java 1.1 "deprecated" warnings will show you where, but they
won't alert you to change the Asteroids loadSound code. The key things
you have to put back in for 1.1 are (a) fix the version number in the JDK
class, (b) comment in the "contains" code in the isTouching method, (c)
comment in the jar soundloading code.
Minimal web pages for the multiple file and jar file version are,
respectively,
<applet code="Asteroids.class" width=w height=h></applet>
<applet archive="Asteroids.jar" code="Asteroids.class" width=w
height=h></applet>
In order to make a jar file, run this command line in a directory where
your class and au files are present. Double-check that all au file names
have the same spellings that you use in loadSounds.
jar cf Asteroids.jar *.class *.au
All the application code is manually commented out for the applet build.
The application code consists of the Asteroids main method and all the
AsteroidsFrame class code. This application code must be compiled with
Java 1.1 to work at all, and must be compiled with Java 1.2 for the sounds
to work in the application.
When changing your JDK, remember to change the JDK.version setting in the
first class below, also remember to make changes to the Asteroids loadSound code.
Program Keyboard Controls:
B - Toggle Black/White
background
C - Toggle Collisions
D - Toggle Graphics Detail
H - Hyperspace jump
M - Toggle Sound
S - Start Game
P - Pause Game
W - Toggle Bounce/Wrap
Cursor Left - Rotate Left
Cursor Right- Rotate Right Cursor Down - Fire Retro Thrusters
Cursor Up - Fire Thrusters
Spacebar - Fire Cannon
Revision Log.
Started log December 14, 1999.
Actually started revising in November, 1999. Revisions include. Making
an OOP architecture with Sprite and derived sublclasses. Putting a
spritevector in AsteroidsGame. Making into an application and an applet.
Adding collision detection and energy&momentum-conserving collisions. Giving
asteroids an avoidance routine. Made scale indpendent for resizable application
windows. Added wrap/bounce toggle. Added white/black backgroung toggle.
December 14, 1999. Made explosions fancier by drawing the center points of
the offcenter explosion lines. Unified all space and time dependent params
into a Units class, preperatory to making it update timestep independent and
having an adaptive update speed. Made the bullets die when they hit the edge
of the screen.
Dec 17, 1999. Ver 10. Made time units be in seconds. Added circular mode with diskflag.
Dec 20, 1999. Some more debugging and tweaks.
Dec 23, 1999. Ver 11. Some big fixes. I had a bunch of Asteroids variables static,
and this was causing crashes when I left an applet and returned to it.
Having the AudioClip variables statics was a particularly dumb idea. Live
and learn. Now I think you can flip in and out of an applet page without
a crash. There was still an intermittent hang problem, but I think maybe
getting rid of some other static values and cleaning up the sound checking
code fixed this. A desperation move is to set the Asteroids
triedgettingsounds field to true at start prevent any attempts at using
sounds, but I don't think I need this. I think my hang problem was in
fact only a graphical hang (the program still ran in background) becuase
I'd moved the offGraphics initialization away from the right spot.
*********************************************/
import java.awt.*;
import java.net.*;
import java.util.*;
import java.applet.*;
//BEGIN COMMENT OUT FOR JDK 1.0 ============
//Need this to build as application, need WindowAdapter in AsteroidsFrame.
import java.awt.event.*;
//End COMMENT OUT FOR JDK 1.0 ============
//START================JDK CODE===============
/** Use this class to hold a number to signal which
version of the JDK I'm using here: 0 for 1.0, 1 for 1.1,
2 for 1.2. I do this to try and mimic some of the
effect of a C++ #ifdef. This works inside the loadSounds
code, but not really. You still need to manually comment some code
in and out in loadSound. Only set version to 1 if you are using the jar file.*/
class JDK
{
/** Can be 0, 1, or 2 for JDK 1.0, 1.1, or 1.2 (also known as JDK 2). */
public final static int version = 1;
}
//END===================JDK CODE===============
//START=================VECTOR2 CODE===================
/** A helper class used for pairs of real numbers. */
class Vector2
{
public double x;
public double y;
static public Vector2 sum(Vector2 u, Vector2 v)
{
return new Vector2(u.x + v.x, u.y + v.y);
}
static public Vector2 difference(Vector2 u, Vector2 v)
{
return new Vector2(u.x - v.x, u.y - v.y);
}
/** Scalar product. */
static public Vector2 product(double r, Vector2 u)
{
return new Vector2(r*u.x, r*u.y);
}
static public double dotproduct(Vector2 u, Vector2 v)
{ return u.x*v.x + u.y*v.y;}
/** Returns an angle between -PI and PI expressing the turn from
u to v. That is, u.turn(Vector2.angle(u,v)) points in the same
direction as v.*/
static public double angle(Vector2 u, Vector2 v)
{
/* Old way was nondirectional
if (!u.nonzero() || !v.nonzero())
return 0.0;
double cosine = Vector2.dotproduct(u, v) /
(u.magnitude() *
v.magnitude());
return Math.acos(cosine);
*/
double turnangle = v.angle() - u.angle();
if (turnangle > Math.PI)
turnangle -=
2.0*Math.PI;
if (turnangle < Math.PI)
turnangle +=
2.0*Math.PI;
return turnangle;
}
/** Returns the actual angle between -PI/2 and 3PI/2 of the vector's
direction. */
public double angle()
{
double atanangle;
if (!nonzero())
return 0;
if (x == 0)
{
if (y < 0)
return
-Math.PI / 2;
else
return
Math.PI / 2;
}
else
{
atanangle =
Math.atan(y/x); //Ranges -PI/2 to PI/2.
if (x < 0)
return
(atanangle + Math.PI); //Ranges PI/2 to 3PI/2.
return atanangle;
}
}
public Vector2(){x = 0.0; y = 0.0;}
public Vector2(double ix, double iy){x=ix; y=iy;}
public Vector2(Vector2 u){x=u.x; y=u.y;}
public Vector2(double angle){x=Math.cos(angle);y=Math.sin(angle);}
public boolean nonzero(){return (x != 0.0 && y != 0.0);}
public void copy(Vector2 v){x = v.x; y = v.y;}
/** Shorter name for existing Point2D setLocation method. */
public void set(double ix, double iy){x=ix; y=iy;}
public void set(double angle){x=Math.cos(angle);y=Math.sin(angle);}
public double magnitude(){return Math.sqrt(x*x + y*y);}
public double distanceTo(Vector2 u)
{Vector2 dv = Vector2.difference(this,u); return dv.magnitude();}
public double setMagnitude(double newmag) //Returns old magnitude
{
double oldmag = magnitude();
double multiplier;
if (oldmag == 0.0)
set(newmag, 0.0);
else
{
multiplier =
newmag/oldmag;
multiply(multiplier);
}
return oldmag;
}
public double normalize()//Makes a unit vector, returns old magnitude.
{
return setMagnitude(1.0);
}
public void setzero(){set(0.0, 0.0);}
public void add(Vector2 v){x += v.x; y += v.y;}
public void subtract(Vector2 v){x -= v.x; y -= v.y;}
public void multiply(double r){ x*=r; y*= r;}
public void turn(double angle)
{
double c = Math.cos(angle), s =
Math.sin(angle);
double newx; //Need this so that you use the
"old" x in the y turn line.
newx = c*x - s*y;
y = s*x + c*y;
x = newx;
}
public String toString()
{
return "(" + (int)x + ", "
+ (int)y + ")";
}
}
//END=================VECTOR2 CODE===================
//START=================Vector2Colored CODE===================
/** The Vector2Colored class defines a little colored cross, to use for a star */
class Vector2Colored extends Vector2
{
Color fillcolor;
public Vector2Colored(){super();}
public Vector2Colored(Randomizer rand, double width, double height)
{
x = rand.nextDouble(-width/2, width/2);
y = rand.nextDouble(-height/2, height/2);
fillcolor = rand.nextColor(0.25, 0.75);
}
public void draw(Graphics g, RealPixelConverter rtopc)
{
Vector2 pixelpoint = rtopc.transform(this);
int pixelx = (int)pixelpoint.x;
int pixely = (int)pixelpoint.y;
g.setColor(fillcolor);
g.drawLine(pixelx-1, pixely, pixelx+1, pixely);
g.drawLine(pixelx, pixely-1, pixelx, pixely+1);
}
}
//END=================Vector2Colored CODE===================
//BEGIN==============REALPIXELCONVERTER CODE===============
/** This class is designed to transform a real-number-sized window into a
pixel window. If were using Java 1.2, it could extend AffineTransform */
class RealPixelConverter
{
//Members
private int widthpixel = 600, heightpixel = 480;
private double widthreal = 4.0, heightreal = 3.0;
private double m00, m02, m11, m12; /* As in an affine transformation
where
where x' = m00 x + m01 y + m02, y' = m10 x +
m11 y + m12.*/
//Methods
public RealPixelConverter()
{
fixAffineTransform();
}
public RealPixelConverter(double iwidthreal, double iheightreal,
int iwidthpixel, int iheightpixel)
{
widthreal = iwidthreal;
heightreal = iheightreal;
widthpixel = iwidthpixel;
heightpixel = iheightpixel;
fixAffineTransform();
}
private void fixAffineTransform()
{
double scalex = (double)widthpixel/widthreal;
double scaley = (double)heightpixel/heightreal;
double scale = Math.min(scalex, scaley);
m00 = scale; m02 = widthpixel/2.0;
m11 = -scale; m12 = heightpixel/2.0;
/* Like the AffineTransform setTransform(scale, 0.0, 0.0, -scale,
widthpixel/2.0, heightpixel/2.0); The arguments are in the order m00, m10,
m01, m11, m02, m12. */
}
public double widthreal(){return widthreal;}
public double heightreal() {return heightreal;}
public double widthpixel() {return widthpixel;}
public double heightpixel() {return heightpixel;}
public double scale(){return m00;}
public void setPixelWindow(int width, int height)
{
widthpixel = width; heightpixel = height;
fixAffineTransform();
}
public void setRealWindow(double width, double height)
{
widthreal = width; heightreal = height;
fixAffineTransform();
}
public void transform(Vector2 in, Vector2 out)
{
out.set(m00*in.x + m02, m11*in.y + m12);
}
public Vector2 transform(Vector2 in)
{
Vector2 out = new Vector2();
transform(in, out);
return out;
}
public double transform(double realx)
{
return m00*realx;
}
public void inverseTransform(Vector2 in, Vector2 out)
{
if (m00 == 0.0 || m11 == 0.0)
return;
out.set((in.x - m02)/m00, (in.y - m12)/m11);
}
public Vector2 inverseTransform(Vector2 in)
{
Vector2 out = new Vector2();
inverseTransform(in, out);
return out;
}
public double inverseTransform(double realx)
{
if (m00 == 0.0)
return realx;
return realx/m00;
}
/* Useful method, but we don't currently need it for this program.
Must be commented out for JDK 1.0 in any case. */
/*
public Vector2 mousePick(MouseEvent evt)
{
Vector2 pixelpick = new Vector2(evt.getX(),
evt.getY());
return inverseTransform(pixelpick);
}
*/
}
//END===================CONVERTER CODE===========
//START=================RANDOMIZER CODE============
/** Add a few useful methods to the standard Random class. This is Java 1.0
compatible, so I had to reimplement a few of the Java 1.2 Random additions.*/
class Randomizer extends Random
{
public Randomizer(){super();}
public Randomizer(long seed){super(seed);}
/** Return a double in the range 0 to hi exclusive of hi. */
public double nextDouble(double hi){return nextDouble()*hi;}
/** Return a double in the range lo to hi exclusive of hi. */
public double nextDouble(double lo, double hi){return lo +
nextDouble(hi - lo);}
/** This is a copy of the code used for the Random nextBoolean() mehtod
added in the JDK 1.2. We put it here so that we can use our class with
earlier JDK. It returns a true or a false with equal likelihood.*/
public boolean nextBoolean() {return nextDouble() < 0.5;}
/** This is a copy of the code used for the Random nextInt(int) mehtod
added in the JDK 1.2. We put it here so that we can use our class with
earlier JDK. It returns an integer between 0 and n, exclusive of n.*/
public int nextInt(int n)
{
int sign = 1; //The code depends on n being
positive.
if (n<=0) {n = -n; sign = -1;}
int bits, val;
bits = nextInt();
val = bits % n;
return sign*val;
}
/** Return an int in the range lo to hi inclusive. */
public int nextInt(int lo, int hi){return lo + nextInt(hi+1-lo);}
/** Return a Color in which the red, green and blue each have a
a brightness between lo and hi percent of the maximum value. */
public Color nextColor(double lo, double hi)
{
return new Color((float)nextDouble(lo, hi),
(float)nextDouble(lo, hi),
(float)nextDouble(lo,
hi));
}
}
//END=================RANDOMZER CODE===================
//START================UNITS CODE====================
/** The Units clas is a catch-all for holding a lot of the spatial size,
temporal duration, and speed constants used in the Asteroids program.
These statics could just as well live in the individual classes, but since
they interact with each other, it's more perspicacious to have them in one
spot. All the times are specified in seconds. The size units are arbitrary,
we generally state them in terms of SIZE_WORLD, which is the diameter of the
world where the asteroids live. A speed of k * SIZE_WORLD means that the
object travels k* SIZE_WORLD in one second, and that the object takes
1/k seconds to travel all the way across the world. Thus a speed of 0.25 * SIZE_WORLD
means the object travels a quarter wau across in one second, and takes four seconds
to travel all the way across.*/
class Units
{
//Asteroids main units===============================
/** Seconds between screen updates. Used in the Asteroids main function's
update as part of an argument to a wait call. We have 0.04, which means we
try for 25 updates a second. I still need to look into
exactly how to handle the case when the processor can't go this fast. */
static final double TIMEUNIT = 0.04;
/** The width of the unchanging real world in which the Asteroids move, the
document size, rather than the view size. Used in Asteroids.*/
static final double WIDTH_WORLD = 510;
/** The height of the unchanging real world in which the Asteroids move, the
document size, rather than the view size. Used in Asteroids.*/
static final double HEIGHT_WORLD = 510;
/** The minimum cross diameter of the world. Used for the rest of the Unit
definitions. */
static final double SIZE_WORLD = Math.min(WIDTH_WORLD, HEIGHT_WORLD);
/** Useful for disk wrap. */
static final double RADIUS_WORLD = SIZE_WORLD/2.0;
//Asteroid Sprite Units======================
/** Number of updates to pause after an old wave of asteroids before a new one.
Was 30.*/
static final double STORM_PAUSE = 1.0;
//Sprite Units======================
/** Default Sprite maxspeed, crosses world in 1.2 seconds. */
static final double MAX_SPEED_SPRITE = 0.8 * SIZE_WORLD;
//SpriteAsteroid Units======================
/** Min speed of asteroid motion in world units per update. 0.1 */
static final double MIN_SPEED_ASTEROID = 0.1 * SIZE_WORLD;
/* Max speed for asteroid. 0.3 */
static final double MAX_SPEED_ASTEROID = 0.4 * SIZE_WORLD;
/** Increment to the maxspeed (short of MAX_SPEED_ASTEROID) to be used
when starting a new board. */
static final double SPEED_INCREMENT_ASTEROIDS = 0.04 * SIZE_WORLD;
/* Min size of asteroid side in units defined using the game doc. */
static final double MIN_SIZE_ASTEROID = 0.04 * SIZE_WORLD;
/** Max size of asteroid side in world units. */
static final double MAX_SIZE_ASTEROID = 0.065 * SIZE_WORLD;
/** Max speed of asteroid rotation, in radians per second. */
static final double MAX_TURN_SPEED_ASTEROID = 0.3 * Math.PI;
/** Acceleration used by asteroid to avoid the nearest bullet. */
static final double ACCELERATION_ASTEROID_DART = 2.0 * SIZE_WORLD;
/** Factor by which a darting asteroid is allowed to exceed its
normal maxspeed. */
static final double DARTING_SPEEDUP = 2.0;
/** Acceleration towards the ship. */
static final double ACCELERATION_ASTEROID_TOSHIP = 0.002 * SIZE_WORLD;
//UFO Units======================
/** Min speed of Ufo motion in units per update. Was 3. */
static final double MIN_SPEED_UFO = 0.012 * SIZE_WORLD;
/** Speed in world units per second. */
static final double START_MAX_SPEED_UFO = 0.72 * SIZE_WORLD;
/** Seconds that a UFO lives */
static final double LIFETIME_UFO = 2.0;
/** Seconds between missile shots, so the missles aren't too often. */
static final double FIRE_WAIT_UFO = 2.5;
//==========Ship Units======================
/** Ship's maximum speed. */
static final double MAX_SPEED_SHIP = 0.64 * SIZE_WORLD;
/** Number of seconds that a ship is immune while doing a hyperjump. */
static final double HYPER_WAIT_SHIP = 3.0;
/** Seconds between bullet shots, so the bullets aren't too close together. */
static final double FIRE_WAIT_SHIP = 0.035;
/** Size of ship's rotation speed when you hold down the arrow key.
Needs to be small so you can aim accurately enough to hit a missile, on the
other hand it needs to be large so you can spin and fire off a big sweep of
bullets. So we have two kinds of TURN_STEP. Speed is radians per second. 0.6 */
static final double TURN_STEP_SLOW_SHIP = 0.6 * Math.PI;
/** See TURN_STEP_SLOW. Radians per second. 1.2*/
static final double TURN_STEP_FAST_SHIP = 1.8 * Math.PI;
/** Acceleration in world widths per second per second. 1.0 */
static final double ACCELERATION_THRUST = 1.0 * SIZE_WORLD;
//Missile Units===============================
/** Speed the missile uses to pursue a ship. Worlds per second. 0.27. */
static final double SPEED_MISSILE = 0.25 * SIZE_WORLD;
/** Missile lives long enough to cross the screen once. */
static final double LIFETIME_MISSILE = SIZE_WORLD/SPEED_MISSILE;
//Explosion Units===============================
/** Multiply this times a shape vertex's distance from the origin to get
the outward speed of an explosion fragment. 4.0*/
static final double SPEED_MULTIPLIER_EXPLOSION = 4.0;
/** Max speed of explosion rotation. Radians per second. 1.3 */
static final double MAX_TURN_SPEED_EXPLOSION = 1.3 * Math.PI;
/** Number of seconds that explosion lines live. 1.5*/
static final double LIFETIME_EXPLOSION = 1.5;
//Bullet Units===============================
/**Let Bullet go faster than the ship can go. */
static final double MAX_SPEED_BULLET = 1.5 * MAX_SPEED_SHIP;
/** Bullet speed in worlds per second. 0.8 */
static final double SPEED_BULLET = 1.0 * SIZE_WORLD;
/** life in seconds. Make sure it lives long enough to reach the edge of
the screen. 2.0*/
static final double LIFETIME_BULLET = 1.0;
}
//END==================UNITS CODE============
//START=================SPRITE CODE===================
/**
The Sprite class defines a game object, including its shape, position, movement and
rotation. It also can detemine if two objects collide. We have a number of
derived classes for the special kinds of sprites.
*/
abstract class Sprite
{
// Fields:
/** The basic shape which gets translated, rotated, and pixeld.*/
Polygon shape;
/** A flag to indicate if the sprite is currently alive and visible. */
boolean active;
/** Measure the sprite's age in seconds to time things like explosions,
hyperjumps and waits between bullet shots. */
double age;
/** What to do when the asteroids hit the edge of the screen - initially false*/
public boolean bounce = true;
/** The current rotation aspect. */
double angle;
/** The size of the thing. Make this private so nobody chagnes it directly,
must be chagned using setRadius, so as to keep mass in synch. */
private double radius;
/** We need an accessor for radius. */
public double radius(){return radius;}
/** Use this to set a fake radius to make something hard to hit. */
public void setFakeRadius(double fakeradius){radius = fakeradius;}
/** We'll maintain the mass as the cube of the radius, setting it in
the same fixRadius function that fixes the radius. */
private double mass;
/** Accessor */
public double mass(){return mass;}
/** The "acceleration" or
speed at which the sprite is rotating. That is, this is d angle/ dt.*/
double angularAcceleration;
/** The screen pixel position.*/
Vector2 position;
/** The pixel position. */
Vector2 positiontransform;
/** The velocity vector. */
Vector2 velocity;
/** The maximum speed. */
double maxspeed;
/** The acceleration vector. */
Vector2 acceleration;
/** The transformed shape polygon, after the rotation and the translation. */
Polygon shapetransform;
/** Whether or not to fill the sprite in. Default true. Set false for the
Spritebullet only. */
boolean fillflag;
/** The color to fill in the sprite with. */
Color fillcolor;
/** The color to use for the sprite's outline. Normally white. */
Color edgecolor;
/** Sprites can use the updateCounter to time things like explosions or
a fade to black. UFO and missiles use this to time how long they live.*/
double ageDone;
/** Is the sound currently on?*/
boolean soundplaying;
/** The "owner" applet or application. We need this as a way of referring
to the AudioClip sounds and the sound flags. Also we use it to refer
to the game member of Asteroids as a way to see the other
sprites. */
Asteroids ownerApp;
/** The Sprite constructor sets everything to 0, except for the fillcolor,
which gets randomized. */
public Sprite(Asteroids owner)
{
ownerApp = owner;
shape = new Polygon();
shapetransform = new Polygon();
active = false;
age = 0.0;
angle = 0.0;
angularAcceleration = 0.0;
radius = 0.0;
mass = 0.0;
position = new Vector2();
positiontransform = new Vector2();
velocity = new Vector2();
acceleration = new Vector2();
randomizeFillcolor();
maxspeed = Units.MAX_SPEED_SPRITE;
position.set(0.0, 0.0);
velocity.set(0.0, 0.0);
acceleration.set(0.0, 0.0);
edgecolor = Color.white;
fillflag = true;
}
public void randomizeFillcolor()
{
fillcolor = ownerApp.randomizer.nextColor(0.5, 1.0);
}
// Methods:
/** First sets the centroid to be the average of the shape vertices,
then translatees the shape so that its centroid is at the origin,
then sets the radius value as the maximum distance from the origin to one
of the shape vertices. */
public void fixRadius()
{
radius = 0;
if (shape.npoints == 0)
return;
Vector2[] Vector2points = new Vector2[shape.npoints];
double distance;
Vector2 centroid = new Vector2();
for (int i=0; i<Vector2points.length; i++)
{
Vector2points[i] = new
Vector2(shape.xpoints[i], shape.ypoints[i]);
centroid.add(Vector2points[i]);
}
centroid.multiply(1/shape.npoints);
for (int i=0; i<Vector2points.length; i++)
{
Vector2points[i].subtract(centroid);
shape.xpoints[i] = (int)Vector2points[i].x;
shape.ypoints[i] = (int)Vector2points[i].y;
distance = Vector2points[i].magnitude();
if (distance > radius)
radius = distance;
}
mass = Math.pow(radius, 3);
}
/** Update the rotation and position coordinates based on the delta
values. If the coordinates move off the edge of the screen, they are wrapped
around to the other side. */
public boolean advance(double dt)
{
age += dt;
angle += dt * angularAcceleration;
velocity.add(Vector2.product(dt, acceleration));
if (velocity.magnitude() > maxspeed)
velocity.setMagnitude(maxspeed);
position.add(Vector2.product(dt, velocity));
return wrapandbounce();
}
/** Handles the wrapping or bouncing. We have four possibilites: wrap or
bounce in either a disk world or a rectangular world.*/
public boolean wrapandbounce()
{
boolean outcode = false;
//Now wrap things.
if (angle < 0)
angle += 2 * Math.PI;
if (angle > 2 * Math.PI)
angle -= 2 * Math.PI;
//DISK code
double mag = position.magnitude();
/* If you're at the center, return now, as you might get into trouble
trying to work with a zero magnitude vector. */
if (mag == 0.0)
return false;
double limit = Units.RADIUS_WORLD - radius;
if (ownerApp.game.diskflag) //Think of world as a disk.
{
if (mag > limit)
{
outcode = true;
/* The disk bounce uses the idea that you are effectively
reflecting the velocity in a line perpendicular to the radial line to
the postion. You can do this by rotating velocity to the position
rotating through PI radians, and then rotating the same amount and
sense that you rotated to get to the position direction. */
if (bounce)
{
double
turnangle = Vector2.angle(velocity, position);
velocity.turn(Math.PI
+ 2.0*turnangle);
position.multiply(limit/mag);
}
else //wrap
position.multiply(-limit/mag);
}
} //End disk case.
else //!ownerApp.game.diskflag means rectangular world
{
if (bounce)
{ //bounce off walls
if (position.x <
radius-Units.WIDTH_WORLD / 2)
{ velocity.x *= -1;
position.x
= radius-Units.WIDTH_WORLD / 2;
outcode
= true;
}
if (position.x >
-radius+Units.WIDTH_WORLD / 2)
{ velocity.x *= -1;
position.x
= -radius+Units.WIDTH_WORLD / 2;
outcode
= true;
}
if (position.y <
radius-Units.HEIGHT_WORLD / 2)
{ velocity.y *= -1;
position.y
= radius-Units.HEIGHT_WORLD / 2;
outcode
= true;
}
if (position.y >
-radius+Units.HEIGHT_WORLD / 2)
{
velocity.y *= -1;
position.y
= -radius+Units.HEIGHT_WORLD / 2;
outcode
= true;
}
}
else
{ //wrap to other side
if (position.x <
-Units.WIDTH_WORLD / 2)
{position.x
+= Units.WIDTH_WORLD;
outcode
= true;}
if (position.x >
Units.WIDTH_WORLD / 2)
{position.x
-= Units.WIDTH_WORLD;
outcode
= true;}
if (position.y <
-Units.HEIGHT_WORLD / 2)
{position.y
+= Units.HEIGHT_WORLD;
outcode
= true;}
if (position.y >
Units.HEIGHT_WORLD / 2)
{position.y -=
Units.HEIGHT_WORLD;
outcode
= true;}
}
} //End rectangular world
return outcode;
}
/** Applies the rotation and the position translation to the shape to get the
shapetransform polygon. The shape is rotated angle degrees around the origin
and then translated by the amount pixel*position + (width/2, height/2).*/
public void render()
{
int i;
RealPixelConverter rtopc = ownerApp.realpixelconverter;
shapetransform = new Polygon();
rtopc.transform(position, positiontransform);
double scale = rtopc.scale();
double scalecos = scale*Math.cos(angle);
double scalesin = scale*Math.sin(angle);
for (i = 0; i < shape.npoints; i++)
/* I write out the transform operation by hand here to make it faster.
I am doing three things to each vertex of the polygon (1) rotate it
around the origin, (2) carry out the RealPixelConverter transform, which
means multiplying each term by scale or -scale, (3) add the point to
the positiontransform location.
I need to do Math.max, because the IE 4 browser treats negative pixel
coordinates a large positives, making a poly with a vertex offscreen to top
or left be drawn as if that vertex were at positive infinity, making a
horizontal or vertical line. IE 5 and Netscape don't do this. Java: write
once debug everywhere. */
shapetransform.addPoint(
Math.max(0,(int)Math.round(
scalecos*shape.xpoints[i]
-
scalesin*shape.ypoints[i]
+
positiontransform.x)),
Math.max(0, (int)Math.round(
-(scalecos*shape.ypoints[i]
+ //Flip sign because pixel y runs down.
scalesin*shape.xpoints[i]) +
positiontransform.y)));
}
/** Just look at the distances between the positions of the two and compare to
the sum of the radii. This is a faster method than isTouching, though
less accurate. We use this for asteroid-asteroid collisions only. It is
good for this purpose becuase it makes the collision back-off correction look
smooth. */
public boolean isColliding(Sprite s)
{
return (position.distanceTo(s.position) < radius + s.radius);
}
/** This is more accurate than isColliding, but a bit slower.
We use it for the ship-asteroid collisions, for ship-bullet, and for
bullet-missile collisions. You have to use inside for JDK 1.0, but don't
use it for JDK 1.1 as it doesn't work there. */
public boolean isTouching(Sprite s)
{
int i;
if (JDK.version == 0)
{
for (i = 0; i < s.shapetransform.npoints;
i++)
if
(shapetransform.inside( //contains method for JDK 1.1
s.shapetransform.xpoints[i],s.shapetransform.ypoints[i]))
return
true;
for (i = 0; i < shapetransform.npoints; i++)
if
(s.shapetransform.inside( //contains method for JDK 1.1
shapetransform.xpoints[i],
shapetransform.ypoints[i]))
return
true;
}
else
{
//BEGIN COMMENT OUT FOR JDK 1.0=========
for (i = 0; i < s.shapetransform.npoints; i++)
if
(shapetransform.contains(
s.shapetransform.xpoints[i],s.shapetransform.ypoints[i]))
return
true;
for (i = 0; i < shapetransform.npoints; i++)
if
(s.shapetransform.contains(
shapetransform.xpoints[i],
shapetransform.ypoints[i]))
return
true;
//END COMMENT OUT FOR JDK 1.0=======
}
return false;
}
/** The abstract reset method is called by the child sprite constructors, and
may also be called when resetting a sprite's state.*/
public abstract void reset();
/** The update method can be overloaded by the child sprite. */
public abstract void update();
/** The base method bails out if the the Asteroids soundsloaded isn't
true, and it won't turn on a sound if Asteroids soundflag is off. */
public boolean loopsound(boolean onoff)
{
if (ownerApp == null)
return false;
if (!(ownerApp.triedgettingsounds && ownerApp.soundsloaded))
return false;
if (onoff && !ownerApp.soundflag)
return false;
soundplaying = onoff; //The value you'd like to do.
return true;
}
/** The stop method sets active to false and turns off the sound. */
public void stop()
{
active = false;
ageDone = age;
loopsound(false);
}
/** Create sprites for explosion animation. Each individual line segment
of the given shapetransform is used to create a new shapetransform that will
move outward from the shapetransform's original position with a random
rotation. We deliberately don't call setRadius for the explosions because
we want their centers not to be in the middle of their segments. Instead
we set their radii by hand. */
public void explode()
{
int skip, i, j;
SpriteExplosion explosion;
skip = 1;
if (shape.npoints >= 12)
skip = 2;
for (i = 0; i < shape.npoints; i += skip)
{
explosion = ownerApp.game.nextExplosion();
explosion.active = true;
explosion.shape = new Polygon();
explosion.shape.addPoint(shape.xpoints[i],
shape.ypoints[i]);
j = i + 1;
if (j >= shape.npoints)
j -= shape.npoints;
explosion.shape.addPoint(shape.xpoints[j],
shape.ypoints[j]);
explosion.angle = angle;
explosion.radius = radius;
explosion.angularAcceleration =
ownerApp.randomizer.nextDouble(
-Units.MAX_TURN_SPEED_EXPLOSION,
Units.MAX_TURN_SPEED_EXPLOSION);
explosion.position.copy(position);
explosion.velocity.set(-shape.xpoints[i],
-shape.ypoints[i]);
explosion.velocity.multiply(Units.SPEED_MULTIPLIER_EXPLOSION);
explosion.ageDone = explosion.age +
Units.LIFETIME_EXPLOSION;
explosion.render();
}
}
/** Use the fillPolygon and drawPolygon functions to draw. */
public void draw(Graphics g, boolean detail)
{
if (active)
{
if (fillflag && detail)
{
g.setColor(fillcolor);
g.fillPolygon(shapetransform);
}
g.setColor(edgecolor);
g.drawPolygon(shapetransform);
/*
//Draw the bounding box
Rectangle box = shapetransform.getBounds();
g.setColor(edgecolor);
g.drawRect(box.x, box.y, box.width,
box.height);
//Draw the radius circle and center point
int centerx = (int)positiontransform.x;
int centery = (int)positiontransform.y;
int pixeldradius =
(int)(Asteroids.realpixelconverter.scale()*radius());
g.drawOval(centerx - pixeldradius,
centery - pixeldradius,
2*pixeldradius,
2*pixeldradius);
g.drawLine(centerx - 1, centery,
centerx + 1, centery);
g.drawLine(centerx, centery-1,
centerx, centery+1);
*/
}
}
} //end of Sprite class
/** The player sprite, controlled by the keyboard. */
class SpriteShip extends Sprite
{
/** How many turn step updates before you shift into fast turning? */
static final int TURN_STEP_THRESHOLD = 8;
/** Track how many turn steps you've done on this key press.*/
int turnstepcount = 0;
/** A function that returns the slow or fast TURN_STEP depending on
turnstepcount. */
double turnstep()
{
if (turnstepcount<TURN_STEP_THRESHOLD) return Units.TURN_STEP_SLOW_SHIP;
else return Units.TURN_STEP_FAST_SHIP;
}
/** Next score needed to get a new ship. */
static int newShipScore;
/** Number of ships left in game */
static int shipsLeft;
/** Key flag for left arrow key pressed. */
boolean left = false;
/** Key flag for right arrow key pressed. */
boolean right = false;
/** Key flag for up arrow key pressed. */
boolean up = false;
/** Key flag for down arrow key pressed. */
boolean down = false;
/** Machine-gun firing key. */
boolean firing = false;
/** A ship needs an ageDoneHyperjump as well as the Sprite ageDone,
uses it for fading out of hyperjumps.*/
double ageDoneHyperjump;
/** Use the ageShootwait to time the wait between bullets shot. */
double ageDoneShootwait;
/** Use this flag to mean that your ship never dies. */
static boolean godmode = false;
/** Constructor sets colors, calls reset(). */
public SpriteShip(Asteroids owner)
{
super(owner);
maxspeed = Units.MAX_SPEED_SHIP;
fillcolor = Color.black;
shape.addPoint(10, 0);
shape.addPoint(-10, 7);
shape.addPoint(-10, -7);
fixRadius();
ageDoneHyperjump = age;
reset();
}
/** Puts the ship motionless in the middle of the screen. */
public void reset()
{
active = true;
angle = Math.PI/2;
angularAcceleration = 0.0;
position.setzero();
velocity.setzero();
render();
loopsound(false);
ageDone = age;
ageDoneShootwait = age;
}
/** Toggles the thrustersSound. */
public boolean loopsound(boolean onoff)
{
if (!super.loopsound(onoff))
return false;
if (!soundplaying)
ownerApp.thrustersSound.stop();
else
ownerApp.thrustersSound.loop();
return true;
}
/** The ship update method rotates the ship if left or right cursor key is down,
Fires thrusters if up or down arrow key is down, calls advance and render,
counts down the hyperspace counter if ship is in hyperspace. If Ship is
exploding, advance the countdown or create a new ship if it is done exploding.
The new ship is added as though it were in hyperspace.(This gives the player
time to move the ship if it is in imminent danger.) If that was the last
ship,
end the game. */
public void update()
{
double dx, dy;
if (!ownerApp.game.gameonflag)
return;
angularAcceleration = 0.0;
// Rotate the ship if left or right cursor key is down.
if (left)
{ angularAcceleration = turnstep();
turnstepcount++;
}
if (right)
{ angularAcceleration = -turnstep();
turnstepcount++;
}
/* Fire thrusters if up or down cursor key is down. */
acceleration.set(0.0, 0.0);
Vector2 thrust = new Vector2(Math.cos(angle), Math.sin(angle));
thrust.multiply(Units.ACCELERATION_THRUST);
if (up)
acceleration = thrust;
if (down)
acceleration = Vector2.product(-1.0, thrust);
/* Try to shoot if firing*/
if (firing)
fire();
/* Ship is exploding, advance the countdown or create a new ship if it is
done exploding. The new ship is added as though it were in hyperspace.
(This gives the player time to move the ship if it is in imminent
danger.)
If that was the last ship, end the game.*/
if (!active)
{
if (age > ageDone)
{
if(shipsLeft > 0)
{
reset();
ageDoneHyperjump
= age + Units.HYPER_WAIT_SHIP;
}
else
ownerApp.game.stop();
}
}
}
public void stop()
{
if (godmode)
return;
super.stop();
ageDone = age + Units.LIFETIME_EXPLOSION;
ageDoneShootwait = ageDone;
if (shipsLeft > 0)
shipsLeft--;
}
/** Make a sound and get a blank bullet slot or the oldest
bullet slot and put a new bullet in there, aimed in your direction
and starting at your location. */
public void fire()
{
if(age <= ageDoneShootwait)
return;
SpriteBullet bullet = ownerApp.game.nextBullet();
if (ownerApp.soundflag && ownerApp.soundsloaded &&
!ownerApp.game.paused)
ownerApp.fireSound.play();
bullet.initialize(this);
ageDoneShootwait = age + Units.FIRE_WAIT_SHIP;
}
/** Randomize your location and give yourself an "immunity" for
a few cycles. */
public void hyperjump()
{
position.x = ownerApp.randomizer.nextDouble(-Units.WIDTH_WORLD/2,
Units.WIDTH_WORLD/2);
position.y = ownerApp.randomizer.nextDouble(-Units.HEIGHT_WORLD/2,
Units.HEIGHT_WORLD/2);
ageDoneHyperjump = age + Units.HYPER_WAIT_SHIP;
if (ownerApp.soundflag && ownerApp.soundsloaded &&
!ownerApp.game.paused)
ownerApp.warpSound.play();
}
/** Process the arrow keys and spacebar. */
public void keyPressed(int key)
{
// Arrowkeys: Set flags and use them.
if (key == Event.LEFT)
left = true;
if (key == Event.RIGHT)
right = true;
if (key == Event.UP)
up = true;
if (key == Event.DOWN)
down = true;
//Use arrowkey flags.
if ((up || down) && active && !soundplaying)
{ if (ownerApp.soundflag &&
!ownerApp.game.paused)
loopsound(true);
}
// Spacebar start firing.
if (key == 32 && active)
firing = true;
// 'H' key: warp ship into hyperspace by moving to a random location.
if (key == 104 && active && age > ageDoneHyperjump)
hyperjump();
}
/** Process the arrow keys and spacebar. */
public void keyReleased(int key)
{
// Check if any cursor keys where released and set flags.
if (key == Event.LEFT)
{
left = false;
turnstepcount = 0;
}
if (key == Event.RIGHT)
{
right = false;
turnstepcount = 0;
}
if (key == Event.UP)
up = false;
if (key == Event.DOWN)
down = false;
if (key == 32)
firing = false;
if (!up && !down)
loopsound(false);
}
/** This fades the edge from black to white when you have a black fill,
as is customary with a black background. It fades the edge from white to black
otherwise. */
public void draw(Graphics g, boolean detail)
{
int brightness;
if (age > ageDoneHyperjump)
brightness = 255;
else
brightness = 255 -
(int)((255-64)*(ageDoneHyperjump - age)/Units.HYPER_WAIT_SHIP);
if (ownerApp.backgroundcolor != Color.black)
brightness = 255 - brightness;
edgecolor = new Color(brightness, brightness, brightness);
super.draw(g, detail);
}
}
/** Our jagged asteroid sprites, they come in two sizes. */
class SpriteAsteroid extends Sprite
{
/** Min number of asteroid sides. 9. */
static final int MIN_SIDES = 9;
/** Max number of asteroid sides. 13. */
static final int MAX_SIDES = 13;
/** Points for shooting a big asteroid. 25.*/
static final int BIG_POINTS = 25;
/** Points for shooting a small asteroid. 50.*/
static final int SMALL_POINTS = 50;
/** Number of fragments to split an asteroid into. */
static final int FRAG_COUNT = 2;
/** Flag used to distinguish between big and small asteroids. */
boolean asteroidIsSmall;
/** I use the same speed for all the asteroids, so this is static. */
//static int speed;
/**Number of asteroids remaining.*/
static int asteroidsLeft;
/**The number of updates to pause between waves of asteroids. */
static double ageDoneStormpause;
/**The age of the whole flock of asteroids */
static double agegame = 0.0;
/** Use this to allow temporary speedup when running away from a bullet. */
boolean darting = false;
public SpriteAsteroid(Asteroids owner)
{
super(owner);
maxspeed = Units.MAX_SPEED_ASTEROID;
reset();
}
/** Creates a random jagged shape and movement. Places asteroid
at an edge of the screen. Randomizes the fillcolor.*/
public void reset()
{
int i, j;
int sides;
double theta, r;
int x, y;
randomizeFillcolor();
// Create a jagged shape for the asteroid and give it a random rotation.
shape = new Polygon();
sides = + ownerApp.randomizer.nextInt(MIN_SIDES, MAX_SIDES);
for (j = 0; j < sides; j ++)
{ theta = j* (2.0 * Math.PI)
/ sides;
r =
ownerApp.randomizer.nextDouble(Units.MIN_SIZE_ASTEROID, Units.MAX_SIZE_ASTEROID);
x = (int) -Math.round(r
* Math.sin(theta));
y = (int)
Math.round(r * Math.cos(theta));
shape.addPoint(x, y);
}
fixRadius();
angle = 0.0;
angularAcceleration = ownerApp.randomizer.nextDouble(
-Units.MAX_TURN_SPEED_ASTEROID, Units.MAX_TURN_SPEED_ASTEROID);
// Place the asteroid at one edge of the screen.
if (ownerApp.randomizer.nextBoolean())
{ position.x = -Units.WIDTH_WORLD / 2;
if (ownerApp.randomizer.nextBoolean())
position.x =
Units.WIDTH_WORLD / 2;
position.y =
ownerApp.randomizer.nextDouble(-Units.HEIGHT_WORLD/2,
Units.HEIGHT_WORLD/2);
}
else
{ position.x =
ownerApp.randomizer.nextDouble(-Units.WIDTH_WORLD/2,
Units.WIDTH_WORLD/2);
position.y = -Units.HEIGHT_WORLD / 2;
if (ownerApp.randomizer.nextBoolean())
position.y =
Units.HEIGHT_WORLD / 2;
}
// Set a random motion for the asteroid.
double direction = ownerApp.randomizer.nextDouble(2.0 * Math.PI);
velocity.set(Math.cos(direction), Math.sin(direction));
double speed = ownerApp.randomizer.nextDouble(Units.MIN_SPEED_ASTEROID,
maxspeed);
velocity.setMagnitude(speed);
render();
asteroidIsSmall = false;
if (active == false)
{
active = true;
asteroidsLeft++;
}
}
/** Create one or more smaller asteroids from a larger one using inactive
asteroids. The new asteroids will be placed in the same position as
the old one but will have a new, smaller shape and new, randomly
generated movements. */
public void split(int fragcount)
{
double oldX = position.x;
double oldY = position.y;
Vector2 oldvelocity = new Vector2(velocity);
Color oldfillcolor = new Color(fillcolor.getRGB());
double oldmaxspeed = maxspeed;
int sides;
double theta, r;
int x, y;
SpriteAsteroid smallasteroid;
//First replace this asteroid by a smaller one.
smallasteroid = this;
for (int i=0; i<fragcount; i++)
{
smallasteroid.shape = new Polygon();
sides = ownerApp.randomizer.nextInt(MIN_SIDES,
MAX_SIDES);
for (int j = 0; j < sides; j++)
{ theta = j * (2.0 * Math.PI)
/ sides;
r =
ownerApp.randomizer.nextDouble(Units.MIN_SIZE_ASTEROID,
Units.MAX_SIZE_ASTEROID) / fragcount;
x = (int) -Math.round(r
* Math.sin(theta));
y = (int) Math.round(r
* Math.cos(theta));
smallasteroid.shape.addPoint(x,
y);
}
smallasteroid.fixRadius();
smallasteroid.active = true;
smallasteroid.angle = 0.0;
smallasteroid.angularAcceleration =
ownerApp.randomizer.nextDouble(
-Units.MAX_TURN_SPEED_ASTEROID, Units.MAX_TURN_SPEED_ASTEROID);
smallasteroid.position.x = oldX;
smallasteroid.position.y = oldY;
smallasteroid.fillcolor = oldfillcolor;
smallasteroid.velocity.copy(oldvelocity);
smallasteroid.maxspeed = oldmaxspeed;
/* Turn between -90 and 90 degrees */
smallasteroid.velocity.turn(ownerApp.randomizer.nextDouble(-Math.PI/2.0,
Math.PI/2.0));
smallasteroid.velocity.multiply(ownerApp.randomizer.nextDouble(0.5,
1.5));
smallasteroid.render();
smallasteroid.asteroidIsSmall = true;
asteroidsLeft++;
smallasteroid = ownerApp.game.nextAsteroid();
if (smallasteroid == null)
break;
}
}
/** Give the asteroids an ability to speed up for darting. */
public boolean advance(double dt)
{
angle += dt*angularAcceleration;
velocity.add(Vector2.product(dt, acceleration));
if (darting)
{
if (velocity.magnitude() >
Units.DARTING_SPEEDUP * maxspeed)
velocity.setMagnitude(Units.DARTING_SPEEDUP
* maxspeed);
// System.out.println("accel " +
acceleration + "vel " + velocity);
}
else if (velocity.magnitude() > maxspeed)
velocity.setMagnitude(maxspeed);
position.add(Vector2.product(dt, velocity));
return wrapandbounce();
}
/** The idea in this algorithm is that the caller asteroid C and the argument asteroid
A will do some exchange of their velocities. We'll think of them as pointlike,
so that all that matters it the components of their velocities which lie along the
line connecting their two positions. We calculate these two vectors as
C's give and A's receive vector.
The way this works is that (a) the momentum is conserved and (b) the
energy is
conserved. We get (a) Ma*newVa + Mb*newVb == Ma*Va + Mb*Vb, and
(b) (1/2)*((Ma*newVa^2 + Mb*newVb^2)) == (1/2)*(Ma*Va^2 + Mb*Vb^2).
This is the intersection of a line and an ellipse, which is two points.
I ran this through Mathematica and came up with the trivial solution
newVa = Va and newVb = Vb and the one we'll use:
newVa = (Ma*Va - Mb*Va + 2*Mb*Vb) / (Ma + Mb)
new Vb =( 2*Ma*Va - Ma*Vb + Mb*Vb)/ (Ma + Mb).
If I divide both numerators and denominators by Ma, and call Mb/Ma "massratio" I
get
newVa = (Va - massratio*Va + 2*massratio*Vb) / (1 + massratio)
newVb =( 2*Va - Vb + massratio*Vb)/ (1 + massratio).
Simplifying a little more, we get
newVa = ((1-massratio)*Va + 2*massratio*Vb) / (1 + massratio)
newVb =( 2*Va + (massratio - 1)*Vb)/ (1 + massratio).
Note that if massratio is 1, then this is simply newVa = Vb and newVb = Va.
If a has a huge (infinite) mass compared to b, then massratio is about 0 and
we get newVa = Va, newVb = 2Va - Vb, so if a is motionless, this is a simple bounce.
We want contact point to be on the line between the two centers.
Rather than making it the midpoint, we weight it so that it divides
this line in the same ratio as radius and pother->radius(). That
is, we need to pick a sublength "contactdistance" of the "distance"
length between the two centers so that
radius/radius+otheradius = contactdistance/distance.
Multiply both sides of this equation to solve for contactdistance. */
public void bounceOff(SpriteAsteroid asteroid)
{
//Exchange momenta.
Vector2 toOther = Vector2.difference(asteroid.position, position);
double distance = toOther.normalize(); //Make unit vector towards other
asteroid.
double myComponent = Vector2.dotproduct(velocity, toOther);
//Give up all your velocity that lies along the line connecting
centers.
Vector2 give = Vector2.product(myComponent, toOther);
double asteroidComponent = Vector2.dotproduct(asteroid.velocity,
toOther);
Vector2 receive = Vector2.product(asteroidComponent, toOther);
if (radius() == 0.0)
return;
//Think of mass as proportional to the cube of the radius.
double massratio = asteroid.mass() / mass();
double massdivisor = (1.0/(1.0 + massratio));
velocity.subtract(give); //Give up your momentum
//Take a mass-weighted version of the other's momentum.
velocity.add(Vector2.product(massdivisor*2.0*massratio, receive));
velocity.add(Vector2.product(massdivisor*(1.0 - massratio), give));
asteroid.velocity.subtract(receive); //Other gives up his momemtum
asteroid.velocity.add(Vector2.product(massdivisor*2.0, give));
asteroid.velocity.add(Vector2.product(massdivisor*(massratio - 1.0),
receive));
//Takes yrs
//Move away from each other to prevent double collision.
Vector2 contactpoint = Vector2.sum(position,
Vector2.product(((radius()*distance)/(radius()+asteroid.radius())),toOther));
position.copy(Vector2.difference(contactpoint,
Vector2.product(radius(),toOther)));
asteroid.position.copy(Vector2.sum(contactpoint,
Vector2.product(asteroid.radius(), toOther)));
}
/** Check if you're colliding with any other asteroids, and if you are,
update your velocity and position accordingly. Do the symmetric update to
the other guy's veloicty and postion at the same time. Always just check
against the guys with index greater than you, that way we only handle each
pair once. This all takes time, and we can toggle it off with the AsteroidsGame
collideflag. */
public void collideAsteroids()
{
if (!ownerApp.game.collideflag)
return;
SpriteAsteroid[] asteroids = ownerApp.game.asteroids;
boolean pastmyindex = false;
int j;
for (j = 0; j < asteroids.length; j++)
{
if (asteroids[j] == this)
pastmyindex = true;
if (pastmyindex && asteroids[j].active
&& asteroids[j] != this &&
isColliding(asteroids[j]))
{
bounceOff(asteroids[j]);
break;
}
}
}
/** If hit by bullet, kill asteroid and advance score. If asteroid is large,
make some smaller ones to replace it. We'll avoid the bullets here by
setting acceleration. */
public void update()
{
if (!active)
return;
acceleration.set(0.0, 0.0);
collideAsteroids();
//Set up some variables to avoid bullets.
SpriteBullet[] bullets = ownerApp.game.bullets;
double closestbulletdistance = 1000000000.0; /* Just a big number so
that the
first bullet you look at will be closer than
this. */
SpriteBullet closestbullet = null;
Vector2 frombullet;
for (int j = 0; j < bullets.length; j++)
{
if (bullets[j].active && active)
{
if (isTouching(bullets[j]))
{
asteroidsLeft--;
active
= false;
bullets[j].active
= false;
if
(ownerApp.soundflag && ownerApp.soundsloaded)
ownerApp.explosionSound.play();
explode();
if
(!asteroidIsSmall)
{
ownerApp.game.score
+= BIG_POINTS;
split(FRAG_COUNT);
}
else
ownerApp.game.score
+= SMALL_POINTS;
}
else //See if this is the closest bullet and if it's heading my way
{
frombullet
= Vector2.difference(position, bullets[j].position);
double
bulletdistance = frombullet.magnitude();
/*
Am I getting closer to this bullet heading towards me AND
is
it closer than any other bullets heading towards me?
I
imagine myself motionless by subtracting my velocity from
everything,
and then look at how the bullet moves. */
Vector2 relativevelocity = Vector2.difference(
bullets[j].velocity,
velocity);
if ( Vector2.dotproduct(relativevelocity, frombullet) > 0.0 &&
bulletdistance
< closestbulletdistance)
{
closestbullet
= bullets[j];
closestbulletdistance
= bulletdistance;
}
}
}// end of j loop on bullets
if (closestbullet != null &&
closestbulletdistance < Units.HEIGHT_WORLD /
2)
{
darting = true;
frombullet = Vector2.difference(position,
closestbullet.position);
frombullet.setMagnitude(Units.ACCELERATION_ASTEROID_DART);
acceleration.add(frombullet);
angle = acceleration.angle(); //So you notice
that they're fleeing.
/* The next few lines are optional. The idea is to to make asteroid not be
like a rabbit running down a train-track away from an oncoming train. This
doesn't seem to have that big an effect, which is why it's optional. */
Vector2 relativevelocity =
Vector2.difference(bullets[j].velocity,
velocity);
double turnangle;
if (Math.abs(Vector2.angle(frombullet,
relativevelocity))
< Math.PI/16.0 ||
Math.abs(Vector2.angle(Vector2.product(-1.0,frombullet),velocity))
< Math.PI/16.0)
{
if
(ownerApp.randomizer.nextBoolean())
turnangle
= Math.PI/2.0;
else
turnangle
= -Math.PI/2.0;
velocity.turn(turnangle);
}
//End of the rabbit-avoiding-train code.
}
else //Head for ship
{
darting = false;
Vector2 toship = Vector2.difference(ownerApp.game.ship.position,
position);
toship.setMagnitude(Units. ACCELERATION_ASTEROID_TOSHIP);
acceleration.add(toship);
}
// If the ship is not in hyperspace, see if it is hit.
if (ownerApp.game.ship.active &&
ownerApp.game.ship.age > ownerApp.game.ship.ageDoneHyperjump
&&
active &&
isTouching(ownerApp.game.ship))
{
if (ownerApp.soundflag && ownerApp.soundsloaded)
ownerApp.crashSound.play();
ownerApp.game.ship.explode();
ownerApp.game.ship.stop();
ownerApp.game.ufo.stop();
ownerApp.game.missile.stop();
} // end of if (ship.active...)
}
}//end of if (active...), also end of i loop.}
}
class SpriteUfo extends Sprite
{
/** Number of times a Ufo gets to cross the screen. 5.*/
static final int UFO_PASSES = 5;
/** Points for shooting a Ufo. 250.*/
static final int UFO_POINTS = 250;
/** The ufo gets a fixed number of passes across the screen. */
int ufoPassesLeft;
/** The next score the player has to pass to make a new Ufo come to life. */
static int newUfoScore;
/** Next time you can shoot a missile. */
double ageDoneShootwait;
public SpriteUfo(Asteroids owner)
{
super(owner);
maxspeed = Units.START_MAX_SPEED_UFO;
fillcolor = Color.gray;
shape.addPoint(-15, 0);
shape.addPoint(-10, 5);
shape.addPoint(-5, 5);
shape.addPoint(-5, 9);
shape.addPoint(5, 9);
shape.addPoint(5, 5);
shape.addPoint(10, 5);
shape.addPoint(15, 0);
shape.addPoint(10, -5);
shape.addPoint(-10, -5);
fixRadius();
ageDoneShootwait = 0;
reset();
}
/**Randomly set flying saucer at an edge of the screen. */
public void reset()
{
double temp;
active = true;
position.x = -Units.WIDTH_WORLD / 2;
position.y = -Units.HEIGHT_WORLD/2 +
ownerApp.randomizer.nextDouble() *
Units.HEIGHT_WORLD;
velocity.x = ownerApp.randomizer.nextDouble(Units.MIN_SPEED_UFO,
maxspeed);
if (ownerApp.randomizer.nextBoolean())
{ velocity.x = -velocity.x;
position.x = Units.WIDTH_WORLD / 2;
}
velocity.y = ownerApp.randomizer.nextDouble(Units.MIN_SPEED_UFO,
maxspeed);
if (ownerApp.randomizer.nextBoolean())
velocity.y = -velocity.y;
render();
loopsound(true);
// Set ageDone timer for this pass.
ageDone = age + Units.LIFETIME_UFO;
}
/** Toggles the saucerSound. */
public boolean loopsound(boolean onoff)
{
if (!super.loopsound(onoff))
return false;
if (!soundplaying)
ownerApp.saucerSound.stop();
else
ownerApp.saucerSound.loop();
return true;
}
public void update()
{
int i, distancetoship;
SpriteBullet[] bullets = ownerApp.game.bullets;
/* Move the flying saucer and check for collision with a bullet. Stop it when
its past ageDone. */
if (active)
{
if (age > ageDone)
{
if (--ufoPassesLeft
> 0)
reset();
else
stop();
}
else //normalupdate
{
for (i = 0; i < bullets.length; i++)
if (bullets[i].active &&
isTouching(bullets[i]))
{
if (ownerApp.soundflag && ownerApp.soundsloaded)
ownerApp.crashSound.play();
explode();
stop();
ownerApp.game.score +=
UFO_POINTS;
}
fire();
} //end normal update
} //end if active
}
public void fire()
{
if(age <= ageDoneShootwait)
return;
double distancetoship =
(Vector2.difference(position,
ownerApp.game.ship.position)).magnitude();
if (ownerApp.game.ship.active && active &&
!ownerApp.game.missile.active &&
distancetoship > 10.0 *
ownerApp.game.ship.radius() && //Not point-blank.
ownerApp.game.ship.age >
ownerApp.game.ship.ageDoneHyperjump)
{
ownerApp.game.missile.reset();
if (ownerApp.soundflag &&
ownerApp.soundsloaded)
ownerApp.game.missile.loopsound(true);
ownerApp.game.missile.position.copy(position);
ageDoneShootwait = age + Units.FIRE_WAIT_UFO;
}
}
/** Turn off the UFO, set ufoPassesLeft to 0. */
public void stop()
{
super.stop();
ufoPassesLeft = 0;
}
}
/** A missile is the kind of "bullet" that the Ufo shoots. It's guided, that is,
it heads towards the ship.*/
class SpriteMissile extends Sprite
{
/** Points for shooting a Missile. 500.*/
static final int MISSILE_POINTS = 500;
public SpriteMissile(Asteroids owner)
{
super(owner);
shape.addPoint(5, 0);
shape.addPoint(3, 2);
shape.addPoint(-3, 2 );
shape.addPoint(-5, 3 );
shape.addPoint(-5, -3 );
shape.addPoint(-3, -2 );
shape.addPoint(3, -2 );
fixRadius();
fillcolor = Color.yellow;
}
/** Toggles the missileSound. */
public boolean loopsound(boolean onoff)
{
if (!super.loopsound(onoff))
return false;
if (!soundplaying)
ownerApp.missileSound.stop();
else
ownerApp.missileSound.loop();
return true;
}
public void reset()
{
active = true;
angle = 0.0;
angularAcceleration = 0.0;
velocity.setzero();
render();
ageDone = age + Units.LIFETIME_MISSILE;
loopsound(true);
}
/** At present this method is only used by the missile, but we might
consider letting the asteroids use it too! The boolean flag tells whether
to just menacingly aim yoursef (false) or to actually move this way (true). */
public void headTowardsSprite(Sprite target, boolean moving)
{
Vector2 toTarget;
// Find the angle needed to hit the ship.
toTarget = Vector2.difference(target.position, position);
angle = toTarget.angle();
if (moving)
{
velocity.set(angle); //Unit vector in direction
angle.
velocity.multiply(Units.SPEED_MISSILE);
}
}
/** Move the guided missile and check for collision with ship or bullet.
Stop it when its too old. Use isTouching so its hard to hit missile. */
public void update()
{
int i;
SpriteBullet[] bullets = ownerApp.game.bullets;
boolean moving = false;
if (active)
{
if (age > ageDone)
stop();
else
{
if
(ownerApp.game.ship.active && ownerApp.game.ship.age >
ownerApp.game.ship.ageDoneHyperjump)
moving
= true;
headTowardsSprite(ownerApp.game.ship,
moving); //just aim, or aim & move.
for (i = 0; i <
bullets.length; i++)
if
(bullets[i].active && isTouching(bullets[i]))
{
if (ownerApp.soundflag && ownerApp.soundsloaded)
ownerApp.crashSound.play();
explode();
stop();
ownerApp.game.score
+= MISSILE_POINTS;
}
if (active &&
ownerApp.game.ship.active &&
ownerApp.game.ship.age >
ownerApp.game.ship.ageDoneHyperjump &&
isTouching(ownerApp.game.ship))
{
if
(ownerApp.soundflag && ownerApp.soundsloaded)
ownerApp.crashSound.play();
ownerApp.game.ship.explode();
ownerApp.game.ship.stop();
ownerApp.game.ufo.stop();
stop();
}
} //end else
} //end active
}
}
class SpriteExplosion extends Sprite
{
public SpriteExplosion(Asteroids owner)
{
super(owner);
}
public void reset()
{
shape = new Polygon();
active = false;
ageDone = age;
}
public void update()
{
if (age > ageDone)
active = false;
}
/* We don't call the fixRadius for the SpriteExplosion, and this means
that the position point lies a bit off the line that is the
explosion object. This makes for nicer tumbling, and it gives
us the chance to enhance the explosion appearnce by adding a
little cross at the positon point. */
public void draw(Graphics g, boolean detail)
{
int brightness;
if (age >= ageDone)
brightness = 0;
else
brightness = 64 + (int)((255-64)*
((ageDone - age)/Units.LIFETIME_EXPLOSION));
if (ownerApp.backgroundcolor != Color.black)
brightness = 255 - brightness;
edgecolor = new Color(brightness, brightness, brightness);
super.draw(g, detail);
if (active)
{
int centerx = (int)positiontransform.x;
int centery = (int)positiontransform.y;
g.drawLine(centerx - 1, centery, centerx + 1,
centery);
g.drawLine(centerx, centery-1, centerx,
centery+1);
}
}
}
class SpriteBullet extends Sprite
{
public SpriteBullet(Asteroids owner)
{
super(owner);
fillflag = false;
shape.addPoint(2, 2);
shape.addPoint(2, -2);
shape.addPoint(-2, 2);
shape.addPoint(-2, -2);
fixRadius();
maxspeed = Units.MAX_SPEED_BULLET;
reset();
}
public void reset()
{
active = false;
ageDone = age;
}
void initialize(Sprite shooter)
{
active = true;
position.copy(shooter.position);
Vector2 gunoffset = new Vector2(shooter.angle);
gunoffset.multiply(shooter.radius());
position.add(gunoffset);
velocity.set(shooter.angle); //Unit vector in this direction.
velocity.multiply(Units.SPEED_BULLET);
ageDone = age + Units.LIFETIME_BULLET;
render();
//Make sure you get the shapetransform fixed
right away.
}
/** We'll deactivate a bullet if it wraps or bounces.*/
public boolean advance(double dt)
{
boolean outcode = super.advance(dt);
if (outcode)//&& !bounce) //If you like you can allow bounce
active = false;
return outcode;
}
/** We'll deactivate a bullet if its lifetime runs out. */
public void update()
{
if (age > ageDone)
active = false;
}
}//End SpriteBullet
/** Use a SpriteWall to visually show when the bounce flag is on, we'll
trigger it using the ship.bounce inside AsteroidsGame draw. We make
it a Sprite so we can use the render function to keep resizing it to
the current window size. */
class SpriteWall extends Sprite
{
public SpriteWall(Asteroids owner)
{
super(owner);
fillflag = false;
active = true;
edgecolor = Color.white;
fillcolor = Color.black;
/* Two of the rectangle edges won't show unless we make the box a bit
smaller. Also we need to keep in mind that our coordinates draw thigns
upside down. */
int halfwidth =
(int)(owner.realpixelconverter.widthreal()/2);
int halfheight =
(int)(owner.realpixelconverter.heightreal()/2);
shape.addPoint(-halfwidth, halfheight-1);
shape.addPoint(-halfwidth, -halfheight);
shape.addPoint(halfwidth-1, -halfheight);
shape.addPoint(halfwidth-1, halfheight-1);
}
/** Do-nothing advance method means don't move. */
public boolean advance(double dt){age += dt; return false;}
/** Do-nothing update method means don't look at other sprites. */
public void update(){}
/** Do-nothing reset method means there's nothing to reset.*/
public void reset(){}
/** Draw method draws it as a disk if ownerApp.game.diskflag. If
bouncing
we use the edgecolor (which can be either white or black). Otherwise
we use gray to show a soft, wrapping edge. */
public void draw(Graphics g, boolean detail)
{
Color oldedgecolor = new
Color(edgecolor.getRGB());
if (!ownerApp.game.ship.bounce)
edgecolor = Color.gray;
else
edgecolor =
Color.yellow;
if (!ownerApp.game.diskflag)
super.draw(g, detail);
else //disk case
{
Rectangle box = new
Rectangle();
box.x = shapetransform.xpoints[0];
box.y = shapetransform.ypoints[0];
box.width = shapetransform.xpoints[2] - shapetransform.xpoints[0];
box.height = shapetransform.ypoints[2] - shapetransform.ypoints[0];
/* In Java 1.1 I'd just do box = shapetransform.getBounds(); */
g.setColor(ownerApp.backgroundcolor);
g.fillOval(box.x, box.y, box.width, box.height);
g.setColor(edgecolor);
g.drawOval(box.x,
box.y, box.width, box.height);
}
edgecolor = oldedgecolor;
}
}
//END=================SPRITE CODE===================
//START=================ASTEROIDSGAME CODE===================
/** This container class holds a spritevector with all the sprites, and keeps
the SpriteBullet and SpriteExplosion in separate arrays so we can conveniently
replace the oldest one when we want to make a new one. */
class AsteroidsGame
{
// Constants
/**Starting number of ships per game. 3. */
static final int MAX_SHIPS = 3;
/** Number of active bullets allowed. 8. */
static final int MAX_BULLETS = 8; //8;
/** Number of asteroids in each wave. 8. */
static final int START_ASTEROIDS = 8;
/** Number of asteroids that you can get by splitting up.*/
static final int MAX_ASTEROIDS = (int)(1.5 * START_ASTEROIDS);
/** Total number of lines used to represent exploding objects. 30. */
static final int MAX_EXPLOSIONS = 30;
/** Number of additional points needed to earn a new ship. */
static final int NEW_SHIP_POINTS = 5000;
/** Number of additional points before a fresh Ufo attacks. */
static final int NEW_UFO_POINTS = 1500; //2750;
/** Number of stars per square world unit. We'll have about 250,000 area
at present, so keep it low. */
static double STARDENSITY = 0.0004;
/** Owner Asteroids object, where the randomizer lives. */
Asteroids ownerApp;
/** Array for background stars */
Vector2Colored[] stars;
/** Game score. */
static int score;
/** High score for this play session. */
static int highScore;
/** Flag to indicate if game is started and not yet over. */
boolean gameonflag;
/** Flag for paused game. */
boolean paused;
/** Flag for whether or not to calculate collisions between asteroids. */
boolean collideflag = true;
/** Flag for whether to use a rectangle or a disk for your world.*/
boolean diskflag = true;
/** Have a SpriteWall to show when the bounce is on for the ship. We don't
add this to the spritevector as it won't be colliding like the others,
etc. We will use it in the AsteroidsGame draw method. */
SpriteWall wall;
/** The spritevector holds all of the sprites; we walk this array for our
updates. */
Vector spritevector;
/**It's useful to have a distinguished name for the ship. */
SpriteShip ship;
/**Use a phtons array so that we can always replace the oldest bullet by the
newest bullet. */
SpriteBullet[] bullets;
/* Next available slot to put a new bullet into. */
int bulletIndex;
/** The flying saucer. */
SpriteUfo ufo;
/** The missile. */
SpriteMissile missile;
/** The asteroids. */
SpriteAsteroid[] asteroids;
/**So that we can always replace the oldest explosion line by the newest one,
we need an array for the explosions. */
SpriteExplosion[] explosions;
/* Next available slot to put a new explosion into. */
int explosionIndex = 0;
/** Construct all your sprites and add them to spritevector. The first things
get drawn first and thus appear furthest in the background. So add the wall
first and the ship last. */
public AsteroidsGame(Asteroids owner)
{
int i;
ownerApp = owner; /* Set this right away, as makenewstars uses it. */
highScore = 0;
collideflag = true;
// Generate starry background.
makenewstars();
// Get the vector ready.
spritevector = new Vector();
// Make the wall
wall = new SpriteWall(owner);
//spritevector.addElement(wall);
/* I want to update wall, stars, then sprites, so I don't put wall in
spritevector, since the stars aren't. Probably the stars should be and
then the wall could be too. */
// Create asteroid sprites.
asteroids = new SpriteAsteroid[MAX_ASTEROIDS];
for (i = 0; i < MAX_ASTEROIDS; i++)
{
asteroids[i] = new SpriteAsteroid(owner);
spritevector.addElement(asteroids[i]);
}
// Create explosion sprites.
explosions = new SpriteExplosion[MAX_EXPLOSIONS];
explosionIndex = 0;
for (i=0; i<MAX_EXPLOSIONS; i++)
{
explosions[i] = new SpriteExplosion(owner);
spritevector.addElement(explosions[i]);
}
// Create shape for the flying saucer.
ufo = new SpriteUfo(owner);
spritevector.addElement(ufo);
// Create shape for the guided missile.
missile = new SpriteMissile(owner);
spritevector.addElement(missile);
// Create shape for the ship shapetransform.
ship = new SpriteShip(owner);
spritevector.addElement(ship);
/* Create shape for the bullet sprites. There's an odd bug that I had
for awhile. If I add the bullets to the spritevector before the ship,
then they have lower indices and they get updated and rendered after
the ship. This will make a problem as the ship's update will change
the bullet's positions with the fire function. If I don't hvae the
bullets after the ship in my master spritevector then the bullets
will be drawn at the wrong position for one update. */
bullets = new SpriteBullet[MAX_BULLETS];
bulletIndex = 0;
for (i=0; i<MAX_BULLETS; i++)
{
bullets[i] = new SpriteBullet(owner);
spritevector.addElement(bullets[i]);
}
resetGame();
stop();
}
/** Initialize the game data. */
public void resetGame()
{
score = 0;
SpriteShip.shipsLeft = MAX_SHIPS;
SpriteShip.newShipScore = NEW_SHIP_POINTS;
SpriteUfo.newUfoScore = NEW_UFO_POINTS;
ship.reset();
ufo.stop();
missile.stop();
resetAsteroids();
for (int i = 0; i<START_ASTEROIDS; i++)
asteroids[i].maxspeed =
Units.MIN_SPEED_ASTEROID;
resetBullets();
resetExplosions();
gameonflag = true;
paused = false;
}
public void makenewstars()
{
int numStars;
if (!diskflag)
numStars = (int)(STARDENSITY *
Units.WIDTH_WORLD * Units.HEIGHT_WORLD);
else
numStars = (int)(STARDENSITY * Math.PI *
Units.RADIUS_WORLD *
Units.RADIUS_WORLD);
stars = new Vector2Colored[numStars];
for (int i = 0; i < stars.length; i++)
{
stars[i] = new
Vector2Colored(ownerApp.randomizer,
Units.WIDTH_WORLD,
Units.HEIGHT_WORLD);
if (diskflag)
while
(stars[i].magnitude() > Units.RADIUS_WORLD)
stars[i]
= new Vector2Colored(ownerApp.randomizer,
Units.WIDTH_WORLD,
Units.HEIGHT_WORLD);
}
}
/** Stop ship, flying saucer, guided missile and associated sounds. */
public void stop()
{
gameonflag = false;
ship.stop();
ufo.stop();
missile.stop();
}
SpriteExplosion nextExplosion()
{
explosionIndex++;
if (explosionIndex >= MAX_EXPLOSIONS)
explosionIndex = 0;
return explosions[explosionIndex];
}
SpriteBullet nextBullet()
{
bulletIndex++;
if (bulletIndex >= MAX_BULLETS)
bulletIndex = 0;
return bullets[bulletIndex];
}
/** This returns the slot of the next non-live asteroid, or null if there's
no slot. */
SpriteAsteroid nextAsteroid()
{
for (int i=0; i<MAX_ASTEROIDS; i++)
if (asteroids[i].active == false)
return asteroids[i];
return null;
}
public void resetBullets()
{
for (int i = 0; i < MAX_BULLETS; i++)
bullets[i].reset();
bulletIndex = 0;
}
/** Create START_ASTEROIDS asteroids, make them faster than before,
put them out at the edge of the screen, and set SprieAsteroidpauseCounter. */
public void resetAsteroids()
{
int i;
for (i = 0; i<START_ASTEROIDS; i++)
{
if (asteroids[i].maxspeed <
Units.MAX_SPEED_SPRITE)
asteroids[i].maxspeed
+= Units.SPEED_INCREMENT_ASTEROIDS;
asteroids[i].reset();
}
for (i = START_ASTEROIDS; i<MAX_ASTEROIDS; i++)
asteroids[i].active = false;
SpriteAsteroid.asteroidsLeft = START_ASTEROIDS;
SpriteAsteroid.ageDoneStormpause = SpriteAsteroid.agegame +
Units.STORM_PAUSE;
}
public void resetExplosions()
{
for (int i = 0; i < MAX_EXPLOSIONS; i++)
explosions[i].reset();
explosionIndex = 0;
}
public void toggleBounce()
{
for (int i=0; i<spritevector.size(); i++)
((Sprite)spritevector.elementAt(i)).bounce =
!((Sprite)spritevector.elementAt(i)).bounce;
}
public boolean getBounce()
{
return ship.bounce;
}
/** First call advance for all sprites, then render for all sprites, and then
update for all sprites. Look at the score and add a new ship life or start
a ufo if score is high enough. If all the asteroids are destroyed create a
new batch. */
public void gameUpdate(double dt)
{
int i;
if (paused)
return;
// Move your position.
for (i=0; i<spritevector.size(); i++)
((Sprite)spritevector.elementAt(i)).advance(dt);
// Fix all the shapetransforms.
for (i=0; i<spritevector.size(); i++)
((Sprite)spritevector.elementAt(i)).render();
wall.render();
// Look at the other sprites.
for (i=0; i<spritevector.size(); i++)
((Sprite)spritevector.elementAt(i)).update();
wall.update();
/* Check the score and advance high score, add a new ship or start the
flying saucer as necessary. */
if (score > highScore)
highScore = score;
if (score > SpriteShip.newShipScore)
{ SpriteShip.newShipScore += NEW_SHIP_POINTS;
SpriteShip.shipsLeft++;
}
if (gameonflag && score > SpriteUfo.newUfoScore &&
!ufo.active)
{ SpriteUfo.newUfoScore += NEW_UFO_POINTS;
ufo.ufoPassesLeft = SpriteUfo.UFO_PASSES;
ufo.reset();
}
// If all asteroids have been destroyed create a new batch.
SpriteAsteroid.agegame += dt;
if (SpriteAsteroid.asteroidsLeft <= 0)
if (SpriteAsteroid.agegame >
SpriteAsteroid.ageDoneStormpause)
resetAsteroids();
}
/** Fill in background stars, draw the sprites. */
void draw(Graphics g, RealPixelConverter rtopc, boolean detail)
{
int i;
wall.draw(g, detail);
if (detail)
{
for (i = 0; i < stars.length; i++)
stars[i].draw(g, rtopc);
}
//Draw all the sprites.
for (i=0; i<spritevector.size(); i++)
((Sprite)spritevector.elementAt(i)).draw(g,
detail);
}
}
//END=================ASTEROIDSGAME CODE===================
//START=================ASTEROIDS CODE===================
/** Asteroids code can be used for an applet or an applciation. This works
because an Applet is a panel.
*/
public class Asteroids extends Applet implements Runnable
{
//BEGIN Asteroids static members-----------------
/** Intended size of the start window. This should match the size of the image
on the HTML page. It's used by the Asteroids Frame as well. */
static final int START_PIXELWIDTH = 400;
/** Intended size of the start window. This should match the size of the image
on the HTML page. It's used by the Asteroids Frame as well. */
static final int START_PIXELHEIGHT = 400;
//END Asteroids static members---------------
//BEGIN Asteroids members--------------------------
/** Background color. Changed by 'b' key.*/
Color backgroundcolor = Color.black;
/** Foreground color. Changed by 'b' key.*/
Color foregroundcolor = Color.white;
/** Use this to convert between the real coordinates of the Sprite shape
polygons and the pixel-sized coordinates used in the shapetransform
by the draw function. Construct it in Asteroids init(). */
RealPixelConverter realpixelconverter;
/** Use this to randomize everybody's settings. onstruct it in Asteroids init(),
feeding it the time in milliseconds so that each session is different. */
Randomizer randomizer;
//Threads
/** This thread is for loading the sounds. It seems to be giving me a lot
of problems. Gets constructed and started in Asteroids start(). */
Thread loadThread;
/** This thread is for driving the simulation updates.
Gets constructed and started in Asteroids start(). */
Thread loopThread;
/** This flag tells if you've tried loading the sounds. Leave it off
until you've finished trying to load the sounds We can eliminate sounds,
and a hanging problem I'm having, by making this true. */
boolean triedgettingsounds = false; //true;
/** Use this flag to signal if the attempt to load the sounds succeeded.
It gets set to true or to false inside the loadSounds method.*/
boolean soundsloaded;
/** A flag user can use to turn sound on and off. false is for muting.*/
boolean soundflag = true;
/** Sound clip for ship hitting an asteroid. */
AudioClip crashSound = null;
/** Sound clip for bullet blowing up an asteroid, missile, or Ufo. */
AudioClip explosionSound = null;
/** Sound clip for ship shooting a bullet. */
AudioClip fireSound = null;
/** Sound clip for the missile moving. */
AudioClip missileSound = null;
/** Sound clip for the Ufo moving. */
AudioClip saucerSound = null;
/** Sound clip for the ship's thrusters firing. */
AudioClip thrustersSound = null;
/** Sound clip for the hyperspace jump. */
AudioClip warpSound = null;
/** Use this to time how long the updates take. Our initial strategy is
just to force it to stay constant. Later we might try adaptively changing it.*/
private double timestep = Units.TIMEUNIT;
/** A flag used to toggle more detail in the image. Detail includes background
stars and filling in the asteroids. Turn this off for greater speed. */
boolean detail = true;
/** The game object that holds all of our sprites. */
AsteroidsGame game = null;
/** The font to use onscreen. */
Font font = new Font("Helvetica", Font.BOLD, 12);
FontMetrics fontmetric;
int fontWidth;
int fontHeight;
String currentsoundfilename = "";
/** Size of the offscreen buffer.*/
Dimension offDimension;
/** Image of the offscreen buffer.*/
Image offImage;
/** Graphics of the offscreen buffer.*/
Graphics offGraphics = null;
/**Flag for whether this is being run as a true applet or simply as an application.
It makes a difference in the loadsounds method. */
boolean trueapplet = true;
//END of Asteroids members--------------------------
/** The conversion factor used in strectching or shrinking the real length
to a pixel length. */
double scale(){return realpixelconverter.scale();}
/**Accessor to the current pixel dimensions of the view. */
double widthpixel(){return realpixelconverter.widthpixel();}
/**Accessor to the current pixel dimensions of the view. */
double heightpixel(){return realpixelconverter.heightpixel();}
/** Mutator to tell the converter about a new pixel view window size. */
public void setPixelWindow(Dimension d)
{realpixelconverter.setPixelWindow(d.width, d.height);}
/** Returns applet information. */
public String getAppletInfo()
{
return("Asteroids, Copyright 1998 by Mike Hall. Copyright 1999 by
Rudy Rucker.");
}
/** In order for the panel to be able to get focus so it can use its
KeyListener, I have to do this overload. (By default panels can't get focus.)*/
public boolean isFocusTraversable()
{ return true;
}
/* Constructor. Leave the work for init. Just have it here as a matter
of interest so we can trace if and when the various browsers invoke it. */
public Asteroids()
{
super();
System.out.println("Called Asteroids constructor.");
}
/** Init does the work of a constructor. It constructs the
three nontrivial members of an Asteroids object: the realpixelconverter,
the randomizer, and the game.*/
public void init()
{
realpixelconverter = new RealPixelConverter( Units.WIDTH_WORLD,
Units.HEIGHT_WORLD, START_PIXELWIDTH,
START_PIXELHEIGHT);
randomizer = new Randomizer(System.currentTimeMillis());
game = new AsteroidsGame(this);
//Try to get the focus.
requestFocus();
// Take credit.
System.out.println("Called Asteroids init method. Asteroids,
Copyright 1998 by Mike Hall. Copyright 1999 by Rudy Rucker.");
}
/** Load the sounds by starting a loadThread, also start the main loopThread. */
public void start()
{
if (loopThread == null)
{ loopThread = new Thread(this);
loopThread.start();
}
if (!triedgettingsounds && loadThread == null)
{ loadThread = new Thread(this);
loadThread.start();
}
requestFocus();
System.out.println("Called Asteroids start method.");
}
/** Stop the loadThread and the loopThread by setting them to null. */
public void stop()
{
if (loopThread != null)
{
//loopThread.stop(); //Deprecated.
loopThread = null;
}
if (loadThread != null)
{
//loadThread.stop(); //Deprecated.
loadThread = null;
}
System.out.println("Called Asteroids stop method.");
}
/** This uses the loadSound(AudioClip, String) function which has pieces to
comment in and out depending on the JDK being used. */
public void loadSounds()
{
crashSound = loadSound("crash.au");
explosionSound = loadSound("explosion.au");
fireSound = loadSound("fire.au");
/*Note that although we use the dictionary spelling "missile"
in the code,
but the existing sound file has the "missle" spelling, so
we'll stick with
that for consistency with already-distributed files.*/
missileSound = loadSound("missle.au");
thrustersSound = loadSound("thrusters.au");
saucerSound = loadSound("saucer.au");
warpSound = loadSound("warp.au");
//Will be set to false if any loadSound call failed.
soundsloaded = (crashSound != null) && (explosionSound != null)
&&
(fireSound != null) && (missileSound !=
null) &&
(thrustersSound != null) &&
(saucerSound != null) &&
(warpSound != null);
System.out.println("Finished Asteroids loadSounds()
method.");
}
/** This function tries to load a soundfile and return an audioclip. While
it's at work, it sets the currentsoundname to the soundname for reporting.
If it fails, it returns null.
The loadSound function has complications. (1) The method we use to load
sounds for a true Asteroids applet object (that is, one initialized from within
a web browser) won't work for a fake Asteroids applet object (that is, one
initialized by a call to new Asteroids() from within the public main function).
So we have a trueapplet switch. (2) The application sound-loadng method only
works in Java 1.2, so this code is inside a JDK.version==2 block.
(3) Java 1.0 can't load sounds from a jar file, so we need a separate version
for this.*/
public AudioClip loadSound(String soundname)
{
AudioClip audioclip = null;
//Set this Asteroids field for screen status report.
currentsoundfilename = soundname;
if (trueapplet)
{
//BEGIN Java 1.0 code =================
if (JDK.version == 0)
{
try { audioclip = getAudioClip(new
URL(getCodeBase(), soundname)); }
catch (MalformedURLException e)
{
System.out.println("Can't
load " + soundname + ".");
return
null;
}
}
//END Java 1.0 code ======================
/* The following code can read the *.au out of a jar file. It doesn't
compile in JDK 1.0. Note that we ONLY use this code if we are planning
to deploy the build in a JAR file. */
//BEGIN COMMENT OUT for JDK 1.0 ====================
if (JDK.version != 0)
{
try
{
URL
url = Asteroids.class.getResource(soundname);
audioclip
= getAudioClip(url);
}
catch (Exception e)
{
System.out.println("Can't
find "+ soundname +" inside Jar file.");
return
null;
}
}
//END COMMENT OUT for JDK 1.0 ===========================
}
else // the !trueapplet case, that is, the application case.
{
/* Application version of the sound-loading
code. The static Applet method
newAudioClip is only available with Java 1.2,
so this must be commented out for
a Java 1.1 build. See comment before the
loadSounds declaration. */
//BEGIN COMMENT OUT FOR JDK 1.0 and JDK 1.1 ====================
/*
if ( JDK.version == 2)
{
try //the URL
constructor used here requires exception handling.
{
audioclip
= Applet.newAudioClip(new URL("file", "localhost",
soundfilename);
}
catch
(MalformedURLException e)
{
System.out.println("Can't
load " + soundfilename + ".");
return
null;
}
}
*/
//END COMMENT OUT FOR JDK 1.0 and JDK 1.1 ==================
} //End !trueapplet case.
//Check that you really did get the audioclip.
if (audioclip == null)
{
System.out.println("Can't load " +
soundname + ".");
return null;
}
//Now wake the sound up to hopefully get it into cache or RAM.
audioclip.play();
audioclip.stop();
System.out.println("Loaded " + soundname + ".");
return audioclip;
}
/** First use the loadThread to load the sounds, then set loadThread to null and
use a while loop on loopthread to repeatedly update the sprites and call repaint.*/
public void run()
{
int i, j;
long startTime, currentTime;
// Lower this thread's priority
// Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
// Run thread for loading sounds.
if (!triedgettingsounds && Thread.currentThread() ==
loadThread)
{
loadSounds(); //Loads sounds and sets
soundsloaded.
triedgettingsounds = true;
//loadThread.stop(); //Deprecated.
loadThread = null;
return;
}
/* This is the main loop. Before entering for the first time,
initialize the starting time. */
startTime = System.currentTimeMillis();
while (Thread.currentThread() == loopThread)
{
/* update the new screensize so the sprites can
use it */
Dimension d = size(); //Default deprecated JDK
1.0 method.
//BEGIN COMMENT OUT FOR JDK 1.0=======
if (JDK.version != 0)
d = getSize();
//END COMMENT OUT FOR JDK 1.0=======
setPixelWindow(d);
game.gameUpdate(timestep);
// Update the screen and set the timer for the next loop.
repaint(); //This forces a call to
paintComponent
startTime += 1000.0 * timestep; //convert
timestep to milliseconds
currentTime = System.currentTimeMillis();
try
{
Thread.sleep(Math.max(0,
startTime - currentTime)); /* In case
currentTime > startTime, the difference is negative, this happens
at startup sometimes. Taking Math.max fixes this problem. */
}
catch (InterruptedException e)
{
System.out.println("Exception in
loopThread sleep.");
break;
}
} //End of while loop.
}
/** Call the ship.keyPressed method and let the ship use the arrow keys,
the spacebar and the h key. Then process possible m, p, or s keys. */
public boolean keyDown(Event e, int key)
{
game.ship.keyPressed(key);
Color currentfgColor;
Color currentbgColor;
// 'B' key: toggle Background color
if (key == 98)
{
if (backgroundcolor == Color.white)
{backgroundcolor = Color.black; foregroundcolor = Color.white;}
else
{foregroundcolor = Color.black; backgroundcolor = Color.white;}
for (int i=0; i<game.spritevector.size(); i++)
((Sprite)game.spritevector.elementAt(i)).edgecolor =
foregroundcolor;
game.ship.fillcolor = backgroundcolor;
game.wall.fillcolor = backgroundcolor;
game.wall.edgecolor = foregroundcolor;
}
// 'C' key: toggle collisions
if (key == 99)
game.collideflag = !game.collideflag;
// 'D' key: toggle graphics detail on or off.
if (key == 100)
detail = !detail;
// 'G' key: toggle godmode on or off.
if (key == 103)
SpriteShip.godmode = !SpriteShip.godmode;
// 'M' key: toggle Asteroids.soundflag on or off and stop any looping sound clips.
if (key == 109 && soundsloaded && triedgettingsounds)
{
soundflag = !soundflag;
if (!soundflag)
{
crashSound.stop();
explosionSound.stop();
fireSound.stop();
missileSound.stop();
saucerSound.stop();
thrustersSound.stop();
warpSound.stop();
}
else if (!game.paused)
{
game.ship.loopsound(game.ship.soundplaying);
game.ufo.loopsound(game.ufo.soundplaying);
game.missile.loopsound(game.missile.soundplaying);
}
}
// 'P' key: toggle pause mode and start or stop any active looping sound clips.
if (key == 112)
{
game.paused = !game.paused;
if (!game.paused && soundflag
&& triedgettingsounds)
{ if
(game.missile.soundplaying)
missileSound.loop();
if
(game.ufo.soundplaying)
saucerSound.loop();
if
(game.ship.soundplaying)
thrustersSound.loop();
}
else //paused
{ if
(game.missile.soundplaying && soundsloaded && triedgettingsounds)
missileSound.stop();
if
(game.ufo.soundplaying && soundsloaded && triedgettingsounds)
saucerSound.stop();
if
(game.ship.soundplaying && soundsloaded && triedgettingsounds)
thrustersSound.stop();
}
}
// 'R' key: toggle round diskworld on and off.
if (key == 114)
{
game.diskflag = !game.diskflag;
game.makenewstars();
}
// 'S' key: start the game, if not already in progress.
if (key == 115 && triedgettingsounds &&
!game.gameonflag)
game.resetGame();
// 'W' Key: toggle sprites wrapping
if (key == 119)
game.toggleBounce();
return true;
}
/** Call the ship.keyPressed method and let the ship use the arrow keys,
the spacebar and the h key. Then process possible m, p, or s keys. */
public boolean keyUp(Event e, int key)
{
game.ship.keyReleased(key);
return true;
}
public void paint(Graphics g) //Use in 1.1. instead of paintComponent
{
super.paint(g);
update(g);
}
public void update(Graphics g)
{
Dimension d = size(); //Default deprecated JDK 1.0 method.
//BEGIN COMMENT OUT FOR JDK 1.0=======
if (JDK.version != 0)
d = getSize();
//END COMMENT OUT FOR JDK 1.0=======
int i;
String s;
Color fontColor;
// Create the offscreen graphics context, if no good one exists.
if (offGraphics == null || d.width != offDimension.width ||
d.height != offDimension.height)
{ offDimension = d;
offImage = createImage(d.width, d.height);
offGraphics = offImage.getGraphics();
realpixelconverter.setPixelWindow(d.width,
d.height);
}
//Fix the font stuff (Application only, Applet does in init()).
offGraphics.setFont(font);
fontmetric = offGraphics.getFontMetrics();
fontWidth = fontmetric.getMaxAdvance();
fontHeight = fontmetric.getHeight();
/* Draw the background. When doing disk, make the background the "wrong"
color because I'll use the "right" color wihtin the disk, and this background
is just for the leftover bits between the disk and the rectangular panel
edges. We may flip the color when the ship is paused between lives for drama. */
if (!game.diskflag)
offGraphics.setColor(backgroundcolor);
else
offGraphics.setColor(game.ship.edgecolor);
offGraphics.fillRect(0, 0, d.width, d.height);
//Draw the wall, the starsand the sprites.
game.draw(offGraphics,realpixelconverter, detail);
// Display status and messages.
offGraphics.setFont(font);
offGraphics.setColor(Color.lightGray);
/* if (game.diskflag)
offGraphics.setXORMode(backgroundcolor);
//Java XOR looks bad in text unfortunately.
*/
offGraphics.drawString("Score: " + game.score, fontWidth,
fontHeight);
if (SpriteShip.godmode)
offGraphics.drawString("Godmode",
fontWidth, d.height - fontHeight);
else
offGraphics.drawString("Ships: " +
SpriteShip.shipsLeft, fontWidth, d.height - fontHeight);
s = "High: " + game.highScore;
offGraphics.drawString(s, d.width - (fontWidth +
fontmetric.stringWidth(s)), fontHeight);
if (!soundflag && soundsloaded)
{ s = "Mute";
offGraphics.drawString(s, d.width - (fontWidth
+ fontmetric.stringWidth(s)), d.height - fontHeight);
}
if (!soundsloaded)
{ s = "No Sounds Loaded";
offGraphics.drawString(s, d.width - (fontWidth
+ fontmetric.stringWidth(s)), d.height - fontHeight);
}
if (!game.gameonflag)
{ s = "* A S T E R O I D S * A L I V E *
12/22/99";
offGraphics.drawString(s, (d.width -
fontmetric.stringWidth(s)) / 2, d.height / 2);
s = "Copyright 1998 by Mike Hall.
Copyright 1999 by Rudy Rucker.";
offGraphics.drawString(s, (d.width -
fontmetric.stringWidth(s)) / 2, d.height / 2 + fontHeight);
if (!triedgettingsounds)
{ s = "Loading " +
currentsoundfilename + " sound...";
offGraphics.drawString(s,
(d.width - fontmetric.stringWidth(s)) / 2, d.height / 4);
}
else
{ s = "Ready to Start
New Game";
offGraphics.drawString(s,
(d.width - fontmetric.stringWidth(s)) / 2,
d.height
/ 4);
s = "'S' to
Start.";
offGraphics.drawString(s,
(d.width - fontmetric.stringWidth(s)) / 2,
d.height
/ 4 + fontHeight);
s = "Left/Right
keys turn, Up/Down thrust, Spacebar shoots.";
offGraphics.drawString(s,
(d.width - fontmetric.stringWidth(s)) / 2,
d.height
/ 4 + 2* fontHeight);
s = "'B' Black,
'C' Collide, 'G' Godmode, 'H' hyperjump,";
offGraphics.drawString(s,
(d.width - fontmetric.stringWidth(s)) / 2,
d.height
/ 4 + 3 * fontHeight);
s = "'R' Round,
'W' Wrap, 'M' Mute, 'P' Pause.";
offGraphics.drawString(s,
(d.width - fontmetric.stringWidth(s)) / 2,
d.height
/ 4 + 4 * fontHeight);
}
}
else if (game.paused)
{ s = "Game Paused";
offGraphics.drawString(s, (d.width -
fontmetric.stringWidth(s)) / 2, d.height / 4);
}
// Copy the offscreen buffer to the screen.
g.drawImage(offImage, 0, 0, this);
} //end of update method code.
/**The main function is used by the application, but ignored by the applet.
It creates an Asteroids applet, makes an AsteroidsFrame for the applet, and
calls the applet's init, start, and run methods. We need to set the applet's
trueapplet flag to false so that we don't try and use a loadsound method that
only happens to work for true applets. I don't think we need to
comment this method out in the Applet build for fear it causes a Java security
violation.*/
//BEGIN Application Code ONLY ========
//BEGIN COMMENT out for JDK 1.0
/*
public static void main(String[] args)
{
Asteroids applet = new Asteroids();
applet.trueapplet = false;
Frame frame = new AsteroidsFrame(applet);
frame.show();
}
*/
//END COMMENT out for JDK 1.0
//END Application Code ONLY======================
} //end of Asteroids class code
//END=================ASTEROIDS CODE===================
//START=================ASTEROIDSFRAME CODE===================
//BEGIN Application Code for Java 1.1 or higher ONLY====================
//BEGIN COMMENT out for JDK 1.0
/*
class AsteroidsFrame extends Frame
{
public AsteroidsFrame(Asteroids applet)
{ setTitle("Asteroids 11, December 22,
1999");
setSize(Asteroids.START_PIXELWIDTH,
Asteroids.START_PIXELHEIGHT);
addWindowListener( new WindowAdapter()
{ public void
windowClosing(WindowEvent e)
{
System.exit(0);
}
} );
applet.init();
applet.start();
applet.run();
add("Center", applet);
applet.requestFocus();
}
}
*/
//END COMMENT out for JDK 1.0
//END Application Code for Java 1.1 or higher ONLY================
//END=================ASTEROIDSFRAME CODE=================== |
|