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 */ 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 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 100 public void reset() { 101 mPreviousPosition.zero(); 102 } 103 104 public void setSize(int width, int height) { 105 mWidth = width; 106 mHeight = height; 107 // TODO: Resize might cause new collisions. 108 } 109 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 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. */ 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. */ 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 395 public HitPointDistanceComparator() { 396 super(); 397 mOrigin = new Vector2(); 398 } 399 400 public final void setOrigin(Vector2 origin) { 401 mOrigin.set(origin); 402 } 403 404 public final void setOrigin(float x, float y) { 405 mOrigin.set(x, y); 406 } 407 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