1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 18 package com.replica.replicaisland; 19 20 import java.util.Comparator; 21 22 /** 23 * Handles collision against the background. Snaps colliding objects out of collision and reports 24 * the hit to the parent game object. 25 */ 26 public class BackgroundCollisionComponent extends GameComponent { 27 private Vector2 mPreviousPosition; 28 private int mWidth; 29 private int mHeight; 30 private int mHorizontalOffset; 31 private int mVerticalOffset; 32 33 // Workspace vectors. Allocated up front for speed. 34 private Vector2 mCurrentPosition; 35 private Vector2 mPreviousCenter; 36 private Vector2 mDelta; 37 private Vector2 mFilterDirection; 38 private Vector2 mHorizontalHitPoint; 39 private Vector2 mHorizontalHitNormal; 40 private Vector2 mVerticalHitPoint; 41 private Vector2 mVerticalHitNormal; 42 private Vector2 mRayStart; 43 private Vector2 mRayEnd; 44 private Vector2 mTestPointStart; 45 private Vector2 mTestPointEnd; 46 private Vector2 mMergedNormal; 47 48 /** 49 * Sets up the collision bounding box. This box may be a different size than the bounds of the 50 * sprite that this object controls. 51 * @param width The width of the collision box. 52 * @param height The height of the collision box. 53 * @param horzOffset The offset of the collision box from the object's origin in the x axis. 54 * @param vertOffset The offset of the collision box from the object's origin in the y axis. 55 */ BackgroundCollisionComponent(int width, int height, int horzOffset, int vertOffset)56 public BackgroundCollisionComponent(int width, int height, int horzOffset, int vertOffset) { 57 super(); 58 setPhase(ComponentPhases.COLLISION_RESPONSE.ordinal()); 59 mPreviousPosition = new Vector2(); 60 mWidth = width; 61 mHeight = height; 62 mHorizontalOffset = horzOffset; 63 mVerticalOffset = vertOffset; 64 65 mCurrentPosition = new Vector2(); 66 mPreviousCenter = new Vector2(); 67 mDelta = new Vector2(); 68 mFilterDirection = new Vector2(); 69 mHorizontalHitPoint = new Vector2(); 70 mHorizontalHitNormal = new Vector2(); 71 mVerticalHitPoint = new Vector2(); 72 mVerticalHitNormal = new Vector2(); 73 mRayStart = new Vector2(); 74 mRayEnd = new Vector2(); 75 mTestPointStart = new Vector2(); 76 mTestPointEnd = new Vector2(); 77 mMergedNormal = new Vector2(); 78 } 79 BackgroundCollisionComponent()80 public BackgroundCollisionComponent() { 81 super(); 82 setPhase(ComponentPhases.COLLISION_RESPONSE.ordinal()); 83 mPreviousPosition = new Vector2(); 84 mCurrentPosition = new Vector2(); 85 mPreviousCenter = new Vector2(); 86 mDelta = new Vector2(); 87 mFilterDirection = new Vector2(); 88 mHorizontalHitPoint = new Vector2(); 89 mHorizontalHitNormal = new Vector2(); 90 mVerticalHitPoint = new Vector2(); 91 mVerticalHitNormal = new Vector2(); 92 mRayStart = new Vector2(); 93 mRayEnd = new Vector2(); 94 mTestPointStart = new Vector2(); 95 mTestPointEnd = new Vector2(); 96 mMergedNormal = new Vector2(); 97 } 98 99 @Override reset()100 public void reset() { 101 mPreviousPosition.zero(); 102 } 103 setSize(int width, int height)104 public void setSize(int width, int height) { 105 mWidth = width; 106 mHeight = height; 107 // TODO: Resize might cause new collisions. 108 } 109 setOffset(int horzOffset, int vertOffset)110 public void setOffset(int horzOffset, int vertOffset) { 111 mHorizontalOffset = horzOffset; 112 mVerticalOffset = vertOffset; 113 } 114 115 /** 116 * This function is the meat of the collision response logic. Our collision detection and 117 * response must be capable of dealing with arbitrary surfaces and must be frame rate 118 * independent (we must sweep the space in-between frames to find collisions reliably). The 119 * following algorithm is used to keep the collision box out of the collision world. 120 * 1. Cast a ray from the center point of the box at its position last frame to the edge 121 * of the box at its current position. If the ray intersects anything, snap the box 122 * back to the point of intersection. 123 * 2. Perform Step 1 twice: once looking for surfaces opposing horizontal movement and 124 * again for surfaces opposing vertical movement. These two ray tests approximate the 125 * movement of the box between the previous frame and this one. 126 * 3. Since most collisions are collisions with the ground, more precision is required for 127 * vertical intersections. Perform another ray test, this time from the top of the 128 * box's position (after snapping in Step 2) to the bottom. Snap out of any vertical 129 * surfaces that the ray encounters. This will ensure consistent snapping behavior on 130 * incline surfaces. 131 * 4. Add the normals of the surfaces that were hit up and normalize the result to produce 132 * a direction describing the average slope of the surfaces that the box is resting on. 133 * Physics will use this value as a normal to resolve collisions with the background. 134 */ 135 @Override update(float timeDelta, BaseObject parent)136 public void update(float timeDelta, BaseObject parent) { 137 GameObject parentObject = (GameObject) parent; 138 parentObject.setBackgroundCollisionNormal(Vector2.ZERO); 139 if (mPreviousPosition.length2() != 0) { 140 CollisionSystem collision = sSystemRegistry.collisionSystem; 141 if (collision != null) { 142 final int left = mHorizontalOffset; 143 final int bottom = mVerticalOffset; 144 final int right = left + mWidth; 145 final int top = bottom + mHeight; 146 final float centerOffsetX = ((mWidth) / 2.0f) + left; 147 final float centerOffsetY = ((mHeight) / 2.0f) + bottom; 148 149 mCurrentPosition.set(parentObject.getPosition()); 150 mDelta.set(mCurrentPosition); 151 mDelta.subtract(mPreviousPosition); 152 153 mPreviousCenter.set(centerOffsetX, centerOffsetY); 154 mPreviousCenter.add(mPreviousPosition); 155 156 boolean horizontalHit = false; 157 boolean verticalHit = false; 158 159 mVerticalHitPoint.zero(); 160 mVerticalHitNormal.zero(); 161 mHorizontalHitPoint.zero(); 162 mHorizontalHitNormal.zero(); 163 164 165 // The order in which we sweep the horizontal and vertical space can affect the 166 // final result because we perform incremental snapping mid-sweep. So it is 167 // necessary to sweep in the primary direction of movement first. 168 if (Math.abs(mDelta.x) > Math.abs(mDelta.y)) { 169 horizontalHit = sweepHorizontal(mPreviousCenter, mCurrentPosition, mDelta, left, 170 right, centerOffsetY, mHorizontalHitPoint, mHorizontalHitNormal, 171 parentObject); 172 verticalHit = sweepVertical(mPreviousCenter, mCurrentPosition, mDelta, bottom, 173 top, centerOffsetX, mVerticalHitPoint, mVerticalHitNormal, 174 parentObject); 175 176 } else { 177 verticalHit = sweepVertical(mPreviousCenter, mCurrentPosition, mDelta, bottom, 178 top, centerOffsetX, mVerticalHitPoint, mVerticalHitNormal, 179 parentObject); 180 horizontalHit = sweepHorizontal(mPreviousCenter, mCurrentPosition, mDelta, left, 181 right, centerOffsetY, mHorizontalHitPoint, mHorizontalHitNormal, 182 parentObject); 183 } 184 185 // force the collision volume to stay within the bounds of the world. 186 LevelSystem level = sSystemRegistry.levelSystem; 187 if (level != null) { 188 if (mCurrentPosition.x + left < 0.0f) { 189 mCurrentPosition.x = (-left + 1); 190 horizontalHit = true; 191 mHorizontalHitNormal.x = (mHorizontalHitNormal.x + 1.0f); 192 mHorizontalHitNormal.normalize(); 193 } else if (mCurrentPosition.x + right > level.getLevelWidth()) { 194 mCurrentPosition.x = (level.getLevelWidth() - right - 1); 195 mHorizontalHitNormal.x = (mHorizontalHitNormal.x - 1.0f); 196 mHorizontalHitNormal.normalize(); 197 horizontalHit = true; 198 } 199 200 /*if (mCurrentPosition.y + bottom < 0.0f) { 201 mCurrentPosition.y = (-bottom + 1); 202 verticalHit = true; 203 mVerticalHitNormal.y = (mVerticalHitNormal.y + 1.0f); 204 mVerticalHitNormal.normalize(); 205 } else*/ if (mCurrentPosition.y + top > level.getLevelHeight()) { 206 mCurrentPosition.y = (level.getLevelHeight() - top - 1); 207 mVerticalHitNormal.y = (mVerticalHitNormal.y - 1.0f); 208 mVerticalHitNormal.normalize(); 209 verticalHit = true; 210 } 211 212 } 213 214 215 // One more set of tests to make sure that we are aligned with the surface. 216 // This time we will just check the inside of the bounding box for intersections. 217 // The sweep tests above will keep us out of collision in most cases, but this 218 // test will ensure that we are aligned to incline surfaces correctly. 219 220 // Shoot a vertical line through the middle of the box. 221 if (mDelta.x != 0.0f && mDelta.y != 0.0f) { 222 float yStart = top; 223 float yEnd = bottom; 224 225 226 mRayStart.set(centerOffsetX, yStart); 227 mRayStart.add(mCurrentPosition); 228 229 mRayEnd.set(centerOffsetX, yEnd); 230 mRayEnd.add(mCurrentPosition); 231 232 mFilterDirection.set(mDelta); 233 234 if (collision.castRay(mRayStart, mRayEnd, mFilterDirection, mVerticalHitPoint, 235 mVerticalHitNormal, parentObject)) { 236 237 // If we found a collision, use this surface as our vertical intersection 238 // for this frame, even if the sweep above also found something. 239 verticalHit = true; 240 // snap 241 if (mVerticalHitNormal.y > 0.0f) { 242 mCurrentPosition.y = (mVerticalHitPoint.y - bottom); 243 } else if (mVerticalHitNormal.y < 0.0f) { 244 mCurrentPosition.y = (mVerticalHitPoint.y - top); 245 } 246 } 247 248 249 // Now the horizontal version of the same test 250 float xStart = left; 251 float xEnd = right; 252 if (mDelta.x < 0.0f) { 253 xStart = right; 254 xEnd = left; 255 } 256 257 258 mRayStart.set(xStart, centerOffsetY); 259 mRayStart.add(mCurrentPosition); 260 261 mRayEnd.set(xEnd, centerOffsetY); 262 mRayEnd.add(mCurrentPosition); 263 264 mFilterDirection.set(mDelta); 265 266 if (collision.castRay(mRayStart, mRayEnd, mFilterDirection, mHorizontalHitPoint, 267 mHorizontalHitNormal, parentObject)) { 268 269 // If we found a collision, use this surface as our horizontal intersection 270 // for this frame, even if the sweep above also found something. 271 horizontalHit = true; 272 // snap 273 if (mHorizontalHitNormal.x > 0.0f) { 274 mCurrentPosition.x = (mHorizontalHitPoint.x - left); 275 } else if (mHorizontalHitNormal.x < 0.0f) { 276 mCurrentPosition.x = (mHorizontalHitPoint.x - right); 277 } 278 } 279 } 280 281 282 // Record the intersection for other systems to use. 283 final TimeSystem timeSystem = sSystemRegistry.timeSystem; 284 285 if (timeSystem != null) { 286 float time = timeSystem.getGameTime(); 287 if (horizontalHit) { 288 if (mHorizontalHitNormal.x > 0.0f) { 289 parentObject.setLastTouchedLeftWallTime(time); 290 } else { 291 parentObject.setLastTouchedRightWallTime(time); 292 } 293 //parentObject.setBackgroundCollisionNormal(mHorizontalHitNormal); 294 } 295 296 if (verticalHit) { 297 if (mVerticalHitNormal.y > 0.0f) { 298 parentObject.setLastTouchedFloorTime(time); 299 } else { 300 parentObject.setLastTouchedCeilingTime(time); 301 } 302 //parentObject.setBackgroundCollisionNormal(mVerticalHitNormal); 303 } 304 305 306 // If we hit multiple surfaces, merge their normals together to produce an 307 // average direction of obstruction. 308 if (true) { //(verticalHit && horizontalHit) { 309 mMergedNormal.set(mVerticalHitNormal); 310 mMergedNormal.add(mHorizontalHitNormal); 311 mMergedNormal.normalize(); 312 parentObject.setBackgroundCollisionNormal(mMergedNormal); 313 } 314 315 parentObject.setPosition(mCurrentPosition); 316 } 317 318 } 319 } 320 mPreviousPosition.set(parentObject.getPosition()); 321 } 322 323 /* Sweeps the space between two points looking for surfaces that oppose horizontal movement. */ sweepHorizontal(Vector2 previousPosition, Vector2 currentPosition, Vector2 delta, int left, int right, float centerY, Vector2 hitPoint, Vector2 hitNormal, GameObject parentObject)324 protected boolean sweepHorizontal(Vector2 previousPosition, Vector2 currentPosition, Vector2 delta, 325 int left, int right, float centerY, Vector2 hitPoint, Vector2 hitNormal, 326 GameObject parentObject) { 327 boolean hit = false; 328 if (!Utils.close(delta.x, 0.0f)) { 329 CollisionSystem collision = sSystemRegistry.collisionSystem; 330 331 // Shoot a ray from the center of the previous frame's box to the edge (left or right, 332 // depending on the direction of movement) of the current box. 333 mTestPointStart.y = (centerY); 334 mTestPointStart.x = (left); 335 int offset = -left; 336 if (delta.x > 0.0f) { 337 mTestPointStart.x = (right); 338 offset = -right; 339 } 340 341 // Filter out surfaces that do not oppose motion in the horizontal direction, or 342 // push in the same direction as movement. 343 mFilterDirection.set(delta); 344 mFilterDirection.y = (0); 345 346 mTestPointEnd.set(currentPosition); 347 mTestPointEnd.add(mTestPointStart); 348 if (collision.castRay(previousPosition, mTestPointEnd, mFilterDirection, 349 hitPoint, hitNormal, parentObject)) { 350 // snap 351 currentPosition.x = (hitPoint.x + offset); 352 hit = true; 353 } 354 } 355 return hit; 356 } 357 358 /* Sweeps the space between two points looking for surfaces that oppose vertical movement. */ sweepVertical(Vector2 previousPosition, Vector2 currentPosition, Vector2 delta, int bottom, int top, float centerX, Vector2 hitPoint, Vector2 hitNormal, GameObject parentObject)359 protected boolean sweepVertical(Vector2 previousPosition, Vector2 currentPosition, Vector2 delta, 360 int bottom, int top, float centerX, Vector2 hitPoint, Vector2 hitNormal, 361 GameObject parentObject) { 362 boolean hit = false; 363 if (!Utils.close(delta.y, 0.0f)) { 364 CollisionSystem collision = sSystemRegistry.collisionSystem; 365 // Shoot a ray from the center of the previous frame's box to the edge (top or bottom, 366 // depending on the direction of movement) of the current box. 367 mTestPointStart.x = (centerX); 368 mTestPointStart.y = (bottom); 369 int offset = -bottom; 370 if (delta.y > 0.0f) { 371 mTestPointStart.y = (top); 372 offset = -top; 373 } 374 375 mFilterDirection.set(delta); 376 mFilterDirection.x = (0); 377 378 mTestPointEnd.set(currentPosition); 379 mTestPointEnd.add(mTestPointStart); 380 if (collision.castRay(previousPosition, mTestPointEnd, mFilterDirection, 381 hitPoint, hitNormal, parentObject)) { 382 hit = true; 383 // snap 384 currentPosition.y = (hitPoint.y + offset); 385 } 386 387 } 388 return hit; 389 } 390 391 /** Comparator for hit points. */ 392 private static class HitPointDistanceComparator implements Comparator<HitPoint> { 393 private Vector2 mOrigin; 394 HitPointDistanceComparator()395 public HitPointDistanceComparator() { 396 super(); 397 mOrigin = new Vector2(); 398 } 399 setOrigin(Vector2 origin)400 public final void setOrigin(Vector2 origin) { 401 mOrigin.set(origin); 402 } 403 setOrigin(float x, float y)404 public final void setOrigin(float x, float y) { 405 mOrigin.set(x, y); 406 } 407 compare(HitPoint object1, HitPoint object2)408 public int compare(HitPoint object1, HitPoint object2) { 409 int result = 0; 410 if (object1 != null && object2 != null) { 411 final float obj1Distance = object1.hitPoint.distance2(mOrigin); 412 final float obj2Distance = object2.hitPoint.distance2(mOrigin); 413 final float distanceDelta = obj1Distance - obj2Distance; 414 result = distanceDelta < 0.0f ? -1 : 1; 415 } else if (object1 == null && object2 != null) { 416 result = 1; 417 } else if (object2 == null && object1 != null) { 418 result = -1; 419 } 420 return result; 421 } 422 } 423 } 424