/* * Copyright (c) 2003-onwards Shaven Puppy Ltd * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of 'Shaven Puppy' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package worm; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import org.lwjgl.util.ReadableRectangle; import org.lwjgl.util.Rectangle; /** * Grid-based collision manager. */ class GridCollisionManager implements CollisionManager { private static final int INITIAL_ENTITIES = 512; private static final int BORDER_TILES = 2; private static class Cell { final ArrayList<Entity> contents = new ArrayList<Entity>(4); boolean inUsed; } private static class Pair { Entity a, b; Pair(Entity a, Entity b) { this.a = a; this.b = b; } @Override public int hashCode() { return a.hashCode() + b.hashCode(); } @Override public boolean equals(Object obj) { Pair p = (Pair) obj; return ((p.a == a && p.b == b) || (p.a == b && p.b == a)); } } /** Temp pair */ private final Pair tempPair = new Pair(null, null); /** Temp rects */ private final Rectangle temp = new Rectangle(), cells = new Rectangle(); /** Collision pairs */ private final List<Pair> collisions = new ArrayList<Pair>(); /** Cell size */ private final int cellSize; /** The sparse grid */ private Cell[] grid; /** All cells which actually contain at least 1 entity */ private ArrayList<Cell> used = new ArrayList<Cell>(INITIAL_ENTITIES), used0 = new ArrayList<Cell>(INITIAL_ENTITIES); /** Origin (grid) */ private int ox, oy; /** Size (grid) */ private int w, h; /** Map of Entities to ReadableRectangles; this maps Entities to the cells in which they have been placed */ private Map<Entity, ReadableRectangle> entityMap = new HashMap<Entity, ReadableRectangle>(INITIAL_ENTITIES); private static int fastFloor(float x) { int i = (int) x; return x >= 0.0f ? i : i == x ? i : i - 1; } GridCollisionManager(int cellSize) { this.cellSize = cellSize; ox = -BORDER_TILES; oy = -BORDER_TILES; w = WormGameState.ABS_MAX_SIZE + BORDER_TILES * 2; h = WormGameState.ABS_MAX_SIZE + BORDER_TILES * 2; grid = new Cell[w * h]; } @Override public void clear() { Arrays.fill(grid, null); used.clear(); entityMap.clear(); } private void calcBounds(Entity entity) { float radius = entity.getRadius(); if (radius != 0.0f) { // it's a round entity - we'll expand its bounds a bit to account for freaky rounding int size = (int)(radius * 2.0f) + 2; temp.setBounds((int)(entity.getX() - radius) - 1, (int)(entity.getY() - radius) - 1, size, size); } else { entity.getBounds(temp); } } @Override public void store(Entity entity) { // See which cells we should be in and maybe resize the grid calcBounds(entity); Rectangle r = calcCells(temp, new Rectangle()); if (entityMap.put(entity, r) != null) { assert false : "Entity "+entity+" is already in the entityMap!"; } // Add this entity to each cell int cell = r.getX() + r.getY() * w; for (int y = r.getHeight(); -- y >= 0; ) { for (int x = r.getWidth(); -- x >= 0; ) { Cell c = grid[cell]; if (c == null) { c = new Cell(); grid[cell] = c; } c.contents.add(entity); if (!c.inUsed) { c.inUsed = true; used.add(c); } cell ++; } cell += w - r.getWidth(); } } private Rectangle calcCells(ReadableRectangle src, Rectangle dest) { dest.setBounds(fastFloor(src.getX() / cellSize) - ox, fastFloor(src.getY() / cellSize) - oy, 0, 0); dest.add(fastFloor((src.getX() + src.getWidth() - 1) / cellSize) - ox + 1, fastFloor((src.getY() + src.getHeight() - 1) / cellSize) - oy + 1); dest.setBounds(Math.max(0, dest.getX()), Math.max(0, dest.getY()), Math.min(w, dest.getX() + dest.getWidth()) - dest.getX(), Math.min(h, dest.getY() + dest.getHeight()) - dest.getY()); return dest; } @Override public CollisionManager add(Entity entity) { store(entity); return this; } @Override public CollisionManager submit(ReadableRectangle rect) { return this; } @Override public boolean remove(Entity entity) { // Shortcut: look in entity map first ReadableRectangle cells = entityMap.remove(entity); if (cells != null) { int cell = cells.getX() + cells.getY() * w; for (int yy = cells.getHeight(); -- yy >= 0; ) { for (int xx = cells.getWidth(); -- xx >= 0; ) { Cell test = grid[cell]; if (test == null) { assert false : "Entity "+entity+" was supposed to be at "+cells+" but isn't at "+(cell % w)+","+(cell / w); } if (!test.contents.remove(entity)) { assert false : "Entity "+entity+" not found where it was expected!"; } cell ++; } cell += w - cells.getWidth(); } return true; } else { assert false : "Entity "+entity+" not found!"; return false; } } @Override public List<Entity> checkCollisions(Entity entity, List<Entity> dest) { if (dest == null) { dest = new ArrayList<Entity>(); } dest.clear(); if (!entity.isActive() || !entity.canCollide()) { // Shortcut: src entity can't collide return dest; } calcBounds(entity); calcCells(temp, cells); int cell = cells.getX() + cells.getY() * w; for (int y = cells.getHeight(); -- y >= 0; ) { for (int x = cells.getWidth(); -- x >= 0; ) { Cell c = grid[cell]; if (c != null) { List<Entity> contents = c.contents; final int n = contents.size(); for (int i = n; --i >= 0; ) { Entity test = contents.get(i); if (entity != test && test.isActive() && test.canCollide() && test.isTouching(entity) && !dest.contains(test)) { dest.add(test); } } } cell ++; } cell += w - cells.getWidth(); } collisions.clear(); return dest; } @Override public List<Entity> checkCollisions(ReadableRectangle rect, List<Entity> dest) { if (dest == null) { dest = new ArrayList<Entity>(); } dest.clear(); calcCells(rect, cells); int cell = cells.getX() + cells.getY() * w; for (int y = cells.getHeight(); -- y >= 0; ) { for (int x = cells.getWidth(); --x >= 0; ) { Cell c = grid[cell]; if (c != null) { List<Entity> contents = c.contents; final int n = contents.size(); for (int i = n; --i >= 0; ) { Entity entity = contents.get(i); if (entity.isActive() && entity.canCollide() && entity.isTouching(rect) && !dest.contains(entity)) { dest.add(entity); } } } cell ++; } cell += w - cells.getWidth(); } collisions.clear(); return dest; } @Override public void checkCollisions() { // For each cell with something in it... for (int i = used.size(); --i >= 0; ) { Cell cell = used.get(i); if (cell.contents.size() > 0) { used0.add(cell); } } for (int cell = used0.size(); --cell >= 0; ) { // Process all combinations within that cell List<Entity> contents = used0.get(cell).contents; for (int i = 0; i < contents.size(); i ++) { Entity src = contents.get(i); if (src.isActive() && src.canCollide()) { for (int j = i + 1; j < contents.size(); j ++) { Entity dest = contents.get(j); if (dest.isActive() && src.isActive() && src.canCollide() && dest.canCollide() && src.isTouching(dest)) { // Inform both entities of the collision, in no particular order, unless already done tempPair.a = src; tempPair.b = dest; if (collisions.contains(tempPair)) { continue; } collisions.add(new Pair(src, dest)); src.onCollision(dest); dest.onCollision(src); } } } } } // Compact used list used0.clear(); for (int i = used.size(); --i >= 0; ) { Cell cell = used.get(i); if (cell.contents.size() > 0) { used0.add(cell); } else { cell.inUsed = false; } } ArrayList<Cell> temp = used; used = used0; used0 = temp; used0.clear(); // Clear away collisions now they're all processed collisions.clear(); } }