blob: a252387c79030d0299b5f922c71f87f00b622505 [file] [log] [blame]
/*
* 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;
}
}
}