| /* |
| * 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<CollisionVolumeRecord> mObjects; |
| CollisionVolumeRecordPool mRecordPool; |
| private boolean mDrawDebugBoundingVolume = false; |
| private boolean mDrawDebugCollisionVolumes = false; |
| |
| |
| public GameObjectCollisionSystem() { |
| super(); |
| mObjects = new FixedSizeArray<CollisionVolumeRecord>(MAX_COLLIDING_OBJECTS); |
| mObjects.setComparator(sCollisionVolumeComparator); |
| //mObjects.setSorter(new ShellSorter<CollisionVolumeRecord>()); |
| 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<CollisionVolume> attackVolumes, |
| FixedSizeArray<CollisionVolume> 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<CollisionVolume> attackVolumes, |
| FixedSizeArray<CollisionVolume> 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<CollisionVolume> attackVolumes; |
| public FixedSizeArray<CollisionVolume> 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<CollisionVolumeRecord> { |
| |
| 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<CollisionVolumeRecord> { |
| 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; |
| } |
| } |
| |
| |
| |
| } |