/* * 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.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapFactory.Options; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.Log; import com.google.android.apps.santatracker.doodles.Config; import com.google.android.apps.santatracker.doodles.R; import com.google.android.apps.santatracker.doodles.shared.Actor; 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 java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Queue; import java.util.Random; /** * One chunk of a level in the swimming game. */ public class SwimmingLevelChunk extends Actor { private static final String TAG = SwimmingLevelChunk.class.getSimpleName(); public static final int LEVEL_LENGTH_IN_METERS = 500; public static Queue<SwimmingLevelChunk> swimmingLevelChunks; public static List<SolutionPath> pathList; public static final int CHUNK_HEIGHT = 5000; public static final int NUM_ROWS = 100; public static final int NUM_COLS = 50; public static final float COL_WIDTH = SwimmingModel.LEVEL_WIDTH / (float) NUM_COLS; public static final float ROW_HEIGHT = CHUNK_HEIGHT / (float) NUM_ROWS; private static final int SOLUTION_PATH_NUM_COLS = 50; private static final Random RANDOM = new Random(); private static final List<String> TYPES; static { TYPES = new ArrayList<>(); TYPES.add(BoundingBoxSpriteActor.ICE_CUBE); TYPES.add(BoundingBoxSpriteActor.DUCK); TYPES.add(BoundingBoxSpriteActor.HAND_GRAB); } private final float DEFAULT_OBSTACLE_DENSITY; public final float startY; public final float endY; public List<BoundingBoxSpriteActor> obstacles; private SolutionPath solutionPath; private boolean mirrored; public static SwimmingLevelChunk create(float startY, Context context) { if (pathList == null || pathList.size() == 0) { loadChunkTemplates(context.getResources()); } // Increase the probability that the random chunk will be a "middle open" chunk. int pathIndex = Math.min(pathList.size() - 1, RANDOM.nextInt(pathList.size() + 1)); SolutionPath solutionPath = pathList.get(pathIndex); return new SwimmingLevelChunk(startY, solutionPath, RANDOM.nextBoolean(), context); } public static void generateAllLevelChunks(float startY, Context context) { long startTime = System.currentTimeMillis(); swimmingLevelChunks = new LinkedList<>(); SwimmingLevelChunk chunk = create(startY, context); while (SwimmingModel.getMetersFromWorldY(chunk.endY) < LEVEL_LENGTH_IN_METERS) { swimmingLevelChunks.add(chunk); chunk = create(chunk.endY, context); } Log.d(TAG, "generateAllLevelChunks: finished in " + ((System.currentTimeMillis() - startTime) / 1000.0f) + " seconds."); } public static SwimmingLevelChunk getNextChunk() { if (!swimmingLevelChunks.isEmpty()) { return swimmingLevelChunks.remove(); } return null; } private SwimmingLevelChunk(float startY, SolutionPath solutionPath, boolean mirrored, Context context) { // Get swimming obstacle density from config Config config = new Config(); DEFAULT_OBSTACLE_DENSITY = (float) config.SWIMMING_OBSTACLE_DENSITY; this.solutionPath = solutionPath; this.mirrored = mirrored; this.startY = startY; generateObstacles(context); removeObstaclesFromSolutionPath(startY); this.endY = startY - solutionPath.getChunkHeight(); } @Override public void update(float deltaMs) { for (int i = 0; i < obstacles.size(); i++) { obstacles.get(i).update(deltaMs); } } @Override public void draw(Canvas canvas) { //solutionPath.draw(canvas, startY, mirrored); for (int i = 0; i < obstacles.size(); i++) { obstacles.get(i).draw(canvas); } } public void resolveCollisions(SwimmerActor swimmer, float deltaMs) { for (int i = 0; i < obstacles.size(); i++) { obstacles.get(i).resolveCollision(swimmer, deltaMs); } } public static void loadChunkTemplates(Resources res) { pathList = new ArrayList<>(); Options decodeOptions = new Options(); decodeOptions.inScaled = false; Bitmap b = BitmapFactory.decodeResource(res, R.raw.diamond, decodeOptions); pathList.add(new GridSolutionPath(b)); b = BitmapFactory.decodeResource(res, R.raw.zig, decodeOptions); pathList.add(new GridSolutionPath(b)); b = BitmapFactory.decodeResource(res, R.raw.ziggeroo, decodeOptions); pathList.add(new GridSolutionPath(b)); b = BitmapFactory.decodeResource(res, R.raw.fork_in, decodeOptions); pathList.add(new GridSolutionPath(b)); b = BitmapFactory.decodeResource(res, R.raw.fork_out, decodeOptions); pathList.add(new GridSolutionPath(b)); b = BitmapFactory.decodeResource(res, R.raw.middle_open, decodeOptions); pathList.add(new GridSolutionPath(b)); } private void generateObstacles(Context context) { obstacles = new ArrayList<>(); for (int i = 0; i < solutionPath.getNumRows() * DEFAULT_OBSTACLE_DENSITY; i++) { float x = RANDOM.nextInt((4 * SwimmingModel.LEVEL_WIDTH) / 5); float y = startY - (SwimmingModel.LEVEL_WIDTH / 5) - RANDOM.nextInt((int) solutionPath.getChunkHeight() - (SwimmingModel.LEVEL_WIDTH / 5)); int metersY = SwimmingModel.getMetersFromWorldY(y); int type; if (metersY < SwimmingModel.SCORE_THRESHOLDS[0]) { // Only show ice cubes before the bronze threshold. type = 0; } else if (metersY < SwimmingModel.SCORE_THRESHOLDS[1]) { // Show ice cubes and cans before the silver threshold. type = RANDOM.nextInt(TYPES.size() - 1); } else { // After the silver threshold, use all obstacles. boolean isInMiddleThreeLanes = SwimmingModel.LEVEL_WIDTH / 5 <= x && x <= 3 * SwimmingModel.LEVEL_WIDTH / 5; if (isInMiddleThreeLanes) { // Only place octograbs in the middle three lanes. If we are generating an obstacle in the // middle 3 lanes, give it a higher chance of being an octograb. type = Math.min(TYPES.size() - 1, RANDOM.nextInt(TYPES.size() + 1)); } else { // If we are outside of the middle 3 lanes, give each other option equal weight. type = RANDOM.nextInt(TYPES.size() - 1); } } BoundingBoxSpriteActor obstacle = BoundingBoxSpriteActor.create( Vector2D.get(x, y), TYPES.get(type), context.getResources()); Polygon obstacleBody = obstacle.collisionBody; boolean shouldAdd = true; for (int j = 0; j < obstacles.size(); j++) { Polygon otherBody = obstacles.get(j).collisionBody; if (Util.rectIntersectsRect( otherBody.min.x, otherBody.min.y, otherBody.getWidth(), otherBody.getHeight(), obstacleBody.min.x, obstacleBody.min.y, obstacleBody.getWidth(), obstacleBody.getHeight())) { shouldAdd = false; } } if (shouldAdd) { obstacles.add(obstacle); } } } private void removeObstaclesFromSolutionPath(float startY) { for (int i = obstacles.size() - 1; i >= 0; i--) { BoundingBoxSpriteActor obstacle = obstacles.get(i); Vector2D min = obstacle.collisionBody.min; Vector2D max = obstacle.collisionBody.max; if (solutionPath.intersects(startY, min.x, min.y, max.x, max.y, mirrored)) { obstacles.remove(i); } } } private interface SolutionPath { boolean intersects(float startY, float minX, float minY, float maxX, float maxY, boolean mirrored); void draw(Canvas canvas, float startY, boolean mirrored); int getEndCol(boolean mirrored); float getChunkHeight(); int getNumRows(); } private static class SolutionPathImpl implements SolutionPath { private static final int DRIFT_SAME = 0; private static final int DRIFT_REVERSE = 1; private final int[] driftDistribution = new int[] { 50, 75, 100 }; private SolutionPathRow[] rows; private int endCol; private int drift; private Paint paint; public SolutionPathImpl(int startCol) { rows = new SolutionPathRow[NUM_ROWS]; paint = new Paint(); paint.setColor(Color.DKGRAY); for (int i = 0; i < rows.length; i++) { // Decide which way to drift. int driftToken = RANDOM.nextInt(100); if (driftToken < driftDistribution[DRIFT_SAME]) { if (drift == 0) { // If the path is going straight, switch it to a random direction. drift = RANDOM.nextBoolean() ? 1 : -1; } } else if (driftToken < driftDistribution[DRIFT_REVERSE]) { drift = 0; } else { drift *= -1; } if (startCol == 0) { drift = 1; } else if (startCol == NUM_COLS - SOLUTION_PATH_NUM_COLS - 1) { drift = -1; } startCol = Util.clamp(startCol + drift, 0, NUM_COLS - SOLUTION_PATH_NUM_COLS - 1); rows[i] = new SolutionPathRow(startCol, SOLUTION_PATH_NUM_COLS); } this.endCol = startCol; } @Override public boolean intersects(float startY, float minX, float minY, float maxX, float maxY, boolean mirrored) { // Subtract y from startY because the level proceeds in the negative y direction. int minRowIndex = (int) Math.max(0, (startY - maxY) / ROW_HEIGHT); int maxRowIndex = (int) Math.max(0, (startY - minY) / ROW_HEIGHT); for (int i = minRowIndex; i <= maxRowIndex; i++) { if (rows[i].intersects(Math.min(minX, SwimmingModel.LEVEL_WIDTH), mirrored) || rows[i].intersects(Math.min(maxX, SwimmingModel.LEVEL_WIDTH), mirrored)) { return true; } } return false; } @Override public void draw(Canvas canvas, float startY, boolean mirrored) { float y = startY; for (int i = 0; i < rows.length; i++) { float startX = rows[i].startX; float endX = rows[i].endX; if (mirrored) { startX = SwimmingModel.LEVEL_WIDTH - rows[i].endX; endX = SwimmingModel.LEVEL_WIDTH - rows[i].startX; } canvas.drawRect(startX, y, endX, y + ROW_HEIGHT, paint); y -= ROW_HEIGHT; } } @Override public int getEndCol(boolean mirrored) { return mirrored ? NUM_COLS - 1 - endCol : endCol; } @Override public float getChunkHeight() { return CHUNK_HEIGHT; } @Override public int getNumRows() { return rows.length; } } private static class GridSolutionPath implements SolutionPath { public final float chunkHeight; public final int numRows; public final float rowHeight; private boolean[][] grid; private Paint paint; /** * Initialize this solution path with a bitmap. The length of the solution path will scale with * the height of the image, where 1px in the image = 1 grid unit in the chunk. The width of the * solution path is fixed to NUM_COLS and will just sample the bitmap at NUM_COLS points. Any * bitmap with a higher horizontal resolution than NUM_COLS will be down-sampled, and any bitmap * with a lower resolution will have single pixels being sampled more than once horizontally. * * In order to maintain visual consistency with the supplied bitmap, it is recommended that * the input PNGs are 50px wide. */ public GridSolutionPath(Bitmap bitmap) { int bitmapWidth = bitmap.getWidth(); int bitmapHeight = bitmap.getHeight(); chunkHeight = bitmapHeight * COL_WIDTH; numRows = bitmapHeight; rowHeight = chunkHeight / numRows; Log.d(TAG, "bitmapHeight: " + bitmapHeight); Log.d(TAG, "chunkHeight: " + chunkHeight); Log.d(TAG, "numRows: " + numRows); Log.d(TAG, "rowHeight: " + rowHeight); paint = new Paint(); paint.setColor(Color.DKGRAY); grid = new boolean[numRows][NUM_COLS]; for (int i = 0; i < grid.length; i++) { for (int j = 0; j < grid[0].length; j++) { int bitmapX = (int) ((((float) j) / grid[0].length) * bitmapWidth); int bitmapY = (int) ((((float) i) / grid.length) * bitmapHeight); int pixel = bitmap.getPixel(bitmapX, bitmapY); grid[i][j] = (pixel & 0x00ffffff) != 0; } } } @Override public boolean intersects(float startY, float minX, float minY, float maxX, float maxY, boolean mirrored) { if (mirrored) { float tmpMinX = minX; minX = SwimmingModel.LEVEL_WIDTH - maxX; maxX = SwimmingModel.LEVEL_WIDTH - tmpMinX; } // Subtract y from startY because the level proceeds in the negative y direction. int minRowIndex = (int) Math.max(0, (startY - maxY) / rowHeight); int maxRowIndex = (int) Math.max(0, (startY - minY) / rowHeight); int minColIndex = (int) Math.min(NUM_COLS - 1, minX / COL_WIDTH); int maxColIndex = (int) Math.min(NUM_COLS - 1, maxX / COL_WIDTH); for (int i = minRowIndex; i <= maxRowIndex; i++) { for (int j = minColIndex; j <= maxColIndex; j++) { if (grid[i][j]) { return true; } } } return false; } @Override public void draw(Canvas canvas, float startY, boolean mirrored) { float y = startY - rowHeight; for (int i = 0; i < grid.length; i++) { float x = 0; for (int j = 0; j < grid[0].length; j++) { if (!mirrored && grid[i][j]) { canvas.drawRect(x, y, x + COL_WIDTH, y + rowHeight, paint); } else if (mirrored && grid[i][grid[0].length - 1 - j]) { canvas.drawRect(x, y, x + COL_WIDTH, y + rowHeight, paint); } x += COL_WIDTH; } y -= rowHeight; } } @Override public int getEndCol(boolean mirrored) { return mirrored ? NUM_COLS - 1 : 0; } @Override public float getChunkHeight() { return chunkHeight; } @Override public int getNumRows() { return numRows; } } private static class SolutionPathRow { // The first column which is in the solution path. public final int startCol; // The column after the last column in the solution path. public final int endCol; public final float startX; public final float endX; public SolutionPathRow(int startCol, int numCols) { this.startCol = startCol; this.endCol = startCol + numCols; this.startX = startCol * COL_WIDTH; this.endX = endCol * COL_WIDTH; } public boolean intersects(float x, boolean mirrored) { float startX = this.startX; float endX = this.endX; if (mirrored) { startX = SwimmingModel.LEVEL_WIDTH - this.endX; endX = SwimmingModel.LEVEL_WIDTH - this.startX; } return startX <= x && x <= endX; } } }