/* * Copyright (C) 2016 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.apps.santatracker.doodles.tilt; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.util.Log; import com.google.android.apps.santatracker.doodles.shared.Actor; import com.google.android.apps.santatracker.doodles.shared.AnimatedSprite; import com.google.android.apps.santatracker.doodles.shared.SpriteActor; import com.google.android.apps.santatracker.doodles.shared.Sprites; import com.google.android.apps.santatracker.doodles.shared.Vector2D; import com.google.android.apps.santatracker.doodles.shared.physics.Polygon; import com.google.android.apps.santatracker.doodles.shared.physics.Util; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; /** * A sprite actor which contains a pre-set convex collision body. */ public class BoundingBoxSpriteActor extends CollisionActor implements Touchable { private static final String TAG = BoundingBoxSpriteActor.class.getSimpleName(); public static final String DUCK = "duck"; public static final String ICE_CUBE = "cube1"; public static final String HAND_GRAB = "hand grab"; private static final Random RANDOM = new Random(); protected static final float SCALE = 2; /** * A utility class which contains data needed to create BoundingBoxSpriteActors. */ protected static class Data { public int[] resIds; public int numFrames; public int zIndex; public Vector2D spriteOffset; public Vector2D[] vertexOffsets; public Data(int[] resIds, int zIndex, Vector2D spriteOffset, Vector2D[] vertexOffsets) { this.resIds = resIds; this.numFrames = resIds == null ? 0 : resIds.length; this.zIndex = zIndex; this.spriteOffset = spriteOffset; this.vertexOffsets = vertexOffsets; } } public static final Map<String, Data> TYPE_TO_RESOURCE_MAP; static { TYPE_TO_RESOURCE_MAP = new HashMap<>(); Vector2D[] duckVertexOffsets = { Vector2D.get(0, 0), Vector2D.get(87, 0), Vector2D.get(87, 186), Vector2D.get(0, 186), }; TYPE_TO_RESOURCE_MAP.put(DUCK, new Data(Sprites.penguin_swim_elf, 1, Vector2D.get(0, 0).scale(SCALE), duckVertexOffsets)); Vector2D[] iceCube1VertexOffsets = { Vector2D.get(0, 0), Vector2D.get(101.9f, 0), Vector2D.get(101.9f, 100.2f), Vector2D.get(0, 100.2f) }; TYPE_TO_RESOURCE_MAP.put(ICE_CUBE, new Data(Sprites.penguin_swim_ice, 1, Vector2D.get(0, 0).scale(SCALE), iceCube1VertexOffsets)); // This is just a placeholder so that we can create hand grabs programatically. This data // shouldn't actually be used. TYPE_TO_RESOURCE_MAP.put(HAND_GRAB, new Data(null, 0, Vector2D.get(0, 0).scale(SCALE), null)); } public String type; public final SpriteActor spriteActor; public Vector2D spriteOffset; public BoundingBoxSpriteActor( Polygon collisionBody, SpriteActor spriteActor, Vector2D spriteOffset, String type) { super(collisionBody); this.spriteOffset = spriteOffset; this.type = type; this.spriteActor = spriteActor; scale = SCALE; } @Override public void update(float deltaMs) { super.update(deltaMs); spriteActor.update(deltaMs); spriteActor.position.set(position.x, position.y); } @Override public void draw(Canvas canvas) { spriteActor.draw(canvas, spriteOffset.x, spriteOffset.y, spriteActor.sprite.frameWidth * scale, spriteActor.sprite.frameHeight * scale); collisionBody.draw(canvas); } @Override public boolean canHandleTouchAt(Vector2D worldCoords, float cameraScale) { Vector2D lowerRight = Vector2D.get(position).add(spriteOffset) .add(spriteActor.sprite.frameWidth * scale, spriteActor.sprite.frameHeight * scale); boolean retVal = super.canHandleTouchAt(worldCoords, cameraScale) || Util.pointIsWithinBounds(Vector2D.get(position).add(spriteOffset), lowerRight, worldCoords); lowerRight.release(); return retVal; } @Override public void startTouchAt(Vector2D worldCoords, float cameraScale) { selectedIndex = collisionBody.getSelectedIndex(worldCoords, cameraScale); } @Override public boolean handleMoveEvent(Vector2D delta) { collisionBody.move(-delta.x, -delta.y); position.set(collisionBody.min); // NOTE: Leave this commented-out section here. This is used when adding new // BoundingBoxSpriteActors in order to fine-tune the collision boundaries and sprite offsets. /* boolean moved; if (selectedIndex >= 0) { collisionBody.moveVertex(selectedIndex, Vector2D.get(delta).scale(-1)); Log.d(TAG, "min: " + collisionBody.min); Log.d(TAG, "max: " + collisionBody.max); } else { spriteOffset.subtract(delta); Log.d(TAG, "Sprite offset: " + spriteOffset); } */ return true; } @Override public boolean handleLongPress() { // NOTE: Leave this commented-out section here. This is used when adding new // BoundingBoxSpriteActors in order to fine-tune the collision boundaries and sprite offsets. /* if (selectedIndex >= 0) { if (canRemoveCollisionVertex()) { // If we can, just remove the vertex. collisionBody.removeVertexAt(selectedIndex); return true; } } else if (midpointIndex >= 0) { // Long press on a midpoint, add a vertex to the selected obstacle's polygon. collisionBody.addVertexAfter(midpointIndex); return true; } */ return false; } @Override public boolean resolveCollision(Actor other, float deltaMs) { if (other instanceof SwimmerActor) { return resolveCollisionInternal((SwimmerActor) other); } return false; } @Override public String getType() { return type; } @Override public JSONObject toJSON() throws JSONException { JSONObject json = new JSONObject(); json.put(TYPE_KEY, getType()); json.put(X_KEY, position.x); json.put(Y_KEY, position.y); return json; } protected boolean resolveCollisionInternal(SwimmerActor swimmer) { if (swimmer.isInvincible || swimmer.isUnderwater) { return false; } if (swimmer.collisionBody.min.y > collisionBody.max.y || swimmer.collisionBody.max.y < collisionBody.min.y) { // Perform a short-circuiting check which fails if the swimmer is outside of the vertical // boundaries of this collision body. return false; } // NOTE: We've since removed the diagonal ice cube, so we don't have any // non-axis-aligned rectangles to check collisions with. However, there may still be a few // artifacts of complex polygon collisions in the code. // CAN and ICE_CUBE objects are just axis-aligned rectangles. Use the faster // rectangle-to-rectangle collision code in these cases. if (Util.rectIntersectsRect(swimmer.collisionBody.min.x, swimmer.collisionBody.min.y, swimmer.collisionBody.getWidth(), swimmer.collisionBody.getHeight(), collisionBody.min.x, collisionBody.min.y, collisionBody.getWidth(), collisionBody.getHeight())) { // If the swimmer is colliding with the side of an obstacle, make the swimmer slide along it // instead of colliding. if (swimmer.positionBeforeFrame.y < collisionBody.max.y) { swimmer.moveTo(swimmer.positionBeforeFrame.x, swimmer.position.y); } else { swimmer.collide(type); } } return false; } public static BoundingBoxSpriteActor create(Vector2D position, String type, Resources resources) { if (!TYPE_TO_RESOURCE_MAP.containsKey(type)) { Log.e(TAG, "Unknown object type: " + type); return null; } Data data = TYPE_TO_RESOURCE_MAP.get(type); BoundingBoxSpriteActor actor; if (type.equals(HAND_GRAB)) { actor = HandGrabActor.create(position, resources); } else { actor = new BoundingBoxSpriteActor( getBoundingBox(position, data.vertexOffsets, SCALE), new SpriteActor(AnimatedSprite.fromFrames(resources, data.resIds), Vector2D.get(position), Vector2D.get(0, 0)), Vector2D.get(data.spriteOffset), type); } actor.zIndex = data.zIndex; // Start at a random frame index so that all of the sprites aren't synced up. actor.spriteActor.sprite.setFrameIndex(RANDOM.nextInt(actor.spriteActor.sprite.getNumFrames())); return actor; } public static BoundingBoxSpriteActor fromJSON(JSONObject json, Context context) throws JSONException { String type = json.getString(Actor.TYPE_KEY); Vector2D position = Vector2D.get((float) json.getDouble(X_KEY), (float) json.getDouble(Y_KEY)); return create(position, type, context.getResources()); } protected static Polygon getBoundingBox( Vector2D position, Vector2D[] vertexOffsets, float scale) { List<Vector2D> vertices = new ArrayList<>(); for (int i = 0; i < vertexOffsets.length; i++) { vertices.add(Vector2D.get(position).add(Vector2D.get(vertexOffsets[i]).scale(scale))); } return new Polygon(vertices); } }