// -*- mode: java; c-basic-offset: 2; -*- // Copyright 2009-2011 Google, All Rights reserved // Copyright 2011-2012 MIT, All rights reserved // Released under the Apache License, Version 2.0 // http://www.apache.org/licenses/LICENSE-2.0 package com.google.appinventor.components.runtime; import com.google.appinventor.components.annotations.DesignerProperty; import com.google.appinventor.components.annotations.PropertyCategory; import com.google.appinventor.components.annotations.SimpleEvent; import com.google.appinventor.components.annotations.SimpleFunction; import com.google.appinventor.components.annotations.SimpleObject; import com.google.appinventor.components.annotations.SimpleProperty; import com.google.appinventor.components.common.PropertyTypeConstants; import com.google.appinventor.components.runtime.errors.AssertionFailure; import com.google.appinventor.components.runtime.errors.IllegalArgumentError; import com.google.appinventor.components.runtime.util.BoundingBox; import com.google.appinventor.components.runtime.util.TimerInternal; import android.os.Handler; import android.util.Log; import java.util.HashSet; import java.util.Set; /** * Superclass of sprites able to move and interact with other sprites. * * While the Simple programmer sees the x- and y-coordinates as integers, * they are maintained internally as doubles so fractional changes (caused * by multiplying the speed by a cosine or sine value) have the chance to * add up. * * @author spertus.google.com (Ellen Spertus) */ @SimpleObject public abstract class Sprite extends VisibleComponent implements AlarmHandler, OnDestroyListener, Deleteable { private static final String LOG_TAG = "Sprite"; private static final boolean DEFAULT_ENABLED = true; // Enable timer for movement private static final int DEFAULT_HEADING = 0; // degrees private static final int DEFAULT_INTERVAL = 100; // ms private static final float DEFAULT_SPEED = 0.0f; // pixels per interval private static final boolean DEFAULT_VISIBLE = true; private static final double DEFAULT_Z = 1.0; protected final Canvas canvas; // enclosing Canvas private final TimerInternal timerInternal; // timer to control movement private final Handler androidUIHandler; // for posting actions // Keeps track of which other sprites are currently colliding with this one. // That way, we don't raise CollidedWith() more than once for each collision. // Events are only raised when sprites are added to this collision set. They // are removed when they no longer collide. private final Set<Sprite> registeredCollisions; // This variable prevents events from being raised before construction of // all components has taken place. This was added to fix bug 2262218. protected boolean initialized = false; // Properties: These are protected, instead of private, both so they // can be used by subclasses and tests. protected int interval; // number of milliseconds until next move protected boolean visible = true; // TODO(user): Convert to have co-ordinates be center, not upper left. // Note that this would simplify pointTowards to remove the adjustment // to the center points protected double xLeft; // leftmost x-coordinate protected double yTop; // uppermost y-coordinate protected double zLayer; // z-coordinate, higher values go in front protected float speed; // magnitude in pixels protected Form form; /** * The angle, in degrees above the positive x-axis, specified by the user. * This is private in order to enforce that changing it also changes * {@link #heading}, {@link #headingRadians}, {@link #headingCos}, and * {@link #headingSin}. */ protected double userHeading; /** * The angle, in degrees <em>below</em> the positive x-axis, specified by the * user. We use this to compute new coordinates because, on Android, the * y-coordinate increases "below" the x-axis. */ protected double heading; protected double headingRadians; // heading in radians protected double headingCos; // cosine(heading) protected double headingSin; // sine(heading) /** * Creates a new Sprite component. This version exists to allow injection * of a mock handler for testing. * * @param container where the component will be placed * @param handler a scheduler to which runnable events will be posted */ protected Sprite(ComponentContainer container, Handler handler) { super(); androidUIHandler = handler; // Add to containing Canvas. if (!(container instanceof Canvas)) { throw new IllegalArgumentError("Sprite constructor called with container " + container); } this.canvas = (Canvas) container; this.canvas.addSprite(this); // Maintain a list of collisions. registeredCollisions = new HashSet<Sprite>(); // Set in motion. timerInternal = new TimerInternal(this, DEFAULT_ENABLED, DEFAULT_INTERVAL, handler); this.form = container.$form(); // Set default property values. Heading(0); // Default initial heading Enabled(DEFAULT_ENABLED); Interval(DEFAULT_INTERVAL); Speed(DEFAULT_SPEED); Visible(DEFAULT_VISIBLE); Z(DEFAULT_Z); container.$form().registerForOnDestroy(this); } /** * Creates a new Sprite component. This is called by the constructors of * concrete subclasses, such as {@link Ball} and {@link ImageSprite}. * * @param container where the component will be placed */ protected Sprite(ComponentContainer container) { // Note that although this is creating a new Handler, there is // only one UI thread in an Android app and posting to this // handler queues up a Runnable for execution on that thread. this(container, new Handler()); } public void Initialize() { initialized = true; canvas.registerChange(this); } // Properties (Enabled, Heading, Interval, Speed, Visible, X, Y, Z) /** * Enabled property getter method. * * @return {@code true} indicates a running timer, {@code false} a stopped * timer */ @SimpleProperty( description = "Controls whether the sprite moves when its speed is non-zero.", category = PropertyCategory.BEHAVIOR) public boolean Enabled() { return timerInternal.Enabled(); } /** * Enabled property setter method: starts or stops the timer. * * @param enabled {@code true} starts the timer, {@code false} stops it */ @DesignerProperty( editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, defaultValue = DEFAULT_ENABLED ? "True" : "False") @SimpleProperty public void Enabled(boolean enabled) { timerInternal.Enabled(enabled); } /** * Sets heading in which sprite should move. In addition to changing the * local variables {@link #userHeading} and {@link #heading}, this * sets {@link #headingCos}, {@link #headingSin}, and {@link #headingRadians}. * * @param userHeading degrees above the positive x-axis */ @SimpleProperty( category = PropertyCategory.BEHAVIOR) @DesignerProperty( editorType = PropertyTypeConstants.PROPERTY_TYPE_FLOAT, defaultValue = DEFAULT_HEADING + "") public void Heading(double userHeading) { this.userHeading = userHeading; // Flip, because y increases in the downward direction on Android canvases heading = -userHeading; headingRadians = Math.toRadians(heading); headingCos = Math.cos(headingRadians); headingSin = Math.sin(headingRadians); // changing the heading needs to force a redraw for image sprites that rotate registerChange(); } /** * Returns the heading of the sprite. * * @return degrees above the positive x-axis */ @SimpleProperty( description = "Returns the sprite's heading in degrees above the positive " + "x-axis. Zero degrees is toward the right of the screen; 90 degrees is toward the " + "top of the screen.") public double Heading() { return userHeading; } /** * Interval property getter method. * * @return timer interval in ms */ @SimpleProperty( description = "The interval in milliseconds at which the sprite's " + "position is updated. For example, if the interval is 50 and the speed is 10, " + "then the sprite will move 10 pixels every 50 milliseconds.", category = PropertyCategory.BEHAVIOR) public int Interval() { return timerInternal.Interval(); } /** * Interval property setter method: sets the interval between timer events. * * @param interval timer interval in ms */ @DesignerProperty( editorType = PropertyTypeConstants.PROPERTY_TYPE_NON_NEGATIVE_INTEGER, defaultValue = DEFAULT_INTERVAL + "") @SimpleProperty public void Interval(int interval) { timerInternal.Interval(interval); } /** * Sets the speed with which this sprite should move. * * @param speed the magnitude (in pixels) to move every {@link #interval} * milliseconds */ @SimpleProperty( category = PropertyCategory.BEHAVIOR) @DesignerProperty( editorType = PropertyTypeConstants.PROPERTY_TYPE_FLOAT, defaultValue = DEFAULT_SPEED + "") public void Speed(float speed) { this.speed = speed; } /** * Gets the speed with which this sprite moves. * * @return the magnitude (in pixels) the sprite moves every {@link #interval} * milliseconds. */ @SimpleProperty( description = "he speed at which the sprite moves. The sprite moves " + "this many pixels every interval.") public float Speed() { return speed; } /** * Gets whether sprite is visible. * * @return {@code true} if the sprite is visible, {@code false} otherwise */ @SimpleProperty( description = "True if the sprite is visible.", category = PropertyCategory.APPEARANCE) public boolean Visible() { return visible; } /** * Sets whether sprite should be visible. * * @param visible {@code true} if the sprite should be visible; {@code false} * otherwise. */ @DesignerProperty( editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, defaultValue = DEFAULT_VISIBLE ? "True" : "False") @SimpleProperty public void Visible(boolean visible) { this.visible = visible; registerChange(); } @SimpleProperty( description = "The horizontal coordinate of the left edge of the sprite, " + "increasing as the sprite moves to the right.") public double X() { return xLeft; } @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_FLOAT, defaultValue = "0.0") @SimpleProperty( category = PropertyCategory.APPEARANCE) public void X(double x) { xLeft = x; registerChange(); } @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_FLOAT, defaultValue = "0.0") @SimpleProperty( category = PropertyCategory.APPEARANCE) public void Y(double y) { yTop = y; registerChange(); } @SimpleProperty( description = "The vertical coordinate of the top of the sprite, " + "increasing as the sprite moves down.") public double Y() { return yTop; } /** * Sets the layer of the sprite, indicating whether it will appear in * front of or behind other sprites. * * @param layer higher numbers indicate that this sprite should appear * in front of ones with lower numbers; if values are equal for * sprites, either can go in front of the other */ @SimpleProperty( category = PropertyCategory.APPEARANCE) @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_FLOAT, defaultValue = DEFAULT_Z + "") public void Z(double layer) { this.zLayer = layer; canvas.changeSpriteLayer(this); // Tell canvas about change } @SimpleProperty( description = "How the sprite should be layered relative to other sprits, " + "with higher-numbered layers in front of lower-numbered layers.") public double Z() { return zLayer; } // Methods for event handling: general purpose method postEvent() and // Simple events: CollidedWith, Dragged, EdgeReached, Touched, NoLongeCollidingWith, // Flung, TouchUp, and TouchDown. /** * Posts a dispatch for the specified event. This guarantees that event * handlers run with serial semantics, e.g., appear atomic relative to * each other. * * This method is overridden in tests. * * @param sprite the instance on which the event takes place * @param eventName the name of the event * @param args the arguments to the event handler */ protected void postEvent(final Sprite sprite, final String eventName, final Object... args) { androidUIHandler.post(new Runnable() { public void run() { EventDispatcher.dispatchEvent(sprite, eventName, args); }}); } // TODO(halabelson): Fix collision detection for rotated sprites. /** * Handler for CollidedWith events, called when two sprites collide. * Note that checking for collisions with a rotated ImageSprite currently * checks against the sprite's unrotated position. Therefore, collision * checking will be inaccurate for tall narrow or short wide sprites that are * rotated. * * @param other the other sprite in the collision */ @SimpleEvent public void CollidedWith(Sprite other) { if (registeredCollisions.contains(other)) { Log.e(LOG_TAG, "Collision between sprites " + this + " and " + other + " re-registered"); return; } registeredCollisions.add(other); postEvent(this, "CollidedWith", other); } /** * Handler for Dragged events. On all calls, the starting coordinates * are where the screen was first touched, and the "current" coordinates * describe the endpoint of the current line segment. On the first call * within a given drag, the "previous" coordinates are the same as the * starting coordinates; subsequently, they are the "current" coordinates * from the prior call. Note that the Sprite won't actually move * anywhere in response to the Dragged event unless MoveTo is * specifically called. * * @param startX the starting x-coordinate * @param startY the starting y-coordinate * @param prevX the previous x-coordinate (possibly equal to startX) * @param prevY the previous y-coordinate (possibly equal to startY) * @param currentX the current x-coordinate * @param currentY the current y-coordinate */ @SimpleEvent public void Dragged(float startX, float startY, float prevX, float prevY, float currentX, float currentY) { postEvent(this, "Dragged", startX, startY, prevX, prevY, currentX, currentY); } /** * Event handler called when the sprite reaches an edge of the screen. * If Bounce is then called with that edge, the sprite will appear to * bounce off of the edge it reached. */ @SimpleEvent( description = "Event handler called when the sprite reaches an edge of the screen. " + "If Bounce is then called with that edge, the sprite will appear to " + "bounce off of the edge it reached. Edge here is represented as an integer that " + "indicates one of eight directions north(1), northeast(2), east(3), southeast(4), " + "south (-1), southwest(-2), west(-3), and northwest(-4).") public void EdgeReached(int edge) { if (edge == Component.DIRECTION_NONE || edge < Component.DIRECTION_MIN || edge > Component.DIRECTION_MAX) { throw new IllegalArgumentException("Illegal argument " + edge + " to Sprite.EdgeReached()"); } postEvent(this, "EdgeReached", edge); } /** * Handler for NoLongerCollidingWith events, called when a pair of sprites * cease colliding. This also registers the removal of the collision to a * private variable {@link #registeredCollisions} so that * {@link #CollidedWith(Sprite)} and this event are only raised once per * beginning and ending of a collision. * * @param other the sprite formerly colliding with this sprite */ @SimpleEvent( description = "Event indicating that a pair of sprites are no longer " + "colliding.") public void NoLongerCollidingWith(Sprite other) { if (!registeredCollisions.contains(other)) { Log.e(LOG_TAG, "Collision between sprites " + this + " and " + other + " removed but not present"); } registeredCollisions.remove(other); postEvent(this, "NoLongerCollidingWith", other); } /** * When the user touches the sprite and then immediately lifts finger: provides * the (x,y) position of the touch, relative to the upper left of the canvas * * @param x x-coordinate of touched point * @param y y-coordinate of touched point */ @SimpleEvent public void Touched(float x, float y) { postEvent(this, "Touched", x, y); } /** * When a fling gesture (quick swipe) is made on the sprite: provides * the (x,y) position of the start of the fling, relative to the upper * left of the canvas. Also provides the speed (pixels per millisecond) and heading * (0-360 degrees) of the fling, as well as the x velocity and y velocity * components of the fling's vector. * * @param x x-coordinate of touched point * @param y y-coordinate of touched point * * @param speed the speed of the fling sqrt(xspeed^2 + yspeed^2) * @param heading the heading of the fling * @param xvel the speed in x-direction of the fling * @param yvel the speed in y-direction of the fling */ @SimpleEvent public void Flung(float x, float y, float speed, float heading, float xvel, float yvel) { postEvent(this, "Flung", x, y, speed, heading, xvel, yvel); } /** * When the user stops touching the sprite (lifts finger after a * TouchDown event): provides the (x,y) position of the touch, relative * to the upper left of the canvas * * @param x x-coordinate of touched point * @param y y-coordinate of touched point */ @SimpleEvent public void TouchUp(float x, float y) { postEvent(this, "TouchUp", x, y); } /** * When the user begins touching the sprite (places finger on sprite and * leaves it there): provides the (x,y) position of the touch, relative * to the upper left of the canvas * * @param x x-coordinate of touched point * @param y y-coordinate of touched point */ @SimpleEvent public void TouchDown(float x, float y) { postEvent(this, "TouchDown", x, y); } // Methods providing Simple functions: // Bounce, CollidingWith, MoveIntoBounds, MoveTo, PointTowards. /** * Makes this sprite bounce, as if off of a wall by changing the * {@link #heading} (unless the sprite is not traveling toward the specified * direction). This also calls {@link #MoveIntoBounds()} in case the * sprite is out of bounds. * * @param edge the direction of the object (real or imaginary) to bounce off * of; this should be one of * {@link com.google.appinventor.components.runtime.Component#DIRECTION_NORTH}, * {@link com.google.appinventor.components.runtime.Component#DIRECTION_NORTHEAST}, * {@link com.google.appinventor.components.runtime.Component#DIRECTION_EAST}, * {@link com.google.appinventor.components.runtime.Component#DIRECTION_SOUTHEAST}, * {@link com.google.appinventor.components.runtime.Component#DIRECTION_SOUTH}, * {@link com.google.appinventor.components.runtime.Component#DIRECTION_SOUTHWEST}, * {@link com.google.appinventor.components.runtime.Component#DIRECTION_WEST}, or * {@link com.google.appinventor.components.runtime.Component#DIRECTION_NORTHWEST}. */ @SimpleFunction(description = "Makes this sprite bounce, as if off a wall. " + "For normal bouncing, the edge argument should be the one returned by EdgeReached.") public void Bounce (int edge) { MoveIntoBounds(); // Normalize heading to [0, 360) double normalizedAngle = userHeading % 360; // The following step is necessary because Java's modulus operation yields a // negative number if the dividend is negative and the divisor is positive. if (normalizedAngle < 0) { normalizedAngle += 360; } // Only transform heading if sprite was moving in that direction. // This avoids oscillations. if ((edge == Component.DIRECTION_EAST && (normalizedAngle < 90 || normalizedAngle > 270)) || (edge == Component.DIRECTION_WEST && (normalizedAngle > 90 && normalizedAngle < 270))) { Heading(180 - normalizedAngle); } else if ((edge == Component.DIRECTION_NORTH && normalizedAngle > 0 && normalizedAngle < 180) || (edge == Component.DIRECTION_SOUTH && normalizedAngle > 180)) { Heading(360 - normalizedAngle); } else if ((edge == Component.DIRECTION_NORTHEAST && normalizedAngle > 0 && normalizedAngle < 90) || (edge == Component.DIRECTION_NORTHWEST && normalizedAngle > 90 && normalizedAngle < 180) || (edge == Component.DIRECTION_SOUTHWEST && normalizedAngle > 180 && normalizedAngle < 270) || (edge == Component.DIRECTION_SOUTHEAST && normalizedAngle > 270)) { Heading(180 + normalizedAngle); } } // This is primarily used to enforce raising only // one {@link #CollidedWith(Sprite)} event per collision but is also // made available to the Simple programmer. /** * Indicates whether a collision has been registered between this sprite * and the passed sprite. * * @param other the sprite to check for collision with this sprite * @return {@code true} if a collision event has been raised for the pair of * sprites and they still are in collision, {@code false} otherwise. */ @SimpleFunction public boolean CollidingWith(Sprite other) { return registeredCollisions.contains(other); } /** * Moves the sprite back in bounds if part of it extends out of bounds, * having no effect otherwise. If the sprite is too wide to fit on the * canvas, this aligns the left side of the sprite with the left side of the * canvas. If the sprite is too tall to fit on the canvas, this aligns the * top side of the sprite with the top side of the canvas. */ @SimpleFunction public void MoveIntoBounds() { moveIntoBounds(canvas.Width(), canvas.Height()); } /** * Moves sprite directly to specified point. * * @param x the x-coordinate * @param y the y-coordinate */ @SimpleFunction( description = "Moves the sprite so that its left top corner is at " + "the specfied x and y coordinates.") public void MoveTo(double x, double y) { xLeft = x; yTop = y; registerChange(); } /** * Turns this sprite to point towards a given other sprite. * * @param target the other sprite to point towards */ @SimpleFunction( description = "Turns the sprite to point towards a designated " + "target sprite. The new heading will be parallel to the line joining " + "the centerpoints of the two sprites.") public void PointTowards(Sprite target) { Heading(-Math.toDegrees(Math.atan2( // we adjust for the fact that the sprites' X() and Y() // are not the center points. target.Y() - Y() + (target.Height() - Height()) / 2, target.X() - X() + (target.Width() - Width()) / 2))); } /** * Turns this sprite to point towards a given point. * * @param x parameter of the point to turn to * @param y parameter of the point to turn to */ @SimpleFunction( description = "Turns the sprite to point towards the point " + "with coordinates as (x, y).") public void PointInDirection(double x, double y) { Heading(-Math.toDegrees(Math.atan2( // we adjust for the fact that the sprite's X() and Y() // is not the center point. y - Y() - Height() / 2, x - X() - Width() / 2))); } // Internal methods supporting move-related functionality /** * Responds to a move or change of this sprite by redrawing the * enclosing Canvas and checking for any consequences that need * handling. Specifically, this (1) notifies the Canvas of a change * so it can detect any collisions, etc., and (2) raises the * {@link #EdgeReached(int)} event if the Sprite has reached the edge of the * Canvas. */ protected void registerChange() { // This was added to fix bug 2262218, where Ball.CollidedWith() was called // before all components had been constructed. if (!initialized) { // During REPL, components are not initalized, but we still want to repaint the canvas. canvas.getView().invalidate(); return; } int edge = hitEdge(); if (edge != Component.DIRECTION_NONE) { EdgeReached(edge); } canvas.registerChange(this); } /** * Specifies which edge of the canvas has been hit by the Sprite, if * any, moving the sprite back in bounds. * * @return {@link Component#DIRECTION_NONE} if no edge has been hit, or a * direction (e.g., {@link Component#DIRECTION_NORTHEAST}) if that * edge of the canvas has been hit */ protected int hitEdge() { if (!canvas.ready()) { return Component.DIRECTION_NONE; } return hitEdge(canvas.Width(), canvas.Height()); } /** * Moves the sprite back in bounds if part of it extends out of bounds, * having no effect otherwise. If the sprite is too wide to fit on the * canvas, this aligns the left side of the sprite with the left side of the * canvas. If the sprite is too tall to fit on the canvas, this aligns the * top side of the sprite with the top side of the canvas. */ @SimpleFunction protected final void moveIntoBounds(int canvasWidth, int canvasHeight) { boolean moved = false; // We set the xLeft and/or yTop fields directly, instead of calling X(123) and Y(123), to avoid // having multiple calls to registerChange. // Check if the sprite is too wide to fit on the canvas. if (Width() > canvasWidth) { // Sprite is too wide to fit. If it isn't already at the left edge, move it there. // It is important not to set moved to true if xLeft is already 0. Doing so can cause a stack // overflow. if (xLeft != 0) { xLeft = 0; moved = true; } } else if (overWestEdge()) { xLeft = 0; moved = true; } else if (overEastEdge(canvasWidth)) { xLeft = canvasWidth - Width(); moved = true; } // Check if the sprite is too tall to fit on the canvas. We don't want to cause a stack // overflow by moving the sprite to the top edge and then to the bottom edge, repeatedly. if (Height() > canvasHeight) { // Sprite is too tall to fit. If it isn't already at the top edge, move it there. // It is important not to set moved to true if yTop is already 0. Doing so can cause a stack // overflow. if (yTop != 0) { yTop = 0; moved = true; } } else if (overNorthEdge()) { yTop = 0; moved = true; } else if (overSouthEdge(canvasHeight)) { yTop = canvasHeight - Height(); moved = true; } // Then, call registerChange (just once!) if necessary. if (moved) { registerChange(); } } /** * Updates the x- and y-coordinates based on the heading and speed. The * caller is responsible for calling {@link #registerChange()}. */ protected void updateCoordinates() { xLeft += speed * headingCos; yTop += speed * headingSin; } // Methods for determining collisions with other Sprites and the edge // of the Canvas. private final boolean overWestEdge() { return xLeft < 0; } private final boolean overEastEdge(int canvasWidth) { return xLeft + Width() > canvasWidth; } private final boolean overNorthEdge() { return yTop < 0; } private final boolean overSouthEdge(int canvasHeight) { return yTop + Height() > canvasHeight; } protected int hitEdge(int canvasWidth, int canvasHeight) { // Determine in which direction(s) we are out of bounds, if any. // Note that more than one boolean value can be true. For example, if // the sprite is past the northwest boundary, north and west will be true. boolean west = overWestEdge(); boolean north = overNorthEdge(); boolean east = overEastEdge(canvasWidth); boolean south = overSouthEdge(canvasHeight); // If no edge was hit, return. if (!(north || south || east || west)) { return Component.DIRECTION_NONE; } // Move the sprite back into bounds. Note that we don't just reverse the // last move, since that might have been multiple pixels, and we'd only need // to undo part of it. MoveIntoBounds(); // Determine the appropriate return value. if (west) { if (north) { return Component.DIRECTION_NORTHWEST; } else if (south) { return Component.DIRECTION_SOUTHWEST; } else { return Component.DIRECTION_WEST; } } if (east) { if (north) { return Component.DIRECTION_NORTHEAST; } else if (south) { return Component.DIRECTION_SOUTHEAST; } else { return Component.DIRECTION_EAST; } } if (north) { return Component.DIRECTION_NORTH; } if (south) { return Component.DIRECTION_SOUTH; } throw new AssertionFailure("Unreachable code hit in Sprite.hitEdge()"); } /** * Provides the bounding box for this sprite. Modifying the returned value * does not affect the sprite. * * @param border the number of pixels outside the sprite to include in the * bounding box * @return the bounding box for this sprite */ public BoundingBox getBoundingBox(int border) { return new BoundingBox(X() - border, Y() - border, X() + Width() - 1 + border, Y() + Height() - 1 + border); } /** * Determines whether two sprites are in collision. Note that we cannot * merely see whether the rectangular regions around each intersect, since * some types of sprite, such as BallSprite, are not rectangular. * * @param sprite1 one sprite * @param sprite2 another sprite * @return {@code true} if they are in collision, {@code false} otherwise */ public static boolean colliding(Sprite sprite1, Sprite sprite2) { // If the bounding boxes don't intersect, there can be no collision. BoundingBox rect1 = sprite1.getBoundingBox(1); BoundingBox rect2 = sprite2.getBoundingBox(1); if (!rect1.intersectDestructively(rect2)) { return false; } // If we get here, rect1 has been mutated to hold the intersection of the // two bounding boxes. Now check every point in the intersection to see if // both sprites contain that point. // TODO(user): Handling abutting sprites properly for (double x = rect1.getLeft(); x <= rect1.getRight(); x++) { for (double y = rect1.getTop(); y <= rect1.getBottom(); y++) { if (sprite1.containsPoint(x, y) && sprite2.containsPoint(x, y)) { return true; } } } return false; } /** * Determines whether this sprite intersects with the given rectangle. * * @param rect the rectangle * @return {@code true} if they intersect, {@code false} otherwise */ public boolean intersectsWith(BoundingBox rect) { // If the bounding boxes don't intersect, there can be no intersection. BoundingBox rect1 = getBoundingBox(0); if (!rect1.intersectDestructively(rect)) { return false; } // If we get here, rect1 has been mutated to hold the intersection of the // two bounding boxes. Now check every point in the intersection to see if // the sprite contains it. for (double x = rect1.getLeft(); x < rect1.getRight(); x++) { for (double y = rect1.getTop(); y < rect1.getBottom(); y++) { if (containsPoint(x, y)) { return true; } } } return false; } /** * Indicates whether the specified point is contained by this sprite. * Subclasses of Sprite that are not rectangular should override this method. * * @param qx the x-coordinate * @param qy the y-coordinate * @return whether (qx, qy) falls within this sprite */ public boolean containsPoint(double qx, double qy) { return qx >= xLeft && qx < xLeft + Width() && qy >= yTop && qy < yTop + Height(); } // Convenience methods for dealing with hitting the screen edge and collisions // AlarmHandler implementation /** * Moves and redraws sprite, registering changes. */ public void alarm() { // This check on initialized is currently redundant, since registerChange() // checks it too. if (initialized && speed != 0) { updateCoordinates(); registerChange(); } } // Component implementation @Override public HandlesEventDispatching getDispatchDelegate() { return canvas.$form(); } // OnDestroyListener implementation @Override public void onDestroy() { timerInternal.Enabled(false); } // Deleteable implementation @Override public void onDelete() { timerInternal.Enabled(false); canvas.removeSprite(this); } // Abstract methods that must be defined by subclasses /** * Draws the sprite on the given canvas * * @param canvas the canvas on which to draw */ protected abstract void onDraw(android.graphics.Canvas canvas); }