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
17package com.replica.replicaisland;
18
19import android.content.res.AssetManager;
20
21import java.io.IOException;
22import java.io.InputStream;
23
24/**
25 * Collision detection system.  Provides a ray-based interface for finding surfaces in the collision
26 * world.   This version is based on a collision world of line segments, organized into an array of
27 * tiles.  The underlying detection algorithm isn't relevant to calling code, however, so this class
28 * may be extended to provide a completely different collision detection scheme.
29 *
30 * This class also provides a system for runtime-generated collision segments.  These temporary
31 * segments are cleared each frame, and consequently must be constantly re-submitted if they are
32 * intended to persist.  Temporary segments are useful for dynamic solid objects, such as moving
33 * platforms.
34 *
35 * CollisionSystem.TileVisitor is an interface for traversing individual collision tiles.  Ray casts
36 * can be used to run user code over the collision world by passing different TileVisitor
37 * implementations to executeRay.  Provided is TileTestVisitor, a visitor that compares the segments
38 * of each tile visited with the ray and searches for points of intersection.
39 *
40 */
41public class CollisionSystem extends BaseObject {
42    private TiledWorld mWorld;
43    private CollisionTile[] mCollisionTiles;
44    private LineSegmentPool mSegmentPool;
45    private int mTileWidth;
46    private int mTileHeight;
47    private TileTestVisitor mTileSegmentTester;
48    private FixedSizeArray<LineSegment> mTemporarySegments;
49    private FixedSizeArray<LineSegment> mPendingTemporarySegments;
50    private byte[] mWorkspaceBytes;     // Included here to avoid runtime allocation during file io.
51
52    private static final int MAX_TEMPORARY_SEGMENTS = 256;
53
54    public CollisionSystem() {
55        super();
56        mTileSegmentTester = new TileTestVisitor();
57        mSegmentPool = new LineSegmentPool(MAX_TEMPORARY_SEGMENTS);
58
59        mTemporarySegments = new FixedSizeArray<LineSegment>(MAX_TEMPORARY_SEGMENTS);
60        mPendingTemporarySegments = new FixedSizeArray<LineSegment>(MAX_TEMPORARY_SEGMENTS);
61
62        mWorkspaceBytes = new byte[4];
63    }
64
65    @Override
66    public void reset() {
67        mWorld = null;
68        mCollisionTiles = null;
69
70        final int count = mTemporarySegments.getCount();
71        for (int x = 0; x < count; x++) {
72            mSegmentPool.release(mTemporarySegments.get(x));
73            mTemporarySegments.set(x, null);
74        }
75        mTemporarySegments.clear();
76
77        final int pendingCount = mPendingTemporarySegments.getCount();
78        for (int x = 0; x < pendingCount; x++) {
79            mSegmentPool.release(mPendingTemporarySegments.get(x));
80            mPendingTemporarySegments.set(x, null);
81        }
82        mPendingTemporarySegments.clear();
83    }
84
85    /* Sets the current collision world to the supplied tile world. */
86    public void initialize(TiledWorld world, int tileWidth, int tileHeight) {
87        mWorld = world;
88
89        mTileWidth = tileWidth;
90        mTileHeight = tileHeight;
91    }
92
93    /**
94     * Casts a ray into the collision world.  The ray is bound by the start and end points supplied.
95     * The first intersecting segment that is found is returned; in the case where more than one
96     * segment is found, the segment closest to the start point is returned.
97     *
98     * @param startPoint  The starting point for the ray in world units.
99     * @param endPoint  The end point for the ray in world units.
100     * @param movementDirection  If set, only segments with normals that oppose this direction will
101     *      be counted as valid intersections.  If null, all intersecting segments will be
102     *      considered valid.
103     * @param hitPoint  The point of intersection between a ray and a surface, if one is found.
104     * @param hitNormal  The normal of the intersecting surface if an intersection is found.
105     * @param excludeObject If set, dynamic surfaces from this object will be ignored.
106     * @return  true if a valid intersecting surface was found, false otherwise.
107     */
108    // TODO: switch to return data as a HitPoint.
109    public boolean castRay(Vector2 startPoint, Vector2 endPoint, Vector2 movementDirection,
110            Vector2 hitPoint, Vector2 hitNormal, GameObject excludeObject) {
111
112        boolean hit = false;
113
114        mTileSegmentTester.setup(movementDirection, mTileWidth, mTileHeight);
115
116        if (mCollisionTiles != null &&
117                executeRay(startPoint, endPoint, hitPoint, hitNormal, mTileSegmentTester) != -1) {
118            hit = true;
119        }
120
121        if (mTemporarySegments.getCount() > 0) {
122            VectorPool vectorPool = sSystemRegistry.vectorPool;
123            Vector2 tempHitPoint = vectorPool.allocate();
124            Vector2 tempHitNormal = vectorPool.allocate();
125
126            if (testSegmentAgainstList(mTemporarySegments, startPoint, endPoint, tempHitPoint,
127                    tempHitNormal, movementDirection, excludeObject)) {
128                if (hit) {
129                    // Check to see whether this collision is closer to the one we already found or
130                    // not.
131                    final float firstCollisionDistance = startPoint.distance2(hitPoint);
132                    if (firstCollisionDistance > startPoint.distance2(tempHitPoint)) {
133                        // The temporary surface is closer.
134                        hitPoint.set(tempHitPoint);
135                        hitNormal.set(tempHitNormal);
136                    }
137                } else {
138                    hit = true;
139                    hitPoint.set(tempHitPoint);
140                    hitNormal.set(tempHitNormal);
141                }
142            }
143
144            vectorPool.release(tempHitPoint);
145            vectorPool.release(tempHitNormal);
146        }
147
148        return hit;
149    }
150
151    public boolean testBox(float left, float right, float top, float bottom,
152            Vector2 movementDirection, FixedSizeArray<HitPoint> hitPoints,
153            GameObject excludeObject, boolean testDynamicSurfacesOnly) {
154
155        boolean foundHit = false;
156
157        // Test against the background.
158        if (!testDynamicSurfacesOnly) {
159            float startX = left;
160            float endX = right;
161            float startY = bottom;
162            float endY = top;
163            int xIncrement = 1;
164            int yIncrement = 1;
165
166            if (movementDirection != null) {
167                if (movementDirection.x < 0.0f) {
168                    startX = right;
169                    endX = left;
170                    xIncrement = -1;
171                }
172                if (movementDirection.y < 0.0f) {
173                    startY = top;
174                    endY = bottom;
175                    yIncrement = -1;
176                }
177            }
178            final int startTileX = Utils.clamp((int)(startX / mTileWidth), 0, mWorld.getWidth() - 1);
179            final int endTileX = Utils.clamp((int)(endX / mTileWidth), 0, mWorld.getWidth() - 1);
180            final int startTileY = Utils.clamp((int)(startY / mTileHeight), 0, mWorld.getHeight() - 1);
181            final int endTileY = Utils.clamp((int)(endY / mTileHeight), 0, mWorld.getHeight() - 1);
182
183            VectorPool vectorPool = sSystemRegistry.vectorPool;
184            Vector2 worldTileOffset = vectorPool.allocate();
185
186            final int[][] tileArray = mWorld.getTiles();
187            final int worldHeight = mWorld.getHeight() - 1;
188
189
190            for (int y = startTileY; y != endTileY + yIncrement; y += yIncrement) {
191                for (int x = startTileX; x != endTileX + xIncrement; x += xIncrement) {
192                    final int tileIndex = tileArray[x][worldHeight - y];
193                    if (tileIndex >= 0 && tileIndex < mCollisionTiles.length
194                            && mCollisionTiles[tileIndex] != null) {
195
196                        final float xOffset = x * mTileWidth;
197                        final float yOffset = y * mTileHeight;
198
199                        final float tileSpaceLeft = left - xOffset;
200                        final float tileSpaceRight = right - xOffset;
201                        final float tileSpaceTop = top - yOffset;
202                        final float tileSpaceBottom = bottom - yOffset;
203
204                        worldTileOffset.set(xOffset, yOffset);
205
206                        boolean hit = testBoxAgainstList(mCollisionTiles[tileIndex].segments,
207                                tileSpaceLeft, tileSpaceRight, tileSpaceTop, tileSpaceBottom,
208                                movementDirection, excludeObject, worldTileOffset, hitPoints);
209
210                        if (hit) {
211                            foundHit = true;
212                        }
213
214                    }
215                }
216            }
217
218            vectorPool.release(worldTileOffset);
219        }
220        // temporary segments
221        boolean tempHit = testBoxAgainstList(mTemporarySegments,
222                left, right, top, bottom,
223                movementDirection, excludeObject, Vector2.ZERO, hitPoints);
224
225        if (tempHit) {
226            foundHit = true;
227        }
228
229
230
231        return foundHit;
232    }
233
234    /* Inserts a temporary surface into the collision world.  It will persist for one frame. */
235    public void addTemporarySurface(Vector2 startPoint, Vector2 endPoint, Vector2 normal,
236            GameObject ownerObject) {
237        LineSegment newSegment = mSegmentPool.allocate();
238
239        newSegment.set(startPoint, endPoint, normal);
240        newSegment.setOwner(ownerObject);
241
242        mPendingTemporarySegments.add(newSegment);
243    }
244
245    @Override
246    public void update(float timeDelta, BaseObject parent) {
247        // Clear temporary surfaces
248        final int count = mTemporarySegments.getCount();
249        if (mCollisionTiles != null && count > 0) {
250            for (int x = 0; x < count; x++) {
251                mSegmentPool.release(mTemporarySegments.get(x));
252                mTemporarySegments.set(x, null);
253            }
254            mTemporarySegments.clear();
255        }
256
257        // Temporary surfaces must persist for one frame in order to be reliable independent of
258        // frame execution order.  So each frame we queue up inserted segments and then swap them
259        // into activity when this system is updated.
260        FixedSizeArray<LineSegment> swap = mTemporarySegments;
261        mTemporarySegments = mPendingTemporarySegments;
262        mPendingTemporarySegments = swap;
263    }
264
265    /**
266     * Shoots a ray through the collision world.  This function is similar to executeRay() below,
267     * except that it is optimized for straight lines (either completely horizontal or completely
268     * vertical).
269     *
270     * @param startPoint  The starting point for the ray, in world space.
271     * @param endPoint  The ending point for the ray in world space.
272     * @param hitPoint  Set to the intersection coordinates if an intersection is found.
273     * @param hitNormal  Set to the normal of the intersecting surface if an intersection is found.
274     * @param visitor  Class defining what work to perform at each tile step.
275     * @return  The index of the tile that intersected the ray, or -1 if no intersection was found.
276     */
277    protected int executeStraigtRay(final Vector2 startPoint, final Vector2 endPoint,
278            final int startTileX, final int startTileY, final int endTileX, final int endTileY,
279            final int deltaX, final int deltaY,
280            Vector2 hitPoint, Vector2 hitNormal, TileVisitor visitor) {
281
282        int currentX = startTileX;
283        int currentY = startTileY;
284
285        int xIncrement = 0;
286        int yIncrement = 0;
287        int distance = 0;
288
289        if (deltaX != 0) {
290            distance = Math.abs(deltaX) + 1;
291            xIncrement = Utils.sign(deltaX);
292        } else if (deltaY != 0) {
293            distance = Math.abs(deltaY) + 1;
294            yIncrement = Utils.sign(deltaY);
295        }
296
297        int hitTile = -1;
298        final int worldHeight = mWorld.getHeight() - 1;
299        final int[][] tileArray = mWorld.getTiles();
300        for (int x = 0; x < distance; x++) {
301            final int tileIndex = tileArray[currentX][worldHeight - currentY];
302            if (tileIndex >= 0 && tileIndex < mCollisionTiles.length
303                    && mCollisionTiles[tileIndex] != null) {
304                if (visitor.visit(mCollisionTiles[tileIndex], startPoint, endPoint,
305                        hitPoint, hitNormal, currentX, currentY)) {
306                    hitTile = tileIndex;
307                    break;
308                }
309            }
310            currentX += xIncrement;
311            currentY += yIncrement;
312        }
313
314        return hitTile;
315    }
316
317    /**
318     * Shoots a ray through the collision world.  Since the collision world is a 2D array of tiles,
319     * this algorithm traces a line in tile space and tests against each non-empty tile it visits.
320     * The Bresenham line algorithm is used for the actual traversal, but the action taken at each
321     * tile is defined by the visitor class passed to this function.
322     *
323     * @param startPoint  The starting point for the ray, in world space.
324     * @param endPoint  The ending point for the ray in world space.
325     * @param hitPoint  Set to the intersection coordinates if an intersection is found.
326     * @param hitNormal  Set to the normal of the intersecting surface if an intersection is found.
327     * @param visitor  Class defining what work to perform at each tile step.
328     * @return  The index of the tile that intersected the ray, or -1 if no intersection was found.
329     */
330    protected int executeRay(Vector2 startPoint, Vector2 endPoint,
331            Vector2 hitPoint, Vector2 hitNormal, TileVisitor visitor) {
332
333        final int worldHeight = mWorld.getHeight();
334        final int worldWidth = mWorld.getWidth();
335
336        final int startTileX = worldToTileColumn(startPoint.x, worldWidth);
337        final int startTileY = worldToTileRow(startPoint.y, worldHeight);
338
339        final int endTileX = worldToTileColumn(endPoint.x, worldWidth);
340        final int endTileY = worldToTileRow(endPoint.y, worldHeight);
341
342        int currentX = startTileX;
343        int currentY = startTileY;
344
345        final int deltaX = endTileX - startTileX;
346        final int deltaY = endTileY - startTileY;
347
348        int hitTile = -1;
349
350        if (deltaX == 0 || deltaY == 0) {
351            hitTile = executeStraigtRay(startPoint, endPoint, startTileX, startTileY,
352                    endTileX, endTileY, deltaX, deltaY, hitPoint, hitNormal, visitor);
353        } else {
354
355            final int xIncrement = deltaX != 0 ? Utils.sign(deltaX) : 0;
356            final int yIncrement = deltaY != 0 ? Utils.sign(deltaY) : 0;
357
358            // Note: I'm deviating from the Bresenham algorithm here by adding one to force the end
359            // tile to be visited.
360            final int lateralDelta = (endTileX > 0 && endTileX < worldWidth - 1) ? Math.abs(deltaX) + 1 : Math.abs(deltaX);
361            final int verticalDelta = (endTileY > 0 && endTileY < worldHeight - 1) ? Math.abs(deltaY) + 1 : Math.abs(deltaY);
362
363            final int deltaX2 = lateralDelta * 2;
364            final int deltaY2 = verticalDelta * 2;
365
366            final int worldHeightMinusOne = worldHeight - 1;
367            final int[][]tileArray = mWorld.getTiles();
368
369            // Bresenham line algorithm in tile space.
370            if (lateralDelta >= verticalDelta) {
371                int error = deltaY2 - lateralDelta;
372                for (int i = 0; i < lateralDelta; i++) {
373                    final int tileIndex = tileArray[currentX][worldHeightMinusOne - currentY];
374                    if (tileIndex >= 0 && tileIndex < mCollisionTiles.length
375                            && mCollisionTiles[tileIndex] != null) {
376                        if (visitor.visit(mCollisionTiles[tileIndex], startPoint, endPoint,
377                                hitPoint, hitNormal, currentX, currentY)) {
378                            hitTile = tileIndex;
379                            break;
380                        }
381                    }
382
383                    if (error > 0) {
384                        currentY += yIncrement;
385                        error -= deltaX2;
386                    }
387
388                    error += deltaY2;
389                    currentX += xIncrement;
390                }
391            }
392            else if (verticalDelta >= lateralDelta) {
393                int error = deltaX2 - verticalDelta;
394
395                for (int i = 0; i < verticalDelta; i++) {
396                    final int tileIndex = tileArray[currentX][worldHeightMinusOne - currentY];
397                    if (tileIndex >= 0 && tileIndex < mCollisionTiles.length
398                            && mCollisionTiles[tileIndex] != null) {
399                        if (visitor.visit(mCollisionTiles[tileIndex], startPoint, endPoint,
400                                hitPoint, hitNormal, currentX, currentY)) {
401                            hitTile = tileIndex;
402                            break;
403                        }
404                    }
405
406                    if (error > 0) {
407                        currentX += xIncrement;
408                        error -= deltaY2;
409                    }
410
411                    error += deltaX2;
412                    currentY += yIncrement;
413                }
414            }
415        }
416        return hitTile;
417    }
418
419    protected final int worldToTileColumn(final float x, final int width) {
420        return Utils.clamp((int)Math.floor(x / mTileWidth), 0, width - 1);
421    }
422
423    protected final int worldToTileRow(float y, final int height) {
424        return Utils.clamp((int)Math.floor(y / mTileHeight), 0, height - 1);
425    }
426
427    /*
428     * Given a list of segments and a ray, this function performs an intersection search and
429     * returns the closest intersecting segment, if any exists.
430     */
431    protected static boolean testSegmentAgainstList(FixedSizeArray<LineSegment> segments,
432            Vector2 startPoint, Vector2 endPoint, Vector2 hitPoint, Vector2 hitNormal,
433            Vector2 movementDirection, GameObject excludeObject) {
434        boolean foundHit = false;
435        float closestDistance = -1;
436        float hitX = 0;
437        float hitY = 0;
438        float normalX = 0;
439        float normalY = 0;
440        final int count = segments.getCount();
441        final Object[] segmentArray = segments.getArray();
442        for (int x = 0; x < count; x++) {
443            LineSegment segment = (LineSegment)segmentArray[x];
444            // If a movement direction has been passed, filter out invalid surfaces by ignoring
445            // those that do not oppose movement.  If no direction has been passed, accept all
446            // surfaces.
447            final float dot = movementDirection.length2() > 0.0f ?
448                    movementDirection.dot(segment.mNormal) : -1.0f;
449
450            if (dot < 0.0f &&
451                    (excludeObject == null || segment.owner != excludeObject) &&
452                    segment.calculateIntersection(startPoint, endPoint, hitPoint)) {
453                final float distance = hitPoint.distance2(startPoint);
454
455                if (!foundHit || closestDistance > distance) {
456                    closestDistance = distance;
457                    foundHit = true;
458                    normalX = segment.mNormal.x;
459                    normalY = segment.mNormal.y;
460                    // Store the components on their own so we don't have to allocate a vector
461                    // in this loop.
462                    hitX = hitPoint.x;
463                    hitY = hitPoint.y;
464                }
465            }
466        }
467
468        if (foundHit) {
469            hitPoint.set(hitX, hitY);
470            hitNormal.set(normalX, normalY);
471        }
472        return foundHit;
473    }
474
475    protected static boolean testBoxAgainstList(FixedSizeArray<LineSegment> segments,
476            float left, float right, float top, float bottom,
477            Vector2 movementDirection, GameObject excludeObject, Vector2 outputOffset,
478            FixedSizeArray<HitPoint> outputHitPoints) {
479        int hitCount = 0;
480        final int maxSegments = outputHitPoints.getCapacity() - outputHitPoints.getCount();
481        final int count = segments.getCount();
482        final Object[] segmentArray = segments.getArray();
483
484        VectorPool vectorPool = sSystemRegistry.vectorPool;
485        HitPointPool hitPool = sSystemRegistry.hitPointPool;
486
487        Vector2 tempHitPoint = vectorPool.allocate();
488
489        for (int x = 0; x < count && hitCount < maxSegments; x++) {
490            LineSegment segment = (LineSegment)segmentArray[x];
491            // If a movement direction has been passed, filter out invalid surfaces by ignoring
492            // those that do not oppose movement.  If no direction has been passed, accept all
493            // surfaces.
494            final float dot = movementDirection.length2() > 0.0f ?
495                    movementDirection.dot(segment.mNormal) : -1.0f;
496
497            if (dot < 0.0f &&
498                    (excludeObject == null || segment.owner != excludeObject) &&
499                    segment.calculateIntersectionBox(left, right, top, bottom, tempHitPoint)) {
500
501                Vector2 hitPoint = vectorPool.allocate(tempHitPoint);
502                Vector2 hitNormal = vectorPool.allocate(segment.mNormal);
503
504                hitPoint.add(outputOffset);
505                HitPoint hit = hitPool.allocate();
506
507                hit.hitPoint = hitPoint;
508                hit.hitNormal = hitNormal;
509
510                outputHitPoints.add(hit);
511
512                hitCount++;
513            }
514        }
515
516        vectorPool.release(tempHitPoint);
517
518        return hitCount > 0;
519    }
520
521    /*
522     * Loads line segments from a binary file and builds the tiled collision database
523     * accordingly.
524     */
525    public boolean loadCollisionTiles(InputStream stream) {
526        boolean success = false;
527        AssetManager.AssetInputStream byteStream = (AssetManager.AssetInputStream) stream;
528        int signature;
529
530        // TODO: this is a hack.  I really should only allocate an array that is the size of the
531        // tileset, but at this point I don't actually know that size, so I allocate a buffer that's
532        // probably large enough.
533        mCollisionTiles = new CollisionTile[256];
534        try {
535            signature = (byte)byteStream.read();
536            if (signature == 52) {
537                // This file has the following deserialization format:
538                //   read the number of tiles
539                //   for each tile
540                //     read the tile id
541                //     read the number of segments
542                //     for each segment
543                //       read startx, starty, endx, endy, normalx, normaly
544                final int tileCount = byteStream.read();
545                final int size = (1 + 1 + 4 + 4 + 4 + 4 + 4 + 4) * tileCount;
546                if (byteStream.available() >= size) {
547                    for (int x = 0; x < tileCount; x++) {
548                        final int tileIndex = byteStream.read();
549                        final int segmentCount = byteStream.read();
550
551                        if (mCollisionTiles[tileIndex] == null && segmentCount > 0) {
552                            mCollisionTiles[tileIndex] = new CollisionTile(segmentCount);
553                        }
554
555                        for (int y = 0; y < segmentCount; y++) {
556                            byteStream.read(mWorkspaceBytes, 0, 4);
557                            final float startX = Utils.byteArrayToFloat(mWorkspaceBytes);
558                            byteStream.read(mWorkspaceBytes, 0, 4);
559                            final float startY = Utils.byteArrayToFloat(mWorkspaceBytes);
560                            byteStream.read(mWorkspaceBytes, 0, 4);
561                            final float endX = Utils.byteArrayToFloat(mWorkspaceBytes);
562                            byteStream.read(mWorkspaceBytes, 0, 4);
563                            final float endY = Utils.byteArrayToFloat(mWorkspaceBytes);
564                            byteStream.read(mWorkspaceBytes, 0, 4);
565                            final float normalX = Utils.byteArrayToFloat(mWorkspaceBytes);
566                            byteStream.read(mWorkspaceBytes, 0, 4);
567                            final float normalY = Utils.byteArrayToFloat(mWorkspaceBytes);
568
569                            // TODO: it might be wise to pool line segments.  I don't think that
570                            // this data will be loaded very often though, so this is ok for now.
571                            LineSegment newSegment = new LineSegment();
572                            newSegment.mStartPoint.set(startX, startY);
573                            newSegment.mEndPoint.set(endX, endY);
574                            newSegment.mNormal.set(normalX, normalY);
575
576                            mCollisionTiles[tileIndex].addSegment(newSegment);
577                        }
578                    }
579                }
580            }
581        } catch (IOException e) {
582            //TODO: figure out the best way to deal with this.  Assert?
583        }
584
585        return success;
586    }
587
588
589    /**
590     * An interface for visiting tiles during a ray cast.  Implementations of TileVisitor
591     * can be passed to executeRay(); the visit() function will be invoked for each tile touched by
592     * the ray until the traversal is completed or visit() returns false.
593     */
594    public abstract class TileVisitor extends AllocationGuard {
595        public TileVisitor() {
596            super();
597        }
598
599        // If true is returned, tile scanning continues.  Otherwise it stops.
600        public abstract boolean visit(CollisionTile tile, Vector2 startPoint, Vector2 endPoint,
601            Vector2 hitPoint, Vector2 hitNormal, int tileX, int tileY);
602    }
603
604    /**
605     * TileTestVisitor tests the ray against a list of segments assigned to each tile.  If any
606     * segment in any tile of the ray's path is found to be intersecting with the ray, traversal
607     * stops and intersection information is recorded.
608     */
609    protected class TileTestVisitor extends TileVisitor {
610        // These vectors are all temporary storage variables allocated as class members to avoid
611        // runtime allocation.
612        private Vector2 mDelta;
613        private Vector2 mTileSpaceStart;
614        private Vector2 mTileSpaceEnd;
615        private Vector2 mTileSpaceOffset;
616        private int mTileHeight;
617        private int mTileWidth;
618
619        public TileTestVisitor() {
620            super();
621            mDelta = new Vector2();
622            mTileSpaceStart = new Vector2();
623            mTileSpaceEnd = new Vector2();
624            mTileSpaceOffset = new Vector2();
625        }
626
627        /**
628         * Sets the visitor up for a ray test.  Initializes the size of the tiles and the direction
629         * of movement by which intersections should be filtered.
630         */
631        public void setup(Vector2 movementDirection, int tileWidth, int tileHeight) {
632            if (movementDirection != null) {
633                mDelta.set(movementDirection);
634                mDelta.normalize();
635            } else {
636                mDelta.zero();
637            }
638            mTileWidth = tileWidth;
639            mTileHeight = tileHeight;
640        }
641
642        /**
643         * Converts the ray into tile space and then compares it to the segments
644         * stored in the current tile.
645         */
646        @Override
647        public boolean visit(CollisionTile tile, Vector2 startPoint, Vector2 endPoint,
648                Vector2 hitPoint, Vector2 hitNormal, int tileX, int tileY) {
649            mTileSpaceOffset.set(tileX * mTileWidth, tileY * mTileHeight);
650            mTileSpaceStart.set(startPoint);
651            mTileSpaceStart.subtract(mTileSpaceOffset);
652            mTileSpaceEnd.set(endPoint);
653            mTileSpaceEnd.subtract(mTileSpaceOffset);
654            // find all the hits in the tile and pick the closest to the start point.
655            boolean foundHit = testSegmentAgainstList(tile.segments, mTileSpaceStart, mTileSpaceEnd,
656                    hitPoint, hitNormal, mDelta, null);
657
658            if (foundHit) {
659                // The hitPoint is in tile space, so convert it back to world space.
660                hitPoint.add(mTileSpaceOffset);
661            }
662
663            return foundHit;
664        }
665    }
666
667    /**
668     * A class describing a single surface in the collision world.  Surfaces are stored as a line
669     * segment and a normal. The normal must be normalized (its length must be 1.0) and should
670     * describe the direction that the segment "pushes against" in a collision.
671     */
672    protected class LineSegment extends AllocationGuard {
673        private Vector2 mStartPoint;
674        private Vector2 mEndPoint;
675        public Vector2 mNormal;
676        public GameObject owner;
677
678        public LineSegment() {
679            super();
680            mStartPoint = new Vector2();
681            mEndPoint = new Vector2();
682            mNormal = new Vector2();
683        }
684
685        /* Sets up the line segment.  Values are copied to local storage. */
686        public void set(Vector2 start, Vector2 end, Vector2 norm) {
687            mStartPoint.set(start);
688            mEndPoint.set(end);
689            mNormal.set(norm);
690        }
691
692        public void setOwner(GameObject ownerObject) {
693            owner = ownerObject;
694        }
695        /**
696         * Checks to see if these lines intersect by projecting one onto the other and then
697         * assuring that the collision point is within the range of each segment.
698         */
699        public boolean calculateIntersection(Vector2 otherStart, Vector2 otherEnd,
700                Vector2 hitPoint) {
701            boolean intersecting = false;
702
703            // Reference: http://local.wasp.uwa.edu.au/~pbourke/geometry/lineline2d/
704            final float x1 = mStartPoint.x;
705            final float x2 = mEndPoint.x;
706            final float x3 = otherStart.x;
707            final float x4 = otherEnd.x;
708            final float y1 = mStartPoint.y;
709            final float y2 = mEndPoint.y;
710            final float y3 = otherStart.y;
711            final float y4 = otherEnd.y;
712
713            final float denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
714            if (denom != 0) {
715             final float uA = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom;
716             final float uB = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom;
717
718             if (uA >= 0.0f && uA <= 1.0f && uB >= 0.0f && uB <= 1.0f) {
719                 final float hitX = x1 + (uA * (x2 - x1));
720                 final float hitY = y1 + (uA * (y2 - y1));
721                 hitPoint.set(hitX, hitY);
722                 intersecting = true;
723             }
724            }
725            return intersecting;
726        }
727
728        // Based on http://www.garagegames.com/community/resources/view/309
729        public boolean calculateIntersectionBox(float left, float right, float top, float bottom,
730                Vector2 hitPoint) {
731
732            final float x1 = mStartPoint.x;
733            final float x2 = mEndPoint.x;
734            final float y1 = mStartPoint.y;
735            final float y2 = mEndPoint.y;
736
737            float startIntersect;
738            float endIntersect;
739            float intersectTimeStart = 0.0f;
740            float intersectTimeEnd = 1.0f;
741
742            if (x1 < x2) {
743                if (x1 > right || x2 < left) {
744                    return false;
745                }
746                final float deltaX = x2 - x1;
747                startIntersect = (x1 < left) ? (left - x1) / deltaX : 0.0f;
748                endIntersect = (x2 > right) ? (right - x1) / deltaX : 1.0f;
749            } else {
750                if (x2 > right || x1 < left) {
751                    return false;
752                }
753                final float deltaX = x2 - x1;
754                startIntersect = (x1 > right) ? (right - x1) / deltaX : 0.0f;
755                endIntersect = (x2 < left) ? (left - x1) / deltaX : 1.0f;
756            }
757
758            if (startIntersect > intersectTimeStart) {
759                intersectTimeStart = startIntersect;
760            }
761            if (endIntersect < intersectTimeEnd) {
762                intersectTimeEnd = endIntersect;
763            }
764            if (intersectTimeEnd < intersectTimeStart) {
765                return false;
766            }
767
768            // y
769            if (y1 < y2) {
770                if (y1 > top || y2 < bottom) {
771                    return false;
772                }
773                final float deltaY = y2 - y1;
774                startIntersect = (y1 < bottom) ? (bottom - y1) / deltaY : 0.0f;
775                endIntersect = (y2 > top) ? (top - y1) / deltaY : 1.0f;
776            } else {
777                if (y2 > top || y1 < bottom) {
778                    return false;
779                }
780                final float deltaY = y2 - y1;
781                startIntersect = (y1 > top) ? (top - y1) / deltaY : 0.0f;
782                endIntersect = (y2 < bottom) ? (bottom - y1) / deltaY : 1.0f;
783            }
784
785            if (startIntersect > intersectTimeStart) {
786                intersectTimeStart = startIntersect;
787            }
788            if (endIntersect < intersectTimeEnd) {
789                intersectTimeEnd = endIntersect;
790            }
791            if (intersectTimeEnd < intersectTimeStart) {
792                return false;
793            }
794
795            hitPoint.set(mEndPoint);
796            hitPoint.subtract(mStartPoint);
797            hitPoint.multiply(intersectTimeStart);
798            hitPoint.add(mStartPoint);
799
800            return true;
801        }
802
803    }
804
805    /**
806     * A pool of line segments.
807     */
808    protected class LineSegmentPool extends TObjectPool<LineSegment> {
809        public LineSegmentPool() {
810            super();
811        }
812
813        public LineSegmentPool(int count) {
814            super(count);
815        }
816
817        @Override
818        public void reset() {
819
820        }
821
822        @Override
823        protected void fill() {
824            for (int x = 0; x < getSize(); x++) {
825                getAvailable().add(new LineSegment());
826            }
827        }
828
829        @Override
830        public void release(Object entry) {
831            ((LineSegment)entry).owner = null;
832            super.release(entry);
833        }
834
835
836    }
837
838    /**
839     * A single collision tile.  Manages a list of line segments.
840     */
841    protected class CollisionTile extends AllocationGuard {
842        public FixedSizeArray<LineSegment> segments;
843
844        public CollisionTile(int maxSegments) {
845            super();
846            segments = new FixedSizeArray<LineSegment>(maxSegments);
847        }
848
849        public boolean addSegment(LineSegment segment) {
850            boolean success = false;
851            if (segments.getCount() < segments.getCapacity()) {
852                success = true;
853            }
854            segments.add(segment);
855            return success;
856        }
857    }
858}
859