import java.awt.*; import java.util.Random; import java.util.Hashtable; import java.lang.Math; import java.lang.Thread; /** An onscreen "sprite". Named "HardlySprite" because it operates on clear backgrounds -- ie, I haven't taken on the challenge of moving cleanly over an image background. HardlySprites are added to a SpriteWorld, which takes responsibility for moving and imaging the sprites and detecing collisions between them. HardlySprites are given real-world speeds in pixels-per-second, so they should behave consistenly across platforms, with slower systems just animating them less smoothly. */ public class HardlySprite extends Object { final static float FLOAT_PI = (float) Math.PI; final static float TWOPI = (float) (2.0f * Math.PI); Image spriteImage; SpriteWorld world; int lastx, lasty; float x, y; float dx, dy; // x and y move per second float dthrustx, dthrusty; boolean isThrusting; float lastThrust; long inTime, timeSlice, testSlice, lastSlice; private float speed; int height, width; Color bg; Rectangle hotBox; private boolean needsUnpaint = false; // addresses pixel-bleed in WinNetscape and on slow systems // float distToMove; // junk variable -- hypotenuse during update float timeSliceSeconds; /** Creates a HardlySprite. The sprite won't appear onscreen until it's add()-ed to a SpriteWorld and that SpriteWorld is start()-ed. @param spriteImage The sprite's on-screen image @param world The SpriteWorld the sprite is to be associated with @param x Initial x-coordinate, in pixels @param y Initial y-coordinate, in pixels @param direction Initial movement direction, in radians, from 0 to 2 PI, with 0 as due right @param speed Inital onscreen speed, in pixels per second */ public HardlySprite (Image spriteImage, SpriteWorld world, int x, int y, float direction, float speed) { this.spriteImage = spriteImage; this.world = world; this.x = (float) x; this.y = (float) y; // this.speed = speed; // setDirection (direction); dx = speed * (float) Math.cos (direction); dy = speed * (float) Math.sin (direction); width = spriteImage.getWidth(world); height = spriteImage.getHeight(world); bg = world.getBackground(); // used only in emergency unpaint hotBox = new Rectangle (x, y, width, height); lastSlice = System.currentTimeMillis(); needsUnpaint = false; boolean isThrusting = false; lastThrust = java.lang.Float.NaN; } // full constructor /** Creates a HardlySprite. The sprite won't appear onscreen until it's add()-ed to a SpriteWorld and that SpriteWorld is start()-ed. This constructor accepts degrees instead of the standard radians. @param spriteImage The sprite's on-screen image @param world The SpriteWorld the sprite is to be associated with @param x Initial x-coordinate, in pixels @param y Initial y-coordinate, in pixels @param direction Initial movement direction, in degrees, from 0 to 360, with 0 as due right @param speed Inital onscreen speed, in pixels per second */ public HardlySprite (Image spriteImage, SpriteWorld world, int x, int y, int direction, float speed) { this (spriteImage, world, x, y, (direction * TWOPI / 360), speed); } // full constructor with degrees, sted radians /** Creates a HardlySprite. The sprite won't appear onscreen until it's add()-ed to a SpriteWorld and that SpriteWorld is start()-ed. This constructor has no speed or direction, defaulting to 0 and 0 respectively @param spriteImage The sprite's on-screen image @param world The SpriteWorld the sprite is to be associated with @param x Initial x-coordinate, in pixels @param y Initial y-coordinate, in pixels */ public HardlySprite (Image spriteImage, SpriteWorld world, int x, int y) { this (spriteImage, world, x, y, 0, 0); } // constructor without speed or direction /** Creates a HardlySprite. The sprite won't appear onscreen until it's add()-ed to a SpriteWorld and that SpriteWorld is start()-ed. This constructor has no speed, direction, x, or y, defaulting to an initial position of (0,0), moving 0 pixels per second, due right. @param spriteImage The sprite's on-screen image @param world The SpriteWorld the sprite is to be associated with */ public HardlySprite (Image spriteImage, SpriteWorld world) { this (spriteImage, world, 0, 0, 0, 0); } // constructor without speed, direction, or position /** Sets the sprite's direction @param direction The new direction, in radians, from 0 to 2 PI, with 0 being due right */ public void setDirection (float direction) { float newDirection = direction; float currSpeed = getSpeed(); if (newDirection > TWOPI) { newDirection %= TWOPI; } if (newDirection < 0) { newDirection = TWOPI - newDirection; } dx = currSpeed * (float) Math.cos (newDirection); dy = currSpeed * (float) Math.sin (newDirection); } /** Sets the sprite's direction @param direction The new direction, in degrees, from 0 to 360, with 0 being due right */ public void setDirection (int degrees) { this.setDirection (degrees * TWOPI / 360 ); // radians suck } /** Sets the sprite's speed @param speed The new speed, in pixels per second */ // icky math here. Imagine right triangle with width dx, heigh dy, hypotenuse speed. // new triangle similarly. sin (theta) = old dy / old speed = new dy / new speed // new dy is (new speed / old dpeed) * old y. similarly for dx public final void setSpeed (float speed) { float oldSpeed = this.getSpeed(); if (oldSpeed == 0) { return; // Avoid division by zero. } else { float changeFactor = speed/oldSpeed; dx *= changeFactor; dy *= changeFactor; } } /** Returns the sprite's current direction of movement @return The current direction, in radians, with 0 being due right */ public final float getDirection () { float computedDirection; if (dx == 0) { return (dy < 0) ? (0.5f * FLOAT_PI) : (1.5f * FLOAT_PI); } else { computedDirection = (float) Math.atan (dy/dx); if (dx < 0) { return FLOAT_PI + computedDirection; } else if (dy < 0) { return TWOPI + computedDirection; } return computedDirection; } } /** Returns the sprite's current speed @return The current speed, in pixels per second */ public final float getSpeed () { return (float) (Math.sqrt ( (dx * dx) + (dy * dy))); } public final void setThrusting (float tDirection, float tSpeed) { isThrusting = true; dthrustx = (float) (tSpeed * Math.cos (tDirection)); dthrusty = (float) (tSpeed * Math.sin (tDirection)); // speed = getSpeed(); } //setThrusting public final void stopThrusting () { isThrusting = false; dthrustx = 0f; dthrusty = 0f; } public final boolean isThrusting() { return this.isThrusting; } private final void applyThrust() { dx += dthrustx * timeSliceSeconds; dy += dthrusty * timeSliceSeconds; } /** Returns the sprite's current x-coordinate. This represents the LEFT side of the sprite's onscreen image @return The x-coordinate */ public final float getX () { return x; } /** Returns the sprite's current y-coordinate. This represents the TOP side of the sprite's onscreen image @return The y-coordinate */ public final float getY () { return y; } /** Sets sprite's x-coordinate to the given value @param x The new x-coordinate */ public final void setX (float x) { this.x = x; hotBox.move((int) x, (int) y); } /** Sets sprite's y-coordinate to the given value @param y The new y-coordinate */ public final void setY (float y) { this.y = y; hotBox.move((int) x, (int) y); } /** Returns the sprite's width, in pixels @return The sprite's width */ public final int getWidth () { return width; } /** Returns the sprite's height, in pixels @return The sprite's width */ public final int getHeight () { return height; } /** Changes the sprite's image and notes its new size @param newImage The sprite's new on-screen Image */ public final void changeImage (Image newImage) { spriteImage = newImage; width = spriteImage.getWidth(world); height = spriteImage.getHeight(world); hotBox.resize (width, height); } /** Erases the sprite from its last position. Used by SpriteWorld when updating and moving a sprite. @param g The graphics context (presumably SpriteWorld's offscreen buffer) to unpaint in @param c The background color. Currently unused -- the method originally called fillRect() to erase, but now uses the not-quite-standard g.clearRect(). */ public final void unpaintLast (Graphics g, Color c) { g.fillRect (lastx, lasty, width, height); needsUnpaint = false; } /** Erases the sprite from its current position. Useful if your applet wants to immediately erase the sprite (eg, if you're killing the sprite). @param g The graphics context (presumably SpriteWorld's offscreen buffer) to unpaint in @param c The background color. Currently unused -- the method originally called fillRect() to erase, but now uses the not-quite-standard g.clearRect(). */ public final void unpaintCurrent (Graphics g, Color c) { g.fillRect ((int) x, (int) y, width, height); } /** Called by SpriteWorld's animation thread. Checks to see if the sprite needs to be unpainted (and calls unpaintLast() if it does), then paints the sprite at its current position. @param g The graphics context (presumably SpriteWorld's offscreen buffer) to unpaint in */ public final void paint (Graphics g) { if (needsUnpaint) { unpaintLast (g, bg); } g.drawImage (spriteImage, (int) x, (int) y, world); needsUnpaint = true; } /** Called by SpriteWorld's animation thread. Calculates the distance the sprite has moved since the startup or the last call to this method, and sets x, y, lastx, lasty, and hotbox accordingly. Subclasses should call this method with super.updatePosition() and may then do their work based on inTime (time when updatePosition was called) and/or timeSlice (time elapsed since last call). @return true if HardlySprite still exists and may be drawn. Always returns true. Subclasses should return false if they remove() sprite from the SpriteWorld (eg, an animation that ends, a timed sprite that reaches its expiration, etc.) */ public boolean updatePosition() { // get time elapsed since last update inTime = System.currentTimeMillis(); timeSliceSeconds = (inTime - lastSlice) / 1000.0f; if (isThrusting) { applyThrust(); } lastx = (int) x; lasty = (int) y; x += dx * timeSliceSeconds; y += dy * timeSliceSeconds; hotBox.move((int) x, (int) y); lastSlice = System.currentTimeMillis(); return true; } // updatePosition } // class HardlySprite