package se.nekotronic.satelliterush; import java.util.ArrayList; import java.util.Random; import java.util.Timer; import java.util.TimerTask; import android.app.Activity; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Style; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.location.LocationProvider; import android.os.Bundle; import android.text.format.Time; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; import android.view.View; import android.view.WindowManager; public class RushView extends View { // Ok, these bricks are round, but you get what I mean. Positions are in // pixels. class Brick { public float x, y; public float radius; public int color; public boolean active = true; public Brick(float x, float y, float radius, int color) { this.x = x; this.y = y; this.radius = radius; this.color = color; } } // class Brick // A ball is a moving brick! Speeds are measured in pixels per second. class Ball extends Brick { public float dx, dy; public Ball(float x, float y, float dx, float dy, float radius, int color) { super(x, y, radius, color); this.dx = dx; this.dy = dy; } } // class Ball class Paddle { // The "x_quotient" is the distance from the left corner to the current position, // when the current position is projected on the base line. // Measured in "baseline lengths". Not pixels. Not actual ground meters. public float x_quotient; public float width; public float thickness; public int color; public Paddle(float x_quotient, float width, float thickness, int color) { this.x_quotient = x_quotient; this.width = width; this.thickness = thickness; this.color = color; } } // class Ball private ArrayList bricks = new ArrayList(); private Ball ball; private Paddle paddle; private Timer ticker; private Random random; private LocationManager location_manager; private LocationListener location_listener; private boolean locations_enabled = true; private RushActivity context; private void init(final Context context) { if (context instanceof RushActivity) this.context = (RushActivity) context; random = new Random(); location_manager = (LocationManager) context .getSystemService(Context.LOCATION_SERVICE); // Define a listener that responds to location updates location_listener = new LocationListener() { @Override public void onLocationChanged(Location location) { use_new_location(location); } @Override public void onProviderDisabled(String provider) { if (locations_enabled) { locations_enabled = false; if (context != null) RushActivity.showWarning("GPS has been disabled.", context); } } @Override public void onProviderEnabled(String provider) { if (locations_enabled == false) { locations_enabled = true; if (context != null) RushActivity.showWarning( "GPS has been enabled again. Good!", context); } } @Override public void onStatusChanged(String provider, int status, Bundle extras) { if (status == LocationProvider.OUT_OF_SERVICE) { if (locations_enabled) { locations_enabled = false; if (context != null) RushActivity.showWarning("GPS is out of service.", context); } } else if (status == LocationProvider.TEMPORARILY_UNAVAILABLE) { if (locations_enabled) { locations_enabled = false; if (context != null) RushActivity.showWarning( "GPS is temporarily unavailable.", context); } } else if (status == LocationProvider.AVAILABLE) { if (locations_enabled == false) { locations_enabled = true; if (context != null) RushActivity.showWarning( "GPS is available again. Good!", context); } } } }; // Log.d("SatelliteRush", "init done"); } // init private Location latest_location; private Time latest_location_time; private Location left_corner; private Location right_corner; private Time left_corner_time; private Time right_corner_time; private Time game_start_time; private Time game_win_time; private float game_win_seconds; private float base_line_length; // actual ground distance in meters private float base_speed = -1; // screen speed in pixels per second, calculated from an actual ground speed // This should be carefully adjusted // A BASE_SPEED_FACTOR of 1 lets the ball move across a screen diagonal in the same time it took the player to walk the baseline public static float BASE_SPEED_FACTOR = 2.0f; // How much the ball speeds up after popping a brick public static float BALL_SPEED_UP_FACTOR = 1.03f; private void calculate_base_speed() { if (base_speed == -1) { // The base speed is only calculated once, // and depends on the player's speed when walking the base line // (actually: the time between the setting of the two corners) float walk_seconds = Math.abs(left_corner_time.toMillis(true) - right_corner_time.toMillis(true)) / 1000f; base_speed = (float)Math.sqrt(width_pixels * width_pixels + height_pixels * height_pixels) / walk_seconds * BASE_SPEED_FACTOR; ball.dx = base_speed / 1.4142f; ball.dy = - base_speed / 1.4142f; } } private void use_new_location(Location location) { latest_location = location; Time now = new Time(); now.setToNow(); latest_location_time = now; if (status == Status.WAITING_FOR_LEFT_CORNER) { left_corner = location; left_corner_time = now; if (location.getAccuracy() > 10) { corner_wait_status = CornerWaitStatus.NOT_ENOUGH_ACCURACY; } else if (right_corner == null) { status = Status.NOT_READY; } else { base_line_length = left_corner.distanceTo(right_corner); if (base_line_length < 50) { corner_wait_status = CornerWaitStatus.BASE_LINE_TOO_SHORT; } else { status = Status.READY; calculate_base_speed(); } } } if (status == Status.WAITING_FOR_RIGHT_CORNER) { right_corner = location; right_corner_time = now; if (location.getAccuracy() > 10) { corner_wait_status = CornerWaitStatus.NOT_ENOUGH_ACCURACY; } else if (left_corner == null) { status = Status.NOT_READY; } else { base_line_length = right_corner.distanceTo(left_corner); if (base_line_length < 50) { corner_wait_status = CornerWaitStatus.BASE_LINE_TOO_SHORT; } else { status = Status.READY; calculate_base_speed(); } } } if (left_corner != null && right_corner != null) { float distance_from_left = left_corner.distanceTo(latest_location); float distance_from_right = right_corner.distanceTo(latest_location); // This assumes that we are on (or at least close to) the base line // paddle.x = distance_from_left / (distance_from_left + distance_from_right) * width_pixels; // This projects the player's position onto the base line float baseline_meters_from_left = ((distance_from_left * distance_from_left) - (distance_from_right * distance_from_right) + (base_line_length * base_line_length)) / (2 * base_line_length); if (baseline_meters_from_left < 0) baseline_meters_from_left = 0; else if (baseline_meters_from_left > base_line_length) baseline_meters_from_left = base_line_length; paddle.x_quotient = baseline_meters_from_left / base_line_length; } /* * updateText(); double longitude = location.getLongitude(); double * latitude = location.getLatitude(); double accuracy = * location.getAccuracy(); double altitude= location.getAltitude(); * print("Long " + longitude + ", lat " + latitude + ", accuracy " + * accuracy + ", alt " + altitude); */ // The things to show on the screen may have changed now invalidate_view(); } // use_new_location public RushView(Context context) { super(context); init(context); } public RushView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public RushView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); } private int width_pixels = -1; private int height_pixels = -1; private float screen_density = -1; private boolean bricks_initialized = false; // This can be called not just from Android, but from the Graphical Layout // in Eclipse @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { width_pixels = w; height_pixels = h; // screen_density = 1f; Context context = getContext(); if (context != null && context instanceof Activity) { Activity parent = (Activity) getContext(); // parent.getWindowManager().getDefaultDisplay().getMetrics(dm); DisplayMetrics dm = new DisplayMetrics(); WindowManager window_manager = parent.getWindowManager(); if (dm != null && window_manager != null) { // Crashed with NullPointerException when in the Graphical // Layout in Eclipse Display display = window_manager.getDefaultDisplay(); display.getMetrics(dm); screen_density = dm.density; } } if (!bricks_initialized) { float d = screen_density; if (d == -1) d = 1f; float standard_brick_size = Math.min(w, h) / 10f * d; int nr_columns = (int) (w / standard_brick_size); float column_width = w / nr_columns; int nr_rows = (int) (h / standard_brick_size); // Rows of the entire // screen! float row_height = h / nr_rows; float current_y_center = row_height / 2; for (int y = 0; y < nr_rows / 2; ++y) { // Divide by 2: Only use the // upper half of the screen float current_x_center = column_width / 2; for (int x = 0; x < nr_columns; ++x) { Brick brick = new Brick(current_x_center, current_y_center, standard_brick_size * 0.4f, 0xff00ff00); bricks.add(brick); current_x_center += column_width; } current_y_center += row_height; } // Log.d("SatelliteRush", "h = " + h); ball = new Ball(w * 0.5f, h * 0.60f, 1, 1, standard_brick_size * 0.3f, 0xff0000ff); // paddle = new Paddle(0.5f, width_pixels / 1f, 10 * d, 0xff0000ff); paddle = new Paddle(0.5f, width_pixels / 5f, 10 * d, 0xff0000ff); bricks_initialized = true; } } // This can be called not just from Android, but from the Graphical Layout // in Eclipse // We need to synchronize since we read the same data that update_simulation // updates @Override synchronized protected void onDraw(Canvas canvas) { super.onDraw(canvas); int h = height_pixels; if (h == -1) h = canvas.getHeight(); int w = width_pixels; if (w == -1) w = canvas.getWidth(); float d = screen_density; if (d == -1) d = 1f; Paint p = new Paint(); for (Brick b : bricks) { if (b.active) { p.setColor(b.color); canvas.drawCircle(b.x, b.y, b.radius, p); } } p.setStyle(Style.STROKE); for (Brick b : bricks) { if (b.active) { p.setColor(Color.WHITE); canvas.drawCircle(b.x, b.y, b.radius, p); } } p.setStyle(Style.FILL); if (status != Status.YOU_WIN) { // Draw a line that shows the ball's direction p.setColor(Color.RED); // I don't remember how I found, or invented, this calculation, and I am rather suspicious of it. It seems to work, though. float ball_speed = (float)Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy); float screen_diagonal = (float)Math.sqrt(width_pixels * width_pixels + height_pixels * height_pixels); float speed_scale = screen_diagonal / ball_speed; canvas.drawLine(ball.x, ball.y, ball.x + ball.dx * speed_scale, ball.y + ball.dy * speed_scale, p); } p.setColor(ball.color); canvas.drawCircle(ball.x, ball.y, ball.radius, p); p.setColor(Color.WHITE); p.setStyle(Style.STROKE); canvas.drawCircle(ball.x, ball.y, ball.radius, p); p.setStyle(Style.FILL); float usable_base_pixels = width_pixels - paddle.width; float paddle_left = paddle.x_quotient * usable_base_pixels; float paddle_right = paddle.x_quotient * usable_base_pixels + paddle.width; p.setColor(paddle.color); canvas.drawRect(paddle_left, h - paddle.thickness, paddle_right, h, p); p.setColor(Color.WHITE); p.setStyle(Style.STROKE); canvas.drawRect(paddle_left, h - paddle.thickness, paddle_right, h, p); p.setStyle(Style.FILL); /* p.setColor(Color.WHITE); canvas.drawText("Satellite Rush: status = " + status, 20f, h / 2 + 20f, p); canvas.drawText("h = " + h + " (" + height_pixels + ")" + ", w = " + w + " (" + width_pixels + ")" + ", d = " + d + " (" + screen_density + ")", 20f, h / 2 + 40f, p); canvas.drawText("Ball: x = " + String.format("%.2f", ball.x) + ", y = " + String.format("%.2f", ball.y) + ", dx = " + String.format("%.2f", ball.dx) + ", dy = " + String.format("%.2f", ball.dy), 20f, h / 2 + 60f, p); canvas.drawText("Speed = " + String.format("%.2f", Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy)) + ", base_speed = " + String.format("%.2f", base_speed) + " (" + String.format("%.2f", Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy) / base_speed) + ")", 20f, h / 2 + 80f, p); final long now_nanos = java.lang.System.nanoTime(); if (previous_draw_nanos != -1 && now_nanos > previous_draw_nanos) { long nanos = now_nanos - previous_draw_nanos; float seconds = nanos / 1e9f; float frame_rate = 1 / seconds; if (accumulated_frame_rate == -1) accumulated_frame_rate = frame_rate; else accumulated_frame_rate = (0.99f * accumulated_frame_rate + 0.01f * frame_rate); canvas.drawText("frame rate = " + (int)(accumulated_frame_rate + 0.5), 20f, h / 2 + 100f, p); } previous_draw_nanos = now_nanos; canvas.drawText("paddle.x_quotient = " + String.format("%.2f", paddle.x_quotient), 20f, h / 2 + 120f, p); */ if (status == Status.YOU_WIN) { p.setColor(Color.GREEN); p.setTextSize(40f * d); canvas.drawText("YOU WIN!", 0, height_pixels * 0.25f, p); p.setTextSize(40f * d); int hours = (int)(game_win_seconds / 3600); int minutes = (int)(game_win_seconds % 3600 / 60); float seconds = game_win_seconds % 60; canvas.drawText("Time: " + hours + ":" + String.format("%02d", minutes) + ":" + String.format("%05.2f", seconds), 0, height_pixels * 0.50f, p); } if (this.locations_enabled == false) { p.setColor(Color.RED); p.setTextSize(20f * d); canvas.drawText("You need to enable GPS.", 0, height_pixels * 0.75f, p); } else if (status == Status.WAITING_FOR_LEFT_CORNER) { p.setColor(Color.RED); p.setTextSize(20f * d); canvas.drawText("Setting left corner. Wait...", 0, height_pixels * 0.75f, p); if (corner_wait_status == CornerWaitStatus.NOT_ENOUGH_ACCURACY) { canvas.drawText("(Waiting for better accuracy than " + String.format("%.2f", latest_location.getAccuracy()) + " m.)", 0, height_pixels * 0.75f + 25f * d, p); } else if (corner_wait_status == CornerWaitStatus.BASE_LINE_TOO_SHORT) { canvas.drawText("Base line too short (" + (int)(base_line_length + 0.5) + " m).", 0, height_pixels * 0.75f + 25f * d, p); canvas.drawText("Keep walking!", 0, height_pixels * 0.75f + 50f * d, p); } } else if (status == Status.WAITING_FOR_RIGHT_CORNER) { p.setColor(Color.RED); p.setTextSize(20f * d); canvas.drawText("Setting right corner. Wait...", 0, height_pixels * 0.75f, p); if (corner_wait_status == CornerWaitStatus.NOT_ENOUGH_ACCURACY) { canvas.drawText("(Waiting for better accuracy than " + String.format("%.2f", latest_location.getAccuracy()) + " m.)", 0, height_pixels * 0.75f + 25f * d, p); } else if (corner_wait_status == CornerWaitStatus.BASE_LINE_TOO_SHORT) { canvas.drawText("Base line too short (" + (int)(base_line_length + 0.5) + " m).", 0, height_pixels * 0.75f + 25f * d, p); canvas.drawText("Keep walking!", 0, height_pixels * 0.75f + 50f * d, p); } } else if (left_corner == null && right_corner == null) { p.setColor(Color.RED); p.setTextSize(20f * d); canvas.drawText("Welcome to Satellite Rush!", 0, height_pixels * 0.75f, p); canvas.drawText("First you must set the corners.", 0, height_pixels * 0.75f + 25f * d, p); canvas.drawText("Go there, and use the commands.", 0, height_pixels * 0.75f + 50f * d, p); } else if (left_corner == null) { p.setColor(Color.RED); p.setTextSize(20f * d); canvas.drawText("Now set the left corner.", 0, height_pixels * 0.75f, p); } else if (right_corner == null) { p.setColor(Color.RED); p.setTextSize(20f * d); canvas.drawText("Now set the right corner.", 0, height_pixels * 0.75f, p); } else if (status == status.READY) { p.setColor(Color.GREEN); p.setTextSize(20f * d); canvas.drawText("Ready to start the game!", 0, height_pixels * 0.75f, p); } } // onDraw private long previous_draw_nanos = -1; private float accumulated_frame_rate = -1; enum Status { NOT_READY, // Not started yet READY, // Will not continue until user commands it PAUSED, // Will continue as soon as visible RUNNING, WAITING_FOR_LEFT_CORNER, WAITING_FOR_RIGHT_CORNER, YOU_WIN } private Status status = Status.NOT_READY; enum CornerWaitStatus { NO_LOCATION_YET, NOT_ENOUGH_ACCURACY, BASE_LINE_TOO_SHORT } private CornerWaitStatus corner_wait_status = CornerWaitStatus.NO_LOCATION_YET; // Called when the player wants to begin (or continue) the game public void start_moving() { if (status == Status.READY || status == status.PAUSED) { // Should never be PAUSED if (game_start_time == null) { Time now = new Time(); now.setToNow(); game_start_time = now; } status = Status.RUNNING; context.disable_corner_commands(); start_ticker(); } else if (status == Status.YOU_WIN) { RushActivity.showWarning("You already won.", context); } else { RushActivity.showWarning("No no no! Set the corners first.", context); } invalidate_view(); } // Called when the player wants to pause the game public void stop_moving() { if (status == Status.RUNNING) { status = Status.READY; stop_ticker(); } invalidate_view(); } // Called when the system will show the screen (possibly for the first time) public void resume() { try { // Register the listener with the Location Manager to receive // location updates location_manager.requestLocationUpdates( LocationManager.GPS_PROVIDER, 0, 0, location_listener); } catch (Exception e) { RushActivity.showWarning("Couldn't use the GPS: " + e.getMessage(), context); } if (status == Status.PAUSED) { status = Status.RUNNING; start_ticker(); } invalidate_view(); } // Called when the system (not the player) will hide the screen or otherwise pause the game public void pause() { stop_ticker(); if (status == Status.RUNNING) { status = Status.READY; } location_manager.removeUpdates(location_listener); // invalidate_view(); -- No! } private void start_ticker() { if (ticker != null) return; ticker = new Timer(); TimerTask task = new TimerTask() { @Override public void run() { update_simulation(); invalidate_view(); } }; ticker.schedule(task, 100, 50); // Needs to wait? // ticker.scheduleAtFixedRate(task, 100, 100); } private void invalidate_view() { Runnable r = new Runnable() { @Override public void run() { invalidate(); } }; if (getHandler() != null) getHandler().post(r); } private void stop_ticker() { if (ticker != null) { ticker.cancel(); ticker = null; nanos_when_paused = java.lang.System.nanoTime(); } } // -1 is not guaranteed to never happen, but we ignore that private long previous_nanos = -1; private long nanos_when_paused = -1; // We need to synchronize since we update the same data that onDraw reads synchronized private void update_simulation() { long now_nanos = java.lang.System.nanoTime(); if (previous_nanos == -1 || now_nanos < previous_nanos) { // First time, or overflow, so don't update the game previous_nanos = now_nanos; return; } long nanos = now_nanos - previous_nanos; if (nanos_when_paused != -1) { // We have been paused! nanos = nanos_when_paused - previous_nanos; nanos_when_paused = -1; } previous_nanos = now_nanos; double seconds = nanos / 1e9; ball.x += ball.dx * seconds; ball.y += ball.dy * seconds; // Check for collisions and handle them Brick tragic_victim = null; for (Brick brick : bricks) { // To avoid some floating-point calculations, check a square first. if (brick.active && Math.abs(brick.x - ball.x) <= brick.radius + ball.radius + 1 && Math.abs(brick.y - ball.y) <= brick.radius + ball.radius + 1 && Math.sqrt((brick.x - ball.x) * (brick.x - ball.x) + (brick.y - ball.y) * (brick.y - ball.y)) < brick.radius + ball.radius) { // Log.d("Satellite Rush", "Popped a brick!"); double current_heading = Math.atan(ball.dy / ball.dx); if (ball.dx < 0) current_heading += Math.PI; double normal = Math.atan((ball.y - brick.y) / (ball.x - brick.x)); if ((ball.x - brick.x) < 0) normal += Math.PI; float ball_speed = (float)Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy); // Bounce back! double new_heading = 2 * normal - current_heading - Math.PI; ball.dx = ball_speed * (float)Math.cos(new_heading); ball.dy = ball_speed * (float)Math.sin(new_heading); // The ball speeds up slightly after each popped brick ball.dx *= BALL_SPEED_UP_FACTOR; ball.dy *= BALL_SPEED_UP_FACTOR; // unbore(); -- Done at the end of this simulation step /* // If a speed component (x or y) is less than 1% of the screen // size, increase it a bit float dx_limit = width_pixels / 100f; float dy_limit = height_pixels / 100f; if (Math.abs(ball.dx) < dx_limit) ball.dx = (dx_limit + random.nextFloat()) * Math.signum(ball.dx); if (Math.abs(ball.dy) < dy_limit) ball.dy = (dy_limit + random.nextFloat()) * Math.signum(ball.dy); */ tragic_victim = brick; break; } // if the ball hit a brick } // for each brick if (tragic_victim != null) { tragic_victim.active = false; boolean any_bricks_left = false; for (Brick brick : bricks) { if (brick.active) { any_bricks_left = true; break; } } if (any_bricks_left == false) { Time now = new Time(); now.setToNow(); game_win_time = now; game_win_seconds = (game_win_time.toMillis(true) - game_start_time.toMillis(true)) / 1000.0f; status = Status.YOU_WIN; stop_ticker(); } } // Bounce against the walls, with a small random adjustment to the speed if (ball.x - ball.radius < 0) { ball.x = ball.radius + (ball.radius - ball.x); ball.dx = -ball.dx * (99 + 2 * random.nextFloat()) / 100f; } if (ball.x + ball.radius > width_pixels) { // ball.x = width_pixels - (ball.radius + ball.x + ball.radius - // width_pixels); ball.x = 2 * width_pixels - 2 * ball.radius - ball.x; ball.dx = -ball.dx * (99 + 2 * random.nextFloat()) / 100f; } if (ball.y - ball.radius < 0) { ball.y = ball.radius + (ball.radius - ball.y); ball.dy = -ball.dy * (99 + 2 * random.nextFloat()) / 100f; } if (ball.dy > 0 && ball.y + ball.radius > height_pixels - paddle.thickness) { float usable_base_pixels = width_pixels - paddle.width; float paddle_left = paddle.x_quotient * usable_base_pixels; float paddle_right = paddle.x_quotient * usable_base_pixels + paddle.width; if (ball.x > paddle_left && ball.x < paddle_right) { ball.y = 2 * height_pixels - 2 * paddle.thickness - 2 * ball.radius - ball.y; ball.dy = -ball.dy * (99 + 2 * random.nextFloat()) / 100f; } } if (ball.y + ball.radius > height_pixels) { ball.y = 2 * height_pixels - 2 * ball.radius - ball.y; ball.dy = -ball.dy * (99 + 2 * random.nextFloat()) / 100f; add_a_ball(); ball.dx /= (BALL_SPEED_UP_FACTOR * BALL_SPEED_UP_FACTOR); ball.dy /= (BALL_SPEED_UP_FACTOR * BALL_SPEED_UP_FACTOR); } unbore(); } // update_simulation public float MIN_HORIZONTAL_FRACTION = 0.10f; public float MIN_VERTICAL_FRACTION = 0.20f; // If the ball movement is boring (almost entirely vertical or horizontal), then make it less boring void unbore() { if (ball.dx != 0 && Math.abs(ball.dy / ball.dx) < MIN_VERTICAL_FRACTION) { float ball_speed = (float)Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy); ball.dx = ball_speed * MIN_VERTICAL_FRACTION; ball.dy = (float)Math.sqrt(ball_speed * ball_speed - ball.dx * ball.dx) * Math.signum(ball.dy); } else if (ball.dy != 0 && Math.abs(ball.dx / ball.dy) < MIN_HORIZONTAL_FRACTION) { float ball_speed = (float)Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy); ball.dy = ball_speed * MIN_HORIZONTAL_FRACTION; ball.dx = (float)Math.sqrt(ball_speed * ball_speed - ball.dy * ball.dy) * Math.signum(ball.dx); } } // unbore void add_a_ball() { for (Brick brick : bricks) { if (!brick.active) { brick.color = Color.RED; brick.active = true; break; } } } public void mark_left_corner() { if (left_corner != null) RushActivity.showWarning("Setting new left corner.", context); status = Status.WAITING_FOR_LEFT_CORNER; corner_wait_status = CornerWaitStatus.NO_LOCATION_YET; invalidate_view(); } public void mark_right_corner() { if (right_corner != null) RushActivity.showWarning("Setting new right corner.", context); status = Status.WAITING_FOR_RIGHT_CORNER; corner_wait_status = CornerWaitStatus.NO_LOCATION_YET; invalidate_view(); } } // class RushView