|
/*
DEVELOPING GAME IN JAVA
Caracteristiques
Editeur : NEW RIDERS
Auteur : BRACKEEN
Parution : 09 2003
Pages : 972
Isbn : 1-59273-005-1
Reliure : Paperback
Disponibilite : Disponible a la librairie
*/
import java.awt.AWTException;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.DisplayMode;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferUShort;
import java.awt.image.IndexColorModel;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import java.awt.Graphics2D;
import java.awt.Color;
import java.io.IOException;
import java.util.*;
public class CollisionTestWithSliding extends ShooterCore {
public static void main(String[] args) {
new CollisionTestWithSliding(args).run();
}
protected BSPTree bspTree;
protected String mapFile;
public CollisionTestWithSliding(String[] args) {
super(args);
for (int i = 0; mapFile == null && i < args.length; i++) {
if (mapFile == null && !args[i].startsWith("-")) {
mapFile = args[i];
}
}
if (mapFile == null) {
mapFile = "../images/sample.map";
}
}
public void createPolygons() {
Graphics2D g = screen.getGraphics();
g.setColor(Color.BLACK);
g.fillRect(0, 0, screen.getWidth(), screen.getHeight());
g.setColor(Color.WHITE);
g.drawString("Loading...", 5, screen.getHeight() - 5);
screen.update();
float ambientLightIntensity = .2f;
List lights = new LinkedList();
lights.add(new PointLight3D(-100, 100, 100, .3f, -1));
lights.add(new PointLight3D(100, 100, 0, .3f, -1));
MapLoader loader = new MapLoader();
loader.setObjectLights(lights, ambientLightIntensity);
try {
bspTree = loader.loadMap(mapFile);
} catch (IOException ex) {
ex.printStackTrace();
}
CollisionDetection collisionDetection = new CollisionDetectionWithSliding(
bspTree);
gameObjectManager = new GridGameObjectManager(bspTree.calcBounds(),
collisionDetection);
gameObjectManager.addPlayer(new Player());
((BSPRenderer) polygonRenderer).setGameObjectManager(gameObjectManager);
createGameObjects(loader.getObjectsInMap());
Transform3D start = loader.getPlayerStartLocation();
gameObjectManager.getPlayer().getTransform().setTo(start);
}
private void createGameObjects(List mapObjects) {
Iterator i = mapObjects.iterator();
while (i.hasNext()) {
PolygonGroup group = (PolygonGroup) i.next();
String filename = group.getFilename();
if ("robot.obj".equals(filename)) {
gameObjectManager.add(new Bot(group));
} else {
// static object
gameObjectManager.add(new GameObject(group));
}
}
}
public void drawPolygons(Graphics2D g) {
polygonRenderer.startFrame(g);
// draw polygons in bsp tree (set z buffer)
((BSPRenderer) polygonRenderer).draw(g, bspTree);
// draw game object polygons (check and set z buffer)
gameObjectManager.draw(g, (GameObjectRenderer) polygonRenderer);
polygonRenderer.endFrame(g);
}
}
/**
* The CollisionDetectionWithSliding class handles collision detection between
* the GameObjects, and between GameObjects and a BSP tree. When a collision
* occurs, the GameObject slides to the side rather than stops.
*/
class CollisionDetectionWithSliding extends CollisionDetection {
private Vector3D scratch = new Vector3D();
private Vector3D originalLocation = new Vector3D();
/**
* Creates a new CollisionDetectionWithSliding object for the specified BSP
* tree.
*/
public CollisionDetectionWithSliding(BSPTree bspTree) {
super(bspTree);
}
/**
* Checks for a game object collision with the walls of the BSP tree.
* Returns the first wall collided with, or null if there was no collision.
* If there is a collision, the object slides along the wall and again
* checks for a collision. If a collision occurs on the slide, the object
* reverts back to its old location.
*/
public BSPPolygon checkWalls(GameObject object, Vector3D oldLocation,
long elapsedTime) {
float goalX = object.getX();
float goalZ = object.getZ();
BSPPolygon wall = super.checkWalls(object, oldLocation, elapsedTime);
// if collision found and object didn't stop itself
if (wall != null && object.getTransform().isMoving()) {
float actualX = object.getX();
float actualZ = object.getZ();
// dot product between wall's normal and line to goal
scratch.setTo(actualX, 0, actualZ);
scratch.subtract(goalX, 0, goalZ);
float length = scratch.getDotProduct(wall.getNormal());
float slideX = goalX + length * wall.getNormal().x;
float slideZ = goalZ + length * wall.getNormal().z;
object.getLocation().setTo(slideX, object.getY(), slideZ);
originalLocation.setTo(oldLocation);
oldLocation.setTo(actualX, oldLocation.y, actualZ);
// use a smaller radius for sliding
PolygonGroupBounds bounds = object.getBounds();
float originalRadius = bounds.getRadius();
bounds.setRadius(originalRadius - 1);
// check for collision with slide position
BSPPolygon wall2 = super.checkWalls(object, oldLocation,
elapsedTime);
// restore changed parameters
oldLocation.setTo(originalLocation);
bounds.setRadius(originalRadius);
if (wall2 != null) {
object.getLocation().setTo(actualX, object.getY(), actualZ);
return wall2;
}
}
return wall;
}
/**
* Checks for object collisions with the floor and ceiling. Uses
* object.getFloorHeight() and object.getCeilHeight() for the floor and
* ceiling values. Applies gravity if the object is above the floor, and
* scoots the object up if the player is below the floor (for smooth
* movement up stairs).
*/
protected void checkFloorAndCeiling(GameObject object, long elapsedTime) {
float floorHeight = object.getFloorHeight();
float ceilHeight = object.getCeilHeight();
float bottomHeight = object.getBounds().getBottomHeight();
float topHeight = object.getBounds().getTopHeight();
Vector3D v = object.getTransform().getVelocity();
Physics physics = Physics.getInstance();
// check if on floor
if (object.getY() + bottomHeight == floorHeight) {
if (v.y < 0) {
v.y = 0;
}
}
// check if below floor
else if (object.getY() + bottomHeight < floorHeight) {
if (!object.isFlying()) {
// if falling
if (v.y < 0) {
object.notifyFloorCollision();
v.y = 0;
object.getLocation().y = floorHeight - bottomHeight;
} else if (!object.isJumping()) {
physics.scootUp(object, elapsedTime);
}
} else {
object.notifyFloorCollision();
v.y = 0;
object.getLocation().y = floorHeight - bottomHeight;
}
}
// check if hitting ceiling
else if (object.getY() + topHeight > ceilHeight) {
object.notifyCeilingCollision();
if (v.y > 0) {
v.y = 0;
}
object.getLocation().y = ceilHeight - topHeight;
if (!object.isFlying()) {
physics.applyGravity(object, elapsedTime);
}
}
// above floor
else {
if (!object.isFlying()) {
// if scooting-up, stop the scoot
if (v.y > 0 && !object.isJumping()) {
v.y = 0;
object.getLocation().y = floorHeight - bottomHeight;
} else {
physics.applyGravity(object, elapsedTime);
}
}
}
}
/**
* Handles an object collision. Object A is the moving object, and Object B
* is the object that Object A collided with. Object A slides around or
* steps on top of Object B if possible.
*/
protected boolean handleObjectCollision(GameObject objectA,
GameObject objectB, float distSq, float minDistSq,
Vector3D oldLocation) {
objectA.notifyObjectCollision(objectB);
if (objectA.isFlying()) {
return true;
}
float stepSize = objectA.getBounds().getTopHeight() / 6;
Vector3D velocity = objectA.getTransform().getVelocity();
// step up on top of object if possible
float objectABottom = objectA.getY()
+ objectA.getBounds().getBottomHeight();
float objectBTop = objectB.getY() + objectB.getBounds().getTopHeight();
if (objectABottom + stepSize > objectBTop
&& objectBTop + objectA.getBounds().getTopHeight() < objectA
.getCeilHeight()) {
objectA.getLocation().y = (objectBTop - objectA.getBounds()
.getBottomHeight());
if (velocity.y < 0) {
objectA.setJumping(false);
// don't let gravity get out of control
velocity.y = -.01f;
}
return false;
}
if (objectA.getX() != oldLocation.x || objectA.getZ() != oldLocation.z) {
// slide to the side
float slideDistFactor = (float) Math.sqrt(minDistSq / distSq) - 1;
scratch.setTo(objectA.getX(), 0, objectA.getZ());
scratch.subtract(objectB.getX(), 0, objectB.getZ());
scratch.multiply(slideDistFactor);
objectA.getLocation().add(scratch);
// revert location if passing through a wall
if (super.checkWalls(objectA, oldLocation, 0) != null) {
return true;
}
return false;
}
return true;
}
}
/**
* A GameObject that can jump.
*/
class JumpingGameObject extends GameObject {
public static final float DEFAULT_JUMP_HEIGHT = 64;
protected float jumpVelocity;
public JumpingGameObject(PolygonGroup group) {
super(group);
setJumpHeight(DEFAULT_JUMP_HEIGHT);
}
/**
* Sets how high this GameObject can jump.
*/
public void setJumpHeight(float jumpHeight) {
jumpVelocity = Physics.getInstance().getJumpVelocity(jumpHeight);
}
/**
* Causes this GameObject to jump if the jumping flag is set and this object
* is not already jumping.
*/
public void setJumping(boolean isJumping) {
if (isJumping() != isJumping) {
super.setJumping(isJumping);
if (isJumping) {
Physics.getInstance().jump(this, jumpVelocity);
}
}
}
public void notifyFloorCollision() {
// the object has landed.
setJumping(false);
}
}
/**
* The Physics class is a singleton that represents various attributes (like
* gravity) and the functions to manipulate objects based on those physical
* attributes. Currently, only gravity and scoot-up (acceleration when traveling
* up stairs) are supported.
*/
class Physics {
/**
* Default gravity in units per millisecond squared
*/
public static final float DEFAULT_GRAVITY_ACCEL = -.002f;
/**
* Default scoot-up (acceleration traveling up stairs) in units per
* millisecond squared.
*/
public static final float DEFAULT_SCOOT_ACCEL = .006f;
private static Physics instance;
private float gravityAccel;
private float scootAccel;
private Vector3D velocity = new Vector3D();
/**
* Gets the Physics instance. If a Physics instance does not yet exist, one
* is created with the default attributes.
*/
public static synchronized Physics getInstance() {
if (instance == null) {
instance = new Physics();
}
return instance;
}
protected Physics() {
gravityAccel = DEFAULT_GRAVITY_ACCEL;
scootAccel = DEFAULT_SCOOT_ACCEL;
}
/**
* Gets the gravity acceleration in units per millisecond squared.
*/
public float getGravityAccel() {
return gravityAccel;
}
/**
* Sets the gravity acceleration in units per millisecond squared.
*/
public void setGravityAccel(float gravityAccel) {
this.gravityAccel = gravityAccel;
}
/**
* Gets the scoot-up acceleration in units per millisecond squared. The
* scoot up acceleration can be used for smoothly traveling up stairs.
*/
public float getScootAccel() {
return scootAccel;
}
/**
* Sets the scoot-up acceleration in units per millisecond squared. The
* scoot up acceleration can be used for smoothly traveling up stairs.
*/
public void setScootAccel(float scootAccel) {
this.scootAccel = scootAccel;
}
/**
* Applies gravity to the specified GameObject according to the amount of
* time that has passed.
*/
public void applyGravity(GameObject object, long elapsedTime) {
velocity.setTo(0, gravityAccel * elapsedTime, 0);
object.getTransform().addVelocity(velocity);
}
/**
* Applies the scoot-up acceleration to the specified GameObject according
* to the amount of time that has passed.
*/
public void scootUp(GameObject object, long elapsedTime) {
velocity.setTo(0, scootAccel * elapsedTime, 0);
object.getTransform().addVelocity(velocity);
}
/**
* Applies the negative scoot-up acceleration to the specified GameObject
* according to the amount of time that has passed.
*/
public void scootDown(GameObject object, long elapsedTime) {
velocity.setTo(0, -scootAccel * elapsedTime, 0);
object.getTransform().addVelocity(velocity);
}
/**
* Sets the specified GameObject's vertical velocity to jump to the
* specified height. Calls getJumpVelocity() to calculate the velocity,
* which uses the Math.sqrt() function.
*/
public void jumpToHeight(GameObject object, float jumpHeight) {
jump(object, getJumpVelocity(jumpHeight));
}
/**
* Sets the specified GameObject's vertical velocity to the specified jump
* velocity.
*/
public void jump(GameObject object, float jumpVelocity) {
velocity.setTo(0, jumpVelocity, 0);
object.getTransform().getVelocity().y = 0;
object.getTransform().addVelocity(velocity);
}
/**
* Returns the vertical velocity needed to jump the specified height (based
* on current gravity). Uses the Math.sqrt() function.
*/
public float getJumpVelocity(float jumpHeight) {
// use velocity/acceleration formal: v*v = -2 * a(y-y0)
// (v is jump velocity, a is accel, y-y0 is max height)
return (float) Math.sqrt(-2 * gravityAccel * jumpHeight);
}
}
/**
* A Player object.
*/
class Player extends JumpingGameObject {
public static final float DEFAULT_PLAYER_RADIUS = 32;
public static final float DEFAULT_PLAYER_HEIGHT = 128;
public Player() {
this(new PolygonGroup("Player"));
// set up player bounds
PolygonGroupBounds playerBounds = getBounds();
playerBounds.setTopHeight(DEFAULT_PLAYER_HEIGHT);
playerBounds.setRadius(DEFAULT_PLAYER_RADIUS);
}
public Player(PolygonGroup group) {
super(group);
}
}
/**
* The GridGameObjectManager is a GameObjectManager that integrally arranges
* GameObjects on a 2D grid for visibility determination and to limit the number
* of tests for collision detection.
*/
class GridGameObjectManager implements GameObjectManager {
/**
* Default grid size of 512. The grid size should be larger than the largest
* object's diameter.
*/
private static final int GRID_SIZE_BITS = 9;
private static final int GRID_SIZE = 1 << GRID_SIZE_BITS;
/**
* The Cell class represents a cell in the grid. It contains a list of game
* objects and a visible flag.
*/
private static class Cell {
List objects;
boolean visible;
Cell() {
objects = new ArrayList();
visible = false;
}
}
private Cell[] grid;
private Rectangle mapBounds;
private int gridWidth;
private int gridHeight;
private List allObjects;
private GameObject player;
private Vector3D oldLocation;
private CollisionDetection collisionDetection;
/**
* Creates a new GridGameObjectManager with the specified map bounds and
* collision detection handler. GameObjects outside the map bounds will
* never be shown.
*/
public GridGameObjectManager(Rectangle mapBounds,
CollisionDetection collisionDetection) {
this.mapBounds = mapBounds;
this.collisionDetection = collisionDetection;
gridWidth = (mapBounds.width >> GRID_SIZE_BITS) + 1;
gridHeight = (mapBounds.height >> GRID_SIZE_BITS) + 1;
grid = new Cell[gridWidth * gridHeight];
for (int i = 0; i < grid.length; i++) {
grid[i] = new Cell();
}
allObjects = new ArrayList();
oldLocation = new Vector3D();
}
/**
* Converts a map x-coordinate to a grid x-coordinate.
*/
private int convertMapXtoGridX(int x) {
return (x - mapBounds.x) >> GRID_SIZE_BITS;
}
/**
* Converts a map y-coordinate to a grid y-coordinate.
*/
private int convertMapYtoGridY(int y) {
return (y - mapBounds.y) >> GRID_SIZE_BITS;
}
/**
* Marks all objects as potentially visible (should be drawn).
*/
public void markAllVisible() {
for (int i = 0; i < grid.length; i++) {
grid[i].visible = true;
}
}
/**
* Marks all objects within the specified 2D bounds as potentially visible
* (should be drawn).
*/
public void markVisible(Rectangle bounds) {
int x1 = Math.max(0, convertMapXtoGridX(bounds.x));
int y1 = Math.max(0, convertMapYtoGridY(bounds.y));
int x2 = Math.min(gridWidth - 1, convertMapXtoGridX(bounds.x
+ bounds.width));
int y2 = Math.min(gridHeight - 1, convertMapYtoGridY(bounds.y
+ bounds.height));
for (int y = y1; y <= y2; y++) {
int offset = y * gridWidth;
for (int x = x1; x <= x2; x++) {
grid[offset + x].visible = true;
}
}
}
/**
* Adds a GameObject to this manager.
*/
public void add(GameObject object) {
if (object != null) {
if (object == player) {
// ensure player always moves first
allObjects.add(0, object);
} else {
allObjects.add(object);
}
Cell cell = getCell(object);
if (cell != null) {
cell.objects.add(object);
}
}
}
/**
* Removes a GameObject from this manager.
*/
public void remove(GameObject object) {
if (object != null) {
allObjects.remove(object);
Cell cell = getCell(object);
if (cell != null) {
cell.objects.remove(object);
}
}
}
/**
* Adds a GameObject to this manager, specifying it as the player object. An
* existing player object, if any, is not removed.
*/
public void addPlayer(GameObject player) {
this.player = player;
if (player != null) {
player.notifyVisible(true);
add(player);
}
}
/**
* Gets the object specified as the Player object, or null if no player
* object was specified.
*/
public GameObject getPlayer() {
return player;
}
/**
* Gets the cell the specified GameObject is in, or null if the GameObject
* is not within the map bounds.
*/
private Cell getCell(GameObject object) {
int x = convertMapXtoGridX((int) object.getX());
int y = convertMapYtoGridY((int) object.getZ());
return getCell(x, y);
}
/**
* Gets the cell of the specified grid location, or null if the grid
|