/* * Copyright (C) 2010 The Android Open Source Project * * 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.replica.replicaisland; import java.util.Comparator; import com.replica.replicaisland.CollisionParameters.HitType; /** * A system for calculating collisions between moving game objects. This system accepts collision * volumes from game objects each frame and performs a series of tests to see which of them * overlap. Collisions are only considered between offending "attack" volumes and receiving * "vulnerability" volumes. This implementation works by using a sweep-and-prune algorithm: * objects to be considered are sorted in the x axis and then compared in one dimension for * overlaps. A bounding volume that encompasses all attack and vulnerability volumes is used for * this test, and when an intersection is found the actual offending and receiving volumes are * compared. If an intersection is detected both objects receive notification via a * HitReactionComponent, if one has been specified. */ public class GameObjectCollisionSystem extends BaseObject { private static final int MAX_COLLIDING_OBJECTS = 256; private static final int COLLISION_RECORD_POOL_SIZE = 256; private static final CollisionVolumeComparator sCollisionVolumeComparator = new CollisionVolumeComparator(); private static CollisionVolume.FlipInfo sFlip = new CollisionVolume.FlipInfo(); private static CollisionVolume.FlipInfo sOtherFlip = new CollisionVolume.FlipInfo(); FixedSizeArray mObjects; CollisionVolumeRecordPool mRecordPool; private boolean mDrawDebugBoundingVolume = false; private boolean mDrawDebugCollisionVolumes = false; public GameObjectCollisionSystem() { super(); mObjects = new FixedSizeArray(MAX_COLLIDING_OBJECTS); mObjects.setComparator(sCollisionVolumeComparator); //mObjects.setSorter(new ShellSorter()); mRecordPool = new CollisionVolumeRecordPool(COLLISION_RECORD_POOL_SIZE); } @Override public void reset() { final int count = mObjects.getCount(); for (int x = 0; x < count; x++) { mRecordPool.release(mObjects.get(x)); } mObjects.clear(); mDrawDebugBoundingVolume = false; mDrawDebugCollisionVolumes = false; } /** * Adds a game object, and its related volumes, to the dynamic collision world for one frame. * Once registered for collisions the object may damage other objects via attack volumes or * receive damage from other volumes via vulnerability volumes. * @param object The object to consider for collision. * @param reactionComponent A HitReactionComponent to notify when an intersection is calculated. * If null, the intersection will still occur and no notification will be sent. * @param boundingVolume A volume that describes the game object in space. It should encompass * all of the attack and vulnerability volumes. * @param attackVolumes A list of volumes that can hit other game objects. May be null. * @param vulnerabilityVolumes A list of volumes that can receive hits from other game objects. * May be null. */ public void registerForCollisions(GameObject object, HitReactionComponent reactionComponent, CollisionVolume boundingVolume, FixedSizeArray attackVolumes, FixedSizeArray vulnerabilityVolumes) { CollisionVolumeRecord record = mRecordPool.allocate(); if (record != null && object != null && boundingVolume != null && (attackVolumes != null || vulnerabilityVolumes != null)) { record.object = object; record.boundingVolume = boundingVolume; record.attackVolumes = attackVolumes; record.vulnerabilityVolumes = vulnerabilityVolumes; record.reactionComponent = reactionComponent; mObjects.add(record); } } @Override public void update(float timeDelta, BaseObject parent) { // Sort the objects by their x position. mObjects.sort(true); final int count = mObjects.getCount(); for (int x = 0; x < count; x++) { final CollisionVolumeRecord record = mObjects.get(x); final Vector2 position = record.object.getPosition(); sFlip.flipX = (record.object.facingDirection.x < 0.0f); sFlip.flipY = (record.object.facingDirection.y < 0.0f); sFlip.parentWidth = record.object.width; sFlip.parentHeight = record.object.height; if (sSystemRegistry.debugSystem != null) { drawDebugVolumes(record); } final float maxX = record.boundingVolume.getMaxXPosition(sFlip) + position.x; for (int y = x + 1; y < count; y++) { final CollisionVolumeRecord other = mObjects.get(y); final Vector2 otherPosition = other.object.getPosition(); sOtherFlip.flipX = (other.object.facingDirection.x < 0.0f); sOtherFlip.flipY = (other.object.facingDirection.y < 0.0f); sOtherFlip.parentWidth = other.object.width; sOtherFlip.parentHeight = other.object.height; if (otherPosition.x + other.boundingVolume.getMinXPosition(sOtherFlip) > maxX) { // These objects can't possibly be colliding. And since the list is sorted, // there are no potentially colliding objects after this object // either, so we're done! break; } else { final boolean testRequired = (record.attackVolumes != null && other.vulnerabilityVolumes != null) || (record.vulnerabilityVolumes != null && other.attackVolumes != null); if (testRequired && record.boundingVolume.intersects(position, sFlip, other.boundingVolume, otherPosition, sOtherFlip)) { // These two objects are potentially colliding. // Now we must test all attack vs vulnerability boxes. final int hit = testAttackAgainstVulnerability( record.attackVolumes, other.vulnerabilityVolumes, position, otherPosition, sFlip, sOtherFlip); if (hit != HitType.INVALID) { boolean hitAccepted = false; if (other.reactionComponent != null) { hitAccepted = other.reactionComponent.receivedHit( other.object, record.object, hit); } if (record.reactionComponent != null) { record.reactionComponent.hitVictim( record.object, other.object, hit, hitAccepted); } } final int hit2 = testAttackAgainstVulnerability( other.attackVolumes, record.vulnerabilityVolumes, otherPosition, position, sOtherFlip, sFlip); if (hit2 != HitType.INVALID) { boolean hitAccepted = false; if (record.reactionComponent != null) { hitAccepted = record.reactionComponent.receivedHit( record.object, other.object, hit2); } if (other.reactionComponent != null) { other.reactionComponent.hitVictim( other.object, record.object, hit2, hitAccepted); } } } } } // This is a little tricky. Since we always sweep forward in the list it's safe // to invalidate the current record after we've tested it. This way we don't have to // iterate over the object list twice. mRecordPool.release(record); } mObjects.clear(); } /** Compares the passed list of attack volumes against the passed list of vulnerability volumes * and returns a hit type if an intersection is found. * @param attackVolumes Offensive collision volumes. * @param vulnerabilityVolumes Receiving collision volumes. * @param attackPosition The world position of the attacking object. * @param vulnerabilityPosition The world position of the receiving object. * @return The hit type of the first attacking volume that intersects a vulnerability volume, * or HitType.INVALID if no intersections are found. */ private int testAttackAgainstVulnerability( FixedSizeArray attackVolumes, FixedSizeArray vulnerabilityVolumes, Vector2 attackPosition, Vector2 vulnerabilityPosition, CollisionVolume.FlipInfo attackFlip, CollisionVolume.FlipInfo vulnerabilityFlip) { int intersectionType = HitType.INVALID; if (attackVolumes != null && vulnerabilityVolumes != null) { final int attackCount = attackVolumes.getCount(); for (int x = 0; x < attackCount && intersectionType == HitType.INVALID; x++) { final CollisionVolume attackVolume = attackVolumes.get(x); final int hitType = attackVolume.getHitType(); if (hitType != HitType.INVALID) { final int vulnerabilityCount = vulnerabilityVolumes.getCount(); for (int y = 0; y < vulnerabilityCount; y++) { final CollisionVolume vulnerabilityVolume = vulnerabilityVolumes.get(y); final int vulnerableType = vulnerabilityVolume.getHitType(); if (vulnerableType == HitType.INVALID || vulnerableType == hitType) { if (attackVolume.intersects(attackPosition, attackFlip, vulnerabilityVolume, vulnerabilityPosition, vulnerabilityFlip)) { intersectionType = hitType; break; } } } } } } return intersectionType; } private final void drawDebugVolumes(CollisionVolumeRecord record) { final Vector2 position = record.object.getPosition(); if (mDrawDebugBoundingVolume) { final CollisionVolume boundingVolume = record.boundingVolume; sSystemRegistry.debugSystem.drawShape( position.x + boundingVolume.getMinXPosition(sFlip), position.y + boundingVolume.getMinYPosition(sFlip), boundingVolume.getMaxX() - boundingVolume.getMinX(), boundingVolume.getMaxY() - boundingVolume.getMinY(), DebugSystem.SHAPE_CIRCLE, DebugSystem.COLOR_OUTLINE); } if (mDrawDebugCollisionVolumes) { if (record.attackVolumes != null) { final int attackVolumeCount = record.attackVolumes.getCount(); for (int y = 0; y < attackVolumeCount; y++) { CollisionVolume volume = record.attackVolumes.get(y); sSystemRegistry.debugSystem.drawShape( position.x + volume.getMinXPosition(sFlip), position.y + volume.getMinYPosition(sFlip), volume.getMaxX() - volume.getMinX(), volume.getMaxY() - volume.getMinY(), volume.getClass() == AABoxCollisionVolume.class ? DebugSystem.SHAPE_BOX : DebugSystem.SHAPE_CIRCLE, DebugSystem.COLOR_RED); } } if (record.vulnerabilityVolumes != null) { final int vulnVolumeCount = record.vulnerabilityVolumes.getCount(); for (int y = 0; y < vulnVolumeCount; y++) { CollisionVolume volume = record.vulnerabilityVolumes.get(y); sSystemRegistry.debugSystem.drawShape( position.x + volume.getMinXPosition(sFlip), position.y + volume.getMinYPosition(sFlip), volume.getMaxX() - volume.getMinX(), volume.getMaxY() - volume.getMinY(), volume.getClass() == AABoxCollisionVolume.class ? DebugSystem.SHAPE_BOX : DebugSystem.SHAPE_CIRCLE, DebugSystem.COLOR_BLUE); } } } } public void setDebugPrefs(boolean drawBoundingVolumes, boolean drawCollisionVolumes) { mDrawDebugBoundingVolume = drawBoundingVolumes; mDrawDebugCollisionVolumes = drawCollisionVolumes; } /** A record of a single game object and its associated collision info. */ private class CollisionVolumeRecord extends AllocationGuard { public GameObject object; public HitReactionComponent reactionComponent; public CollisionVolume boundingVolume; public FixedSizeArray attackVolumes; public FixedSizeArray vulnerabilityVolumes; public void reset() { object = null; attackVolumes = null; vulnerabilityVolumes = null; boundingVolume = null; reactionComponent = null; } } /** A pool of collision volume records. */ private class CollisionVolumeRecordPool extends TObjectPool { public CollisionVolumeRecordPool(int count) { super(count); } @Override protected void fill() { for (int x = 0; x < getSize(); x++) { getAvailable().add(new CollisionVolumeRecord()); } } @Override public void release(Object entry) { ((CollisionVolumeRecord)entry).reset(); super.release(entry); } } /** * Comparator for game objects that considers the world position of the object's bounding * volume and sorts objects from left to right on the x axis. */ public final static class CollisionVolumeComparator implements Comparator { private static CollisionVolume.FlipInfo sCompareFlip = new CollisionVolume.FlipInfo(); public int compare(CollisionVolumeRecord object1, CollisionVolumeRecord object2) { int result = 0; if (object1 == null && object2 != null) { result = 1; } else if (object1 != null && object2 == null) { result = -1; } else if (object1 != null && object2 != null) { sCompareFlip.flipX = (object1.object.facingDirection.x < 0.0f); sCompareFlip.flipY = (object1.object.facingDirection.y < 0.0f); sCompareFlip.parentWidth = object1.object.width; sCompareFlip.parentHeight = object1.object.height; final float minX1 = object1.object.getPosition().x + object1.boundingVolume.getMinXPosition(sCompareFlip); sCompareFlip.flipX = (object2.object.facingDirection.x < 0.0f); sCompareFlip.flipY = (object2.object.facingDirection.y < 0.0f); sCompareFlip.parentWidth = object2.object.width; sCompareFlip.parentHeight = object2.object.height; final float minX2 = object2.object.getPosition().x + object2.boundingVolume.getMinXPosition(sCompareFlip); final float delta = minX1 - minX2; if (delta < 0.0f) { result = -1; } else if (delta > 0.0f) { result = 1; } } return result; } } }