blob: 40f491b79ee34682ee0b8559f698c9f6e73b98fc [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;
/**
* Handles collision against the background. Snaps colliding objects out of collision and reports
* the hit to the parent game object.
*/
public class BackgroundCollisionComponent extends GameComponent {
private Vector2 mPreviousPosition;
private int mWidth;
private int mHeight;
private int mHorizontalOffset;
private int mVerticalOffset;
// Workspace vectors. Allocated up front for speed.
private Vector2 mCurrentPosition;
private Vector2 mPreviousCenter;
private Vector2 mDelta;
private Vector2 mFilterDirection;
private Vector2 mHorizontalHitPoint;
private Vector2 mHorizontalHitNormal;
private Vector2 mVerticalHitPoint;
private Vector2 mVerticalHitNormal;
private Vector2 mRayStart;
private Vector2 mRayEnd;
private Vector2 mTestPointStart;
private Vector2 mTestPointEnd;
private Vector2 mMergedNormal;
/**
* Sets up the collision bounding box. This box may be a different size than the bounds of the
* sprite that this object controls.
* @param width The width of the collision box.
* @param height The height of the collision box.
* @param horzOffset The offset of the collision box from the object's origin in the x axis.
* @param vertOffset The offset of the collision box from the object's origin in the y axis.
*/
public BackgroundCollisionComponent(int width, int height, int horzOffset, int vertOffset) {
super();
setPhase(ComponentPhases.COLLISION_RESPONSE.ordinal());
mPreviousPosition = new Vector2();
mWidth = width;
mHeight = height;
mHorizontalOffset = horzOffset;
mVerticalOffset = vertOffset;
mCurrentPosition = new Vector2();
mPreviousCenter = new Vector2();
mDelta = new Vector2();
mFilterDirection = new Vector2();
mHorizontalHitPoint = new Vector2();
mHorizontalHitNormal = new Vector2();
mVerticalHitPoint = new Vector2();
mVerticalHitNormal = new Vector2();
mRayStart = new Vector2();
mRayEnd = new Vector2();
mTestPointStart = new Vector2();
mTestPointEnd = new Vector2();
mMergedNormal = new Vector2();
}
public BackgroundCollisionComponent() {
super();
setPhase(ComponentPhases.COLLISION_RESPONSE.ordinal());
mPreviousPosition = new Vector2();
mCurrentPosition = new Vector2();
mPreviousCenter = new Vector2();
mDelta = new Vector2();
mFilterDirection = new Vector2();
mHorizontalHitPoint = new Vector2();
mHorizontalHitNormal = new Vector2();
mVerticalHitPoint = new Vector2();
mVerticalHitNormal = new Vector2();
mRayStart = new Vector2();
mRayEnd = new Vector2();
mTestPointStart = new Vector2();
mTestPointEnd = new Vector2();
mMergedNormal = new Vector2();
}
@Override
public void reset() {
mPreviousPosition.zero();
}
public void setSize(int width, int height) {
mWidth = width;
mHeight = height;
// TODO: Resize might cause new collisions.
}
public void setOffset(int horzOffset, int vertOffset) {
mHorizontalOffset = horzOffset;
mVerticalOffset = vertOffset;
}
/**
* This function is the meat of the collision response logic. Our collision detection and
* response must be capable of dealing with arbitrary surfaces and must be frame rate
* independent (we must sweep the space in-between frames to find collisions reliably). The
* following algorithm is used to keep the collision box out of the collision world.
* 1. Cast a ray from the center point of the box at its position last frame to the edge
* of the box at its current position. If the ray intersects anything, snap the box
* back to the point of intersection.
* 2. Perform Step 1 twice: once looking for surfaces opposing horizontal movement and
* again for surfaces opposing vertical movement. These two ray tests approximate the
* movement of the box between the previous frame and this one.
* 3. Since most collisions are collisions with the ground, more precision is required for
* vertical intersections. Perform another ray test, this time from the top of the
* box's position (after snapping in Step 2) to the bottom. Snap out of any vertical
* surfaces that the ray encounters. This will ensure consistent snapping behavior on
* incline surfaces.
* 4. Add the normals of the surfaces that were hit up and normalize the result to produce
* a direction describing the average slope of the surfaces that the box is resting on.
* Physics will use this value as a normal to resolve collisions with the background.
*/
@Override
public void update(float timeDelta, BaseObject parent) {
GameObject parentObject = (GameObject) parent;
parentObject.setBackgroundCollisionNormal(Vector2.ZERO);
if (mPreviousPosition.length2() != 0) {
CollisionSystem collision = sSystemRegistry.collisionSystem;
if (collision != null) {
final int left = mHorizontalOffset;
final int bottom = mVerticalOffset;
final int right = left + mWidth;
final int top = bottom + mHeight;
final float centerOffsetX = ((mWidth) / 2.0f) + left;
final float centerOffsetY = ((mHeight) / 2.0f) + bottom;
mCurrentPosition.set(parentObject.getPosition());
mDelta.set(mCurrentPosition);
mDelta.subtract(mPreviousPosition);
mPreviousCenter.set(centerOffsetX, centerOffsetY);
mPreviousCenter.add(mPreviousPosition);
boolean horizontalHit = false;
boolean verticalHit = false;
mVerticalHitPoint.zero();
mVerticalHitNormal.zero();
mHorizontalHitPoint.zero();
mHorizontalHitNormal.zero();
// The order in which we sweep the horizontal and vertical space can affect the
// final result because we perform incremental snapping mid-sweep. So it is
// necessary to sweep in the primary direction of movement first.
if (Math.abs(mDelta.x) > Math.abs(mDelta.y)) {
horizontalHit = sweepHorizontal(mPreviousCenter, mCurrentPosition, mDelta, left,
right, centerOffsetY, mHorizontalHitPoint, mHorizontalHitNormal,
parentObject);
verticalHit = sweepVertical(mPreviousCenter, mCurrentPosition, mDelta, bottom,
top, centerOffsetX, mVerticalHitPoint, mVerticalHitNormal,
parentObject);
} else {
verticalHit = sweepVertical(mPreviousCenter, mCurrentPosition, mDelta, bottom,
top, centerOffsetX, mVerticalHitPoint, mVerticalHitNormal,
parentObject);
horizontalHit = sweepHorizontal(mPreviousCenter, mCurrentPosition, mDelta, left,
right, centerOffsetY, mHorizontalHitPoint, mHorizontalHitNormal,
parentObject);
}
// force the collision volume to stay within the bounds of the world.
LevelSystem level = sSystemRegistry.levelSystem;
if (level != null) {
if (mCurrentPosition.x + left < 0.0f) {
mCurrentPosition.x = (-left + 1);
horizontalHit = true;
mHorizontalHitNormal.x = (mHorizontalHitNormal.x + 1.0f);
mHorizontalHitNormal.normalize();
} else if (mCurrentPosition.x + right > level.getLevelWidth()) {
mCurrentPosition.x = (level.getLevelWidth() - right - 1);
mHorizontalHitNormal.x = (mHorizontalHitNormal.x - 1.0f);
mHorizontalHitNormal.normalize();
horizontalHit = true;
}
/*if (mCurrentPosition.y + bottom < 0.0f) {
mCurrentPosition.y = (-bottom + 1);
verticalHit = true;
mVerticalHitNormal.y = (mVerticalHitNormal.y + 1.0f);
mVerticalHitNormal.normalize();
} else*/ if (mCurrentPosition.y + top > level.getLevelHeight()) {
mCurrentPosition.y = (level.getLevelHeight() - top - 1);
mVerticalHitNormal.y = (mVerticalHitNormal.y - 1.0f);
mVerticalHitNormal.normalize();
verticalHit = true;
}
}
// One more set of tests to make sure that we are aligned with the surface.
// This time we will just check the inside of the bounding box for intersections.
// The sweep tests above will keep us out of collision in most cases, but this
// test will ensure that we are aligned to incline surfaces correctly.
// Shoot a vertical line through the middle of the box.
if (mDelta.x != 0.0f && mDelta.y != 0.0f) {
float yStart = top;
float yEnd = bottom;
mRayStart.set(centerOffsetX, yStart);
mRayStart.add(mCurrentPosition);
mRayEnd.set(centerOffsetX, yEnd);
mRayEnd.add(mCurrentPosition);
mFilterDirection.set(mDelta);
if (collision.castRay(mRayStart, mRayEnd, mFilterDirection, mVerticalHitPoint,
mVerticalHitNormal, parentObject)) {
// If we found a collision, use this surface as our vertical intersection
// for this frame, even if the sweep above also found something.
verticalHit = true;
// snap
if (mVerticalHitNormal.y > 0.0f) {
mCurrentPosition.y = (mVerticalHitPoint.y - bottom);
} else if (mVerticalHitNormal.y < 0.0f) {
mCurrentPosition.y = (mVerticalHitPoint.y - top);
}
}
// Now the horizontal version of the same test
float xStart = left;
float xEnd = right;
if (mDelta.x < 0.0f) {
xStart = right;
xEnd = left;
}
mRayStart.set(xStart, centerOffsetY);
mRayStart.add(mCurrentPosition);
mRayEnd.set(xEnd, centerOffsetY);
mRayEnd.add(mCurrentPosition);
mFilterDirection.set(mDelta);
if (collision.castRay(mRayStart, mRayEnd, mFilterDirection, mHorizontalHitPoint,
mHorizontalHitNormal, parentObject)) {
// If we found a collision, use this surface as our horizontal intersection
// for this frame, even if the sweep above also found something.
horizontalHit = true;
// snap
if (mHorizontalHitNormal.x > 0.0f) {
mCurrentPosition.x = (mHorizontalHitPoint.x - left);
} else if (mHorizontalHitNormal.x < 0.0f) {
mCurrentPosition.x = (mHorizontalHitPoint.x - right);
}
}
}
// Record the intersection for other systems to use.
final TimeSystem timeSystem = sSystemRegistry.timeSystem;
if (timeSystem != null) {
float time = timeSystem.getGameTime();
if (horizontalHit) {
if (mHorizontalHitNormal.x > 0.0f) {
parentObject.setLastTouchedLeftWallTime(time);
} else {
parentObject.setLastTouchedRightWallTime(time);
}
//parentObject.setBackgroundCollisionNormal(mHorizontalHitNormal);
}
if (verticalHit) {
if (mVerticalHitNormal.y > 0.0f) {
parentObject.setLastTouchedFloorTime(time);
} else {
parentObject.setLastTouchedCeilingTime(time);
}
//parentObject.setBackgroundCollisionNormal(mVerticalHitNormal);
}
// If we hit multiple surfaces, merge their normals together to produce an
// average direction of obstruction.
if (true) { //(verticalHit && horizontalHit) {
mMergedNormal.set(mVerticalHitNormal);
mMergedNormal.add(mHorizontalHitNormal);
mMergedNormal.normalize();
parentObject.setBackgroundCollisionNormal(mMergedNormal);
}
parentObject.setPosition(mCurrentPosition);
}
}
}
mPreviousPosition.set(parentObject.getPosition());
}
/* Sweeps the space between two points looking for surfaces that oppose horizontal movement. */
protected boolean sweepHorizontal(Vector2 previousPosition, Vector2 currentPosition, Vector2 delta,
int left, int right, float centerY, Vector2 hitPoint, Vector2 hitNormal,
GameObject parentObject) {
boolean hit = false;
if (!Utils.close(delta.x, 0.0f)) {
CollisionSystem collision = sSystemRegistry.collisionSystem;
// Shoot a ray from the center of the previous frame's box to the edge (left or right,
// depending on the direction of movement) of the current box.
mTestPointStart.y = (centerY);
mTestPointStart.x = (left);
int offset = -left;
if (delta.x > 0.0f) {
mTestPointStart.x = (right);
offset = -right;
}
// Filter out surfaces that do not oppose motion in the horizontal direction, or
// push in the same direction as movement.
mFilterDirection.set(delta);
mFilterDirection.y = (0);
mTestPointEnd.set(currentPosition);
mTestPointEnd.add(mTestPointStart);
if (collision.castRay(previousPosition, mTestPointEnd, mFilterDirection,
hitPoint, hitNormal, parentObject)) {
// snap
currentPosition.x = (hitPoint.x + offset);
hit = true;
}
}
return hit;
}
/* Sweeps the space between two points looking for surfaces that oppose vertical movement. */
protected boolean sweepVertical(Vector2 previousPosition, Vector2 currentPosition, Vector2 delta,
int bottom, int top, float centerX, Vector2 hitPoint, Vector2 hitNormal,
GameObject parentObject) {
boolean hit = false;
if (!Utils.close(delta.y, 0.0f)) {
CollisionSystem collision = sSystemRegistry.collisionSystem;
// Shoot a ray from the center of the previous frame's box to the edge (top or bottom,
// depending on the direction of movement) of the current box.
mTestPointStart.x = (centerX);
mTestPointStart.y = (bottom);
int offset = -bottom;
if (delta.y > 0.0f) {
mTestPointStart.y = (top);
offset = -top;
}
mFilterDirection.set(delta);
mFilterDirection.x = (0);
mTestPointEnd.set(currentPosition);
mTestPointEnd.add(mTestPointStart);
if (collision.castRay(previousPosition, mTestPointEnd, mFilterDirection,
hitPoint, hitNormal, parentObject)) {
hit = true;
// snap
currentPosition.y = (hitPoint.y + offset);
}
}
return hit;
}
/** Comparator for hit points. */
private static class HitPointDistanceComparator implements Comparator<HitPoint> {
private Vector2 mOrigin;
public HitPointDistanceComparator() {
super();
mOrigin = new Vector2();
}
public final void setOrigin(Vector2 origin) {
mOrigin.set(origin);
}
public final void setOrigin(float x, float y) {
mOrigin.set(x, y);
}
public int compare(HitPoint object1, HitPoint object2) {
int result = 0;
if (object1 != null && object2 != null) {
final float obj1Distance = object1.hitPoint.distance2(mOrigin);
final float obj2Distance = object2.hitPoint.distance2(mOrigin);
final float distanceDelta = obj1Distance - obj2Distance;
result = distanceDelta < 0.0f ? -1 : 1;
} else if (object1 == null && object2 != null) {
result = 1;
} else if (object2 == null && object1 != null) {
result = -1;
}
return result;
}
}
}