/* * Copyright 2014 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.fpl.gamecontroller.particles; import com.google.fpl.gamecontroller.Utils; /** * Simple class for spatially partitioning particles into a 2-d grid structure. * * The world is partitioned into a 2-d grid of zones. As particles are inserted, they are * assigned to one of the zones. Every frame, the grid must be cleared and the particles * reinserted at their updated positions. * * Proximity queries (getRectPopulation()) can quickly determine the list of particles in * a given rectangular section of the world. * * Assumes that the origin is in the center of the screen. */ public class ParticleCollisionGrid { // The maximum number of particles that can be added to a single grid square. If too // many particles are added to a single zone, only the first 100 will be tracked. private static final int MAX_ENTITIES_PER_ZONE = 100; // The maximum number of particles returned for a proximity query (getRectPopulation()). private static final int MAX_RETURNED_VALUES = 4000; // Row and column sizes in world coordinates. private float mColumnWidth, mRowHeight; // The index of the last row and column in the grid. private int mColumnMax, mRowMax; // The total number of zones in the grid. private int mZoneCount; // Dimensions of the world. private float mWorldWidth, mWorldHeight; // Each zone has an array of particles. The zones are stored in row-major order in the first // dimension of mZoneArray (e.g. the particles in a given row and column can be found by // referencing mZoneArray[row + column * mColumnMax]). private final BaseParticle[][] mZoneArray; // Keeps track of the number of particles in each zone. Stored in row-major order, // like mZoneArray. private final int[] mZonePopulation; // To avoid the overhead of allocating a new list for each collision check, // this list is allocated once and reused for each query. private final BaseParticle[] mReturnValues; /** * Constructs a new collision grid. * * The origin of the world is at 0, 0. * * @param width the total width of the grid. * @param height the total height of the grid. * @param zoneSize the number of world units in each zone. Each zone is square, and * zoneSize is the length of the sides of the square. */ public ParticleCollisionGrid(float width, float height, float zoneSize) { mWorldWidth = width; mWorldHeight = height; mColumnMax = (int) Math.ceil(width / zoneSize); mRowMax = (int) Math.ceil(height / zoneSize); mColumnWidth = zoneSize; mRowHeight = zoneSize; mZoneCount = mColumnMax * mRowMax; mZoneArray = new BaseParticle[mZoneCount][MAX_ENTITIES_PER_ZONE]; mZonePopulation = new int[mZoneCount]; mReturnValues = new BaseParticle[MAX_RETURNED_VALUES]; } /** * Removes all the objects from the grid. */ public void clear() { for (int i = 0; i < mZoneCount; i++) { clearZone(i); } } /** * Insert a particle into the grid. */ public void addParticle(BaseParticle particle) { // The particles use world coordinates, which need to be converted to grid coordinates // before they can be inserted. addObjectHelper(particle, worldXToGridX(particle.getPositionX()), worldYToGridY(particle.getPositionY())); } /** * Returns an array containing all the particles in the given section of the grid. * * The returned particles will start at array element 0. The last valid element * will be followed by an element set to null. * * The returned array will become invalid next time getRectPopulation is called. */ public BaseParticle[] getRectPopulation(float x1, float y1, float x2, float y2) { int leftSlot, rightSlot, topSlot, bottomSlot; leftSlot = (int) Math.floor(worldXToGridX(x1) / mColumnWidth) - 1; if (leftSlot < 0) { leftSlot = 0; } rightSlot = (int) Math.floor(worldXToGridX(x2) / mColumnWidth) + 1; if (rightSlot >= mColumnMax) { rightSlot = mColumnMax - 1; } topSlot = (int) Math.floor(worldYToGridY(y1) / mRowHeight) - 1; if (topSlot < 0) { topSlot = 0; } bottomSlot = (int) Math.floor(worldYToGridY(y2) / mRowHeight) + 1; if (bottomSlot >= mRowMax) { bottomSlot = mRowMax - 1; } int returnedValueCount = 0; // Iterate through each zone covered by the given rectangle. for (int x = leftSlot; x <= rightSlot; ++x) { for (int y = topSlot; y <= bottomSlot; ++y) { int currentZone = x + y * mColumnMax; // Add all the particles in the zone. for (int i = 0; i < mZonePopulation[currentZone]; ++i) { mReturnValues[returnedValueCount] = mZoneArray[currentZone][i]; ++returnedValueCount; if (returnedValueCount >= MAX_RETURNED_VALUES - 1) { // Don't have enough room for more hits, so bail out. Utils.logDebug("Ran out of space in return array."); break; } } } } // Set the slot after the last particle to null. mReturnValues[returnedValueCount] = null; return mReturnValues; } /** * Converts a world coordinate to grid coordinate. * * The world coordinate system has the origin in the middle of the map and extends * mWorldWidth / 2 units to the left and right. The grid can not store negative values, so * world coordinates must be biased by mWorldWidth / 2 so that they are always positive. */ private float worldXToGridX(float worldX) { return worldX + mWorldWidth / 2.0f; } /** * Converts a world coordinate to grid coordinate. * * The world coordinate system has the origin in the middle of the map and extends * mWorldHeight / 2 units to the above and below the origin. * The grid can not store negative values, so world coordinates must be biased by * mWorldHeight / 2 so that they are always positive. */ private float worldYToGridY(float worldY) { return worldY + mWorldHeight / 2.0f; } private void addToZone(BaseParticle particle, int zone) { if (mZonePopulation[zone] < MAX_ENTITIES_PER_ZONE) { mZoneArray[zone][mZonePopulation[zone]] = particle; mZonePopulation[zone]++; } else { Utils.logDebug("Ran out of space in zone " + zone + "/" + mZoneCount); } } private void clearZone(int zone) { mZonePopulation[zone] = 0; // This next part is just to make sure garbage collection can happen. Not that it should. for (int i = 0; i < MAX_ENTITIES_PER_ZONE; i++) { mZoneArray[zone][i] = null; } } private void addObjectHelper(BaseParticle particle, float gridX, float gridY) { int zone = getZoneOnGrid(gridX, gridY); if (zone != -1) { addToZone(particle, zone); } } private int getZoneOnGrid(float gridX, float gridY) { int gridZoneX = (int) Math.floor(gridX / mColumnWidth); int gridZoneY = (int) Math.floor(gridY / mRowHeight); if (gridZoneX < 0) { gridZoneX = 0; } if (gridZoneY < 0) { gridZoneY = 0; } if (gridZoneX >= mColumnMax) { gridZoneX = mColumnMax - 1; } if (gridZoneY >= mRowMax) { gridZoneY = mRowMax - 1; } return (gridZoneX + gridZoneY * mColumnMax); } }