Java tutorial
/* * Copyright (C) 2015 The Android Open Source Project * * 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 info.bartowski.easteregg; import android.animation.LayoutTransition; import android.animation.TimeAnimator; import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Outline; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.media.AudioManager; import android.os.Build; import android.os.Vibrator; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.InputDevice; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import java.util.ArrayList; // It's like LLand, but "M"ultiplayer. public class MLand extends FrameLayout { public static final String TAG = "MLand"; public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); public static final boolean DEBUG_DRAW = false; // DEBUG public static final boolean SHOW_TOUCHES = true; public static void L(String s, Object... objects) { if (DEBUG) { Log.d(TAG, objects.length == 0 ? s : String.format(s, objects)); } } public static final float PI_2 = (float) (Math.PI / 2); public static final boolean AUTOSTART = true; public static final boolean HAVE_STARS = true; public static final float DEBUG_SPEED_MULTIPLIER = 0.5f; // only if DEBUG public static final boolean DEBUG_IDDQD = Log.isLoggable(TAG + ".iddqd", Log.DEBUG); public static final int DEFAULT_PLAYERS = 1; public static final int MIN_PLAYERS = 1; public static final int MAX_PLAYERS = 6; static final float CONTROLLER_VIBRATION_MULTIPLIER = 2f; private static class Params { public float TRANSLATION_PER_SEC; public int OBSTACLE_SPACING, OBSTACLE_PERIOD; public int BOOST_DV; public int PLAYER_HIT_SIZE; public int PLAYER_SIZE; public int OBSTACLE_WIDTH, OBSTACLE_STEM_WIDTH; public int OBSTACLE_GAP; public int OBSTACLE_MIN; public int BUILDING_WIDTH_MIN, BUILDING_WIDTH_MAX; public int BUILDING_HEIGHT_MIN; public int CLOUD_SIZE_MIN, CLOUD_SIZE_MAX; public int STAR_SIZE_MIN, STAR_SIZE_MAX; public int G; public int MAX_V; public float SCENERY_Z, OBSTACLE_Z, PLAYER_Z, PLAYER_Z_BOOST, HUD_Z; public Params(Resources res) { TRANSLATION_PER_SEC = res.getDimension(R.dimen.mland_translation_per_sec); OBSTACLE_SPACING = res.getDimensionPixelSize(R.dimen.mland_obstacle_spacing); OBSTACLE_PERIOD = (int) (OBSTACLE_SPACING / TRANSLATION_PER_SEC); BOOST_DV = res.getDimensionPixelSize(R.dimen.mland_boost_dv); PLAYER_HIT_SIZE = res.getDimensionPixelSize(R.dimen.mland_player_hit_size); PLAYER_SIZE = res.getDimensionPixelSize(R.dimen.mland_player_size); OBSTACLE_WIDTH = res.getDimensionPixelSize(R.dimen.mland_obstacle_width); OBSTACLE_STEM_WIDTH = res.getDimensionPixelSize(R.dimen.mland_obstacle_stem_width); OBSTACLE_GAP = res.getDimensionPixelSize(R.dimen.mland_obstacle_gap); OBSTACLE_MIN = res.getDimensionPixelSize(R.dimen.mland_obstacle_height_min); BUILDING_HEIGHT_MIN = res.getDimensionPixelSize(R.dimen.mland_building_height_min); BUILDING_WIDTH_MIN = res.getDimensionPixelSize(R.dimen.mland_building_width_min); BUILDING_WIDTH_MAX = res.getDimensionPixelSize(R.dimen.mland_building_width_max); CLOUD_SIZE_MIN = res.getDimensionPixelSize(R.dimen.mland_cloud_size_min); CLOUD_SIZE_MAX = res.getDimensionPixelSize(R.dimen.mland_cloud_size_max); STAR_SIZE_MIN = res.getDimensionPixelSize(R.dimen.mland_star_size_min); STAR_SIZE_MAX = res.getDimensionPixelSize(R.dimen.mland_star_size_max); G = res.getDimensionPixelSize(R.dimen.mland_G); MAX_V = res.getDimensionPixelSize(R.dimen.mland_max_v); SCENERY_Z = res.getDimensionPixelSize(R.dimen.mland_scenery_z); OBSTACLE_Z = res.getDimensionPixelSize(R.dimen.mland_obstacle_z); PLAYER_Z = res.getDimensionPixelSize(R.dimen.mland_player_z); PLAYER_Z_BOOST = res.getDimensionPixelSize(R.dimen.mland_player_z_boost); HUD_Z = res.getDimensionPixelSize(R.dimen.mland_hud_z); // Sanity checking if (OBSTACLE_MIN <= OBSTACLE_WIDTH / 2) { L("error: obstacles might be too short, adjusting"); OBSTACLE_MIN = OBSTACLE_WIDTH / 2 + 1; } } } private TimeAnimator mAnim; private Vibrator mVibrator; private AudioManager mAudioManager; private View mSplash; private ViewGroup mScoreFields; private ArrayList<Player> mPlayers = new ArrayList<Player>(); private ArrayList<Obstacle> mObstaclesInPlay = new ArrayList<Obstacle>(); private float t, dt; private float mLastPipeTime; // in sec private int mCurrentPipeId; // basically, equivalent to the current score private int mWidth, mHeight; private boolean mAnimating, mPlaying; private boolean mFrozen; // after death, a short backoff private int mCountdown = 0; private boolean mFlipped; private int mTaps; private int mTimeOfDay; private static final int DAY = 0, NIGHT = 1, TWILIGHT = 2, SUNSET = 3; private static final int[][] SKIES = { { 0xFFc0c0FF, 0xFFa0a0FF }, // DAY { 0xFF000010, 0xFF000000 }, // NIGHT { 0xFF000040, 0xFF000010 }, // TWILIGHT { 0xFFa08020, 0xFF204080 }, // SUNSET }; private int mScene; private static final int SCENE_CITY = 0, SCENE_TX = 1, SCENE_ZRH = 2; private static final int SCENE_COUNT = 3; private static Params PARAMS; private static float dp = 1f; private Paint mTouchPaint, mPlayerTracePaint; private ArrayList<Integer> mGameControllers = new ArrayList<>(); public MLand(Context context) { this(context, null); } public MLand(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MLand(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); setFocusable(true); PARAMS = new Params(getResources()); mTimeOfDay = irand(0, SKIES.length - 1); mScene = irand(0, SCENE_COUNT); mTouchPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTouchPaint.setColor(0x80FFFFFF); mTouchPaint.setStyle(Paint.Style.FILL); mPlayerTracePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPlayerTracePaint.setColor(0x80FFFFFF); mPlayerTracePaint.setStyle(Paint.Style.STROKE); mPlayerTracePaint.setStrokeWidth(2 * dp); // we assume everything will be laid out left|top setLayoutDirection(LAYOUT_DIRECTION_LTR); setupPlayers(DEFAULT_PLAYERS); } @Override public void onAttachedToWindow() { super.onAttachedToWindow(); dp = getResources().getDisplayMetrics().density; reset(); if (AUTOSTART) { start(false); } } @Override public boolean willNotDraw() { return !DEBUG; } public int getGameWidth() { return mWidth; } public int getGameHeight() { return mHeight; } public float getGameTime() { return t; } public float getLastTimeStep() { return dt; } public void setScoreFieldHolder(ViewGroup vg) { mScoreFields = vg; if (vg != null) { final LayoutTransition lt = new LayoutTransition(); lt.setDuration(250); mScoreFields.setLayoutTransition(lt); } for (Player p : mPlayers) { mScoreFields.addView(p.mScoreField, new MarginLayoutParams(MarginLayoutParams.WRAP_CONTENT, MarginLayoutParams.MATCH_PARENT)); } } public void setSplash(View v) { mSplash = v; } public static boolean isGamePad(InputDevice dev) { int sources = dev.getSources(); // Verify that the device has gamepad buttons, control sticks, or both. return (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) || ((sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK)); } public ArrayList getGameControllers() { mGameControllers.clear(); int[] deviceIds = InputDevice.getDeviceIds(); for (int deviceId : deviceIds) { InputDevice dev = InputDevice.getDevice(deviceId); if (isGamePad(dev)) { if (!mGameControllers.contains(deviceId)) { mGameControllers.add(deviceId); } } } return mGameControllers; } public int getControllerPlayer(int id) { final int player = mGameControllers.indexOf(id); if (player < 0 || player >= mPlayers.size()) return 0; return player; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { dp = getResources().getDisplayMetrics().density; stop(); reset(); if (AUTOSTART) { start(false); } } final static float hsv[] = { 0, 0, 0 }; private static float luma(int bgcolor) { return 0.2126f * (float) (bgcolor & 0xFF0000) / 0xFF0000 + 0.7152f * (float) (bgcolor & 0xFF00) / 0xFF00 + 0.0722f * (float) (bgcolor & 0xFF) / 0xFF; } public Player getPlayer(int i) { return i < mPlayers.size() ? mPlayers.get(i) : null; } private int addPlayerInternal(Player p) { mPlayers.add(p); realignPlayers(); TextView scoreField = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.mland_scorefield, null); if (mScoreFields != null) { mScoreFields.addView(scoreField, new MarginLayoutParams(MarginLayoutParams.WRAP_CONTENT, MarginLayoutParams.MATCH_PARENT)); } p.setScoreField(scoreField); return mPlayers.size() - 1; } private void removePlayerInternal(Player p) { if (mPlayers.remove(p)) { removeView(p); mScoreFields.removeView(p.mScoreField); realignPlayers(); } } private void realignPlayers() { final int N = mPlayers.size(); float x = (mWidth - (N - 1) * PARAMS.PLAYER_SIZE) / 2; for (int i = 0; i < N; i++) { final Player p = mPlayers.get(i); p.setX(x); x += PARAMS.PLAYER_SIZE; } } private void clearPlayers() { while (mPlayers.size() > 0) { removePlayerInternal(mPlayers.get(0)); } } public void setupPlayers(int num) { clearPlayers(); for (int i = 0; i < num; i++) { addPlayerInternal(Player.create(this)); } } public void addPlayer() { if (getNumPlayers() == MAX_PLAYERS) return; addPlayerInternal(Player.create(this)); } public int getNumPlayers() { return mPlayers.size(); } public void removePlayer() { if (getNumPlayers() == MIN_PLAYERS) return; removePlayerInternal(mPlayers.get(mPlayers.size() - 1)); } private void thump(int playerIndex, long ms) { if (mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_SILENT) { // No interruptions. Not even game haptics. return; } if (playerIndex < mGameControllers.size()) { int controllerId = mGameControllers.get(playerIndex); InputDevice dev = InputDevice.getDevice(controllerId); if (dev != null && dev.getVibrator().hasVibrator()) { Utility.compatVibrate(dev.getVibrator(), (long) (ms * CONTROLLER_VIBRATION_MULTIPLIER)); return; } } Utility.compatVibrate(mVibrator, (long) (ms * CONTROLLER_VIBRATION_MULTIPLIER)); } public void reset() { L("reset"); final Drawable sky = new GradientDrawable(GradientDrawable.Orientation.BOTTOM_TOP, SKIES[mTimeOfDay]); sky.setDither(true); setBackground(sky); mFlipped = frand() > 0.5f; setScaleX(mFlipped ? -1 : 1); int i = getChildCount(); while (i-- > 0) { final View v = getChildAt(i); if (v instanceof GameView) { removeViewAt(i); } } mObstaclesInPlay.clear(); mCurrentPipeId = 0; mWidth = getWidth(); mHeight = getHeight(); boolean showingSun = (mTimeOfDay == DAY || mTimeOfDay == SUNSET) && frand() > 0.25; if (showingSun) { final Star sun = new Star(getContext()); sun.setBackground(Utility.getCompatDrawable(getContext(), R.drawable.sun)); final int w = getResources().getDimensionPixelSize(R.dimen.mland_sun_size); sun.setTranslationX(frand(w, mWidth - w)); if (mTimeOfDay == DAY) { sun.setTranslationY(frand(w, (mHeight * 0.66f))); DrawableCompat.setTint(sun.getBackground(), 0); } else { sun.setTranslationY(frand(mHeight * 0.66f, mHeight - w)); DrawableCompat.setTintMode(sun.getBackground(), PorterDuff.Mode.SRC_ATOP); DrawableCompat.setTint(sun.getBackground(), 0xC0FF8000); } addView(sun, new LayoutParams(w, w)); } if (!showingSun) { final boolean dark = mTimeOfDay == NIGHT || mTimeOfDay == TWILIGHT; final float ff = frand(); if ((dark && ff < 0.75f) || ff < 0.5f) { final Star moon = new Star(getContext()); moon.setBackground(Utility.getCompatDrawable(getContext(), R.drawable.moon)); moon.getBackground().setAlpha(dark ? 255 : 128); moon.setScaleX(frand() > 0.5 ? -1 : 1); moon.setRotation(moon.getScaleX() * frand(5, 30)); final int w = getResources().getDimensionPixelSize(R.dimen.mland_sun_size); moon.setTranslationX(frand(w, mWidth - w)); moon.setTranslationY(frand(w, mHeight - w)); addView(moon, new LayoutParams(w, w)); } } final int mh = mHeight / 6; final boolean cloudless = frand() < 0.25; final int N = 20; for (i = 0; i < N; i++) { final float r1 = frand(); final Scenery s; if (HAVE_STARS && r1 < 0.3 && mTimeOfDay != DAY) { s = new Star(getContext()); } else if (r1 < 0.6 && !cloudless) { s = new Cloud(getContext()); } else { switch (mScene) { case SCENE_ZRH: s = new Mountain(getContext()); break; case SCENE_TX: s = new Cactus(getContext()); break; case SCENE_CITY: default: s = new Building(getContext()); break; } s.z = (float) i / N; // no more shadows for these things //s.setTranslationZ(PARAMS.SCENERY_Z * (1+s.z)); s.v = 0.85f * s.z; // buildings move proportional to their distance if (mScene == SCENE_CITY) { s.setBackgroundColor(Color.GRAY); s.h = irand(PARAMS.BUILDING_HEIGHT_MIN, mh); } final int c = (int) (255f * s.z); final Drawable bg = s.getBackground(); if (bg != null) bg.setColorFilter(Color.rgb(c, c, c), PorterDuff.Mode.MULTIPLY); } final LayoutParams lp = new LayoutParams(s.w, s.h); if (s instanceof Building) { lp.gravity = Gravity.BOTTOM; } else { lp.gravity = Gravity.TOP; final float r = frand(); if (s instanceof Star) { lp.topMargin = (int) (r * r * mHeight); } else { lp.topMargin = (int) (1 - r * r * mHeight / 2) + mHeight / 2; } } addView(s, lp); s.setTranslationX(frand(-lp.width, mWidth + lp.width)); } for (Player p : mPlayers) { addView(p); // put it back! p.reset(); } realignPlayers(); if (mAnim != null) { mAnim.cancel(); } mAnim = new TimeAnimator(); mAnim.setTimeListener(new TimeAnimator.TimeListener() { @Override public void onTimeUpdate(TimeAnimator timeAnimator, long t, long dt) { step(t, dt); } }); } public void start(boolean startPlaying) { L("start(startPlaying=%s)", startPlaying ? "true" : "false"); if (startPlaying && mCountdown <= 0) { showSplash(); mSplash.findViewById(R.id.play_button).setEnabled(false); final View playImage = mSplash.findViewById(R.id.play_button_image); final TextView playText = (TextView) mSplash.findViewById(R.id.play_button_text); playImage.animate().alpha(0f); playText.animate().alpha(1f); mCountdown = 3; post(new Runnable() { @Override public void run() { if (mCountdown == 0) { startPlaying(); } else { postDelayed(this, 500); } playText.setText(String.valueOf(mCountdown)); mCountdown--; } }); } for (Player p : mPlayers) { p.setVisibility(View.INVISIBLE); } if (!mAnimating) { mAnim.start(); mAnimating = true; } } public void hideSplash() { if (mSplash != null && mSplash.getVisibility() == View.VISIBLE) { mSplash.setClickable(false); ViewCompat.animate(mSplash).alpha(0).translationZ(0).setDuration(300).withEndAction(new Runnable() { @Override public void run() { mSplash.setVisibility(View.GONE); } }); } } public void showSplash() { if (mSplash != null && mSplash.getVisibility() != View.VISIBLE) { mSplash.setClickable(true); mSplash.setAlpha(0f); mSplash.setVisibility(View.VISIBLE); mSplash.animate().alpha(1f).setDuration(1000); mSplash.findViewById(R.id.play_button_image).setAlpha(1f); mSplash.findViewById(R.id.play_button_text).setAlpha(0f); mSplash.findViewById(R.id.play_button).setEnabled(true); mSplash.findViewById(R.id.play_button).requestFocus(); } } public void startPlaying() { mPlaying = true; t = 0; // there's a sucker born every OBSTACLE_PERIOD mLastPipeTime = getGameTime() - PARAMS.OBSTACLE_PERIOD; hideSplash(); realignPlayers(); mTaps = 0; final int N = mPlayers.size(); for (int i = 0; i < N; i++) { final Player p = mPlayers.get(i); p.setVisibility(View.VISIBLE); p.reset(); p.start(); p.boost(-1, -1); // start you off flying! p.unboost(); // not forever, though } } public void stop() { if (mAnimating) { mAnim.cancel(); mAnim = null; mAnimating = false; mPlaying = false; mTimeOfDay = irand(0, SKIES.length - 1); // for next reset mScene = irand(0, SCENE_COUNT); mFrozen = true; for (Player p : mPlayers) { p.die(); } postDelayed(new Runnable() { @Override public void run() { mFrozen = false; } }, 250); } } public static final float lerp(float x, float a, float b) { return (b - a) * x + a; } public static final float rlerp(float v, float a, float b) { return (v - a) / (b - a); } public static final float clamp(float f) { return f < 0f ? 0f : f > 1f ? 1f : f; } public static final float frand() { return (float) Math.random(); } public static final float frand(float a, float b) { return lerp(frand(), a, b); } public static final int irand(int a, int b) { return Math.round(frand((float) a, (float) b)); } public static int pick(int[] l) { return l[irand(0, l.length - 1)]; } private void step(long t_ms, long dt_ms) { t = t_ms / 1000f; // seconds dt = dt_ms / 1000f; if (DEBUG) { t *= DEBUG_SPEED_MULTIPLIER; dt *= DEBUG_SPEED_MULTIPLIER; } // 1. Move all objects and update bounds final int N = getChildCount(); int i = 0; for (; i < N; i++) { final View v = getChildAt(i); if (v instanceof GameView) { ((GameView) v).step(t_ms, dt_ms, t, dt); } } if (mPlaying) { int livingPlayers = 0; for (i = 0; i < mPlayers.size(); i++) { final Player p = getPlayer(i); if (p.mAlive) { // 2. Check for altitude if (p.below(mHeight)) { if (DEBUG_IDDQD) { poke(i); unpoke(i); } else { L("player %d hit the floor", i); thump(i, 80); p.die(); } } // 3. Check for obstacles int maxPassedStem = 0; for (int j = mObstaclesInPlay.size(); j-- > 0;) { final Obstacle ob = mObstaclesInPlay.get(j); if (ob.intersects(p) && !DEBUG_IDDQD) { L("player hit an obstacle"); thump(i, 80); p.die(); } else if (ob.cleared(p)) { if (ob instanceof Stem) { maxPassedStem = Math.max(maxPassedStem, ((Stem) ob).id); } } } if (maxPassedStem > p.mScore) { p.addScore(1); } } if (p.mAlive) livingPlayers++; } if (livingPlayers == 0) { stop(); mTaps = 0; final int playerCount = mPlayers.size(); for (int pi = 0; pi < playerCount; pi++) { final Player p = mPlayers.get(pi); } } } // 4. Handle edge of screen // Walk backwards to make sure removal is safe while (i-- > 0) { final View v = getChildAt(i); if (v instanceof Obstacle) { if (v.getTranslationX() + v.getWidth() < 0) { removeViewAt(i); mObstaclesInPlay.remove(v); } } else if (v instanceof Scenery) { final Scenery s = (Scenery) v; if (v.getTranslationX() + s.w < 0) { v.setTranslationX(getWidth()); } } } // 3. Time for more obstacles! if (mPlaying && (t - mLastPipeTime) > PARAMS.OBSTACLE_PERIOD) { mLastPipeTime = t; mCurrentPipeId++; final int obstacley = (int) (frand() * (mHeight - 2 * PARAMS.OBSTACLE_MIN - PARAMS.OBSTACLE_GAP)) + PARAMS.OBSTACLE_MIN; final int inset = (PARAMS.OBSTACLE_WIDTH - PARAMS.OBSTACLE_STEM_WIDTH) / 2; final int yinset = PARAMS.OBSTACLE_WIDTH / 2; final int d1 = irand(0, 250); final Obstacle s1 = new Stem(getContext(), obstacley - yinset, false); addView(s1, new LayoutParams(PARAMS.OBSTACLE_STEM_WIDTH, (int) s1.h, Gravity.TOP | Gravity.LEFT)); s1.setTranslationX(mWidth + inset); s1.setTranslationY(-s1.h - yinset); ViewCompat.setTranslationZ(s1, PARAMS.OBSTACLE_Z * 0.75f); s1.animate().translationY(0).setStartDelay(d1).setDuration(250); mObstaclesInPlay.add(s1); final Obstacle p1 = new Pop(getContext(), PARAMS.OBSTACLE_WIDTH); addView(p1, new LayoutParams(PARAMS.OBSTACLE_WIDTH, PARAMS.OBSTACLE_WIDTH, Gravity.TOP | Gravity.LEFT)); p1.setTranslationX(mWidth); p1.setTranslationY(-PARAMS.OBSTACLE_WIDTH); ViewCompat.setTranslationZ(p1, PARAMS.OBSTACLE_Z); p1.setScaleX(0.25f); p1.setScaleY(-0.25f); p1.animate().translationY(s1.h - inset).scaleX(1f).scaleY(-1f).setStartDelay(d1).setDuration(250); mObstaclesInPlay.add(p1); final int d2 = irand(0, 250); final Obstacle s2 = new Stem(getContext(), mHeight - obstacley - PARAMS.OBSTACLE_GAP - yinset, true); addView(s2, new LayoutParams(PARAMS.OBSTACLE_STEM_WIDTH, (int) s2.h, Gravity.TOP | Gravity.LEFT)); s2.setTranslationX(mWidth + inset); s2.setTranslationY(mHeight + yinset); ViewCompat.setTranslationZ(s2, PARAMS.OBSTACLE_Z * 0.75f); s2.animate().translationY(mHeight - s2.h).setStartDelay(d2).setDuration(400); mObstaclesInPlay.add(s2); final Obstacle p2 = new Pop(getContext(), PARAMS.OBSTACLE_WIDTH); addView(p2, new LayoutParams(PARAMS.OBSTACLE_WIDTH, PARAMS.OBSTACLE_WIDTH, Gravity.TOP | Gravity.LEFT)); p2.setTranslationX(mWidth); p2.setTranslationY(mHeight); ViewCompat.setTranslationZ(p2, PARAMS.OBSTACLE_Z); p2.setScaleX(0.25f); p2.setScaleY(0.25f); p2.animate().translationY(mHeight - s2.h - yinset).scaleX(1f).scaleY(1f).setStartDelay(d2) .setDuration(400); mObstaclesInPlay.add(p2); } if (SHOW_TOUCHES || DEBUG_DRAW) invalidate(); } @Override public boolean onTouchEvent(MotionEvent ev) { L("touch: %s", ev); final int actionIndex = ev.getActionIndex(); final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); int playerIndex = (int) (getNumPlayers() * (x / getWidth())); if (mFlipped) playerIndex = getNumPlayers() - 1 - playerIndex; switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: poke(playerIndex, x, y); return true; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: unpoke(playerIndex); return true; } return false; } @Override public boolean onTrackballEvent(MotionEvent ev) { L("trackball: %s", ev); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: poke(0); return true; case MotionEvent.ACTION_UP: unpoke(0); return true; } return false; } @Override public boolean onKeyDown(int keyCode, KeyEvent ev) { L("keyDown: %d", keyCode); switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_SPACE: case KeyEvent.KEYCODE_ENTER: case KeyEvent.KEYCODE_BUTTON_A: int player = getControllerPlayer(ev.getDeviceId()); poke(player); return true; } return false; } @Override public boolean onKeyUp(int keyCode, KeyEvent ev) { L("keyDown: %d", keyCode); switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_SPACE: case KeyEvent.KEYCODE_ENTER: case KeyEvent.KEYCODE_BUTTON_A: int player = getControllerPlayer(ev.getDeviceId()); unpoke(player); return true; } return false; } @Override public boolean onGenericMotionEvent(MotionEvent ev) { L("generic: %s", ev); return false; } private void poke(int playerIndex) { poke(playerIndex, -1, -1); } private void poke(int playerIndex, float x, float y) { L("poke(%d)", playerIndex); if (mFrozen) return; if (!mAnimating) { reset(); } if (!mPlaying) { start(true); } else { final Player p = getPlayer(playerIndex); if (p == null) return; // no player for this controller p.boost(x, y); mTaps++; if (DEBUG) { p.dv *= DEBUG_SPEED_MULTIPLIER; p.animate().setDuration((long) (200 / DEBUG_SPEED_MULTIPLIER)); } } } private void unpoke(int playerIndex) { L("unboost(%d)", playerIndex); if (mFrozen || !mAnimating || !mPlaying) return; final Player p = getPlayer(playerIndex); if (p == null) return; // no player for this controller p.unboost(); } @Override public void onDraw(Canvas c) { super.onDraw(c); if (SHOW_TOUCHES) { for (Player p : mPlayers) { if (p.mTouchX > 0) { mTouchPaint.setColor(0x80FFFFFF & p.color); mPlayerTracePaint.setColor(0x80FFFFFF & p.color); float x1 = p.mTouchX; float y1 = p.mTouchY; c.drawCircle(x1, y1, 100, mTouchPaint); float x2 = p.getX() + p.getPivotX(); float y2 = p.getY() + p.getPivotY(); float angle = PI_2 - (float) Math.atan2(x2 - x1, y2 - y1); x1 += 100 * Math.cos(angle); y1 += 100 * Math.sin(angle); c.drawLine(x1, y1, x2, y2, mPlayerTracePaint); } } } if (!DEBUG_DRAW) return; final Paint pt = new Paint(); pt.setColor(0xFFFFFFFF); for (Player p : mPlayers) { final int L = p.corners.length; final int N = L / 2; for (int i = 0; i < N; i++) { final int x = (int) p.corners[i * 2]; final int y = (int) p.corners[i * 2 + 1]; c.drawCircle(x, y, 4, pt); c.drawLine(x, y, p.corners[(i * 2 + 2) % L], p.corners[(i * 2 + 3) % L], pt); } } pt.setStyle(Paint.Style.STROKE); pt.setStrokeWidth(getResources().getDisplayMetrics().density); final int M = getChildCount(); pt.setColor(0x8000FF00); for (int i = 0; i < M; i++) { final View v = getChildAt(i); if (v instanceof Player) continue; if (!(v instanceof GameView)) continue; if (v instanceof Pop) { final Pop pop = (Pop) v; c.drawCircle(pop.cx, pop.cy, pop.r, pt); } else { final Rect r = new Rect(); v.getHitRect(r); c.drawRect(r, pt); } } pt.setColor(Color.BLACK); final StringBuilder sb = new StringBuilder("obstacles: "); for (Obstacle ob : mObstaclesInPlay) { sb.append(ob.hitRect.toShortString()); sb.append(" "); } pt.setTextSize(20f); c.drawText(sb.toString(), 20, 100, pt); } static final Rect sTmpRect = new Rect(); private interface GameView { public void step(long t_ms, long dt_ms, float t, float dt); } private static class Player extends ImageView implements GameView { public float dv; public int color; private MLand mLand; private boolean mBoosting; private float mTouchX = -1, mTouchY = -1; private boolean mAlive; private int mScore; private TextView mScoreField; private final int[] sColors = new int[] { //0xFF78C557, 0xFFDB4437, 0xFF3B78E7, 0xFFF4B400, 0xFF0F9D58, 0xFF7B1880, 0xFF9E9E9E, }; static int sNextColor = 0; private final float[] sHull = new float[] { 0.3f, 0f, // left antenna 0.7f, 0f, // right antenna 0.92f, 0.33f, // off the right shoulder of Orion 0.92f, 0.75f, // right hand (our right, not his right) 0.6f, 1f, // right foot 0.4f, 1f, // left foot BLUE! 0.08f, 0.75f, // sinistram 0.08f, 0.33f, // cold shoulder }; public final float[] corners = new float[sHull.length]; public static Player create(MLand land) { final Player p = new Player(land.getContext()); p.mLand = land; p.reset(); p.setVisibility(View.INVISIBLE); land.addView(p, new LayoutParams(PARAMS.PLAYER_SIZE, PARAMS.PLAYER_SIZE)); return p; } private void setScore(int score) { mScore = score; if (mScoreField != null) { mScoreField.setText(DEBUG_IDDQD ? "??" : String.valueOf(score)); } } public int getScore() { return mScore; } private void addScore(int incr) { setScore(mScore + incr); } public void setScoreField(TextView tv) { mScoreField = tv; if (tv != null) { setScore(mScore); // reapply //mScoreField.setBackgroundResource(R.drawable.scorecard); mScoreField.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_ATOP); mScoreField.setTextColor(luma(color) > 0.7f ? 0xFF000000 : 0xFFFFFFFF); } } public void reset() { //setX(mLand.mWidth / 2); setY(mLand.mHeight / 2 + (int) (Math.random() * PARAMS.PLAYER_SIZE) - PARAMS.PLAYER_SIZE / 2); setScore(0); setScoreField(mScoreField); // refresh color mBoosting = false; dv = 0; } public Player(Context context) { super(context); setBackground(Utility.getCompatDrawable(context, R.drawable.android)); DrawableCompat.setTintMode(getBackground(), PorterDuff.Mode.SRC_ATOP); color = sColors[(sNextColor++ % sColors.length)]; DrawableCompat.setTint(getBackground(), color); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { setOutlineProvider(new ViewOutlineProvider() { @TargetApi(Build.VERSION_CODES.M) @Override public void getOutline(View view, Outline outline) { final int w = view.getWidth(); final int h = view.getHeight(); final int ix = (int) (w * 0.3f); final int iy = (int) (h * 0.2f); outline.setRect(ix, iy, w - ix, h - iy); } }); } } public void prepareCheckIntersections() { final int inset = (PARAMS.PLAYER_SIZE - PARAMS.PLAYER_HIT_SIZE) / 2; final int scale = PARAMS.PLAYER_HIT_SIZE; final int N = sHull.length / 2; for (int i = 0; i < N; i++) { corners[i * 2] = scale * sHull[i * 2] + inset; corners[i * 2 + 1] = scale * sHull[i * 2 + 1] + inset; } final Matrix m = getMatrix(); m.mapPoints(corners); } public boolean below(int h) { final int N = corners.length / 2; for (int i = 0; i < N; i++) { final int y = (int) corners[i * 2 + 1]; if (y >= h) return true; } return false; } public void step(long t_ms, long dt_ms, float t, float dt) { if (!mAlive) { // float away with the garbage setTranslationX(getTranslationX() - PARAMS.TRANSLATION_PER_SEC * dt); return; } if (mBoosting) { dv = -PARAMS.BOOST_DV; } else { dv += PARAMS.G; } if (dv < -PARAMS.MAX_V) dv = -PARAMS.MAX_V; else if (dv > PARAMS.MAX_V) dv = PARAMS.MAX_V; final float y = getTranslationY() + dv * dt; setTranslationY(y < 0 ? 0 : y); setRotation(90 + lerp(clamp(rlerp(dv, PARAMS.MAX_V, -1 * PARAMS.MAX_V)), 90, -90)); prepareCheckIntersections(); } public void boost(float x, float y) { mTouchX = x; mTouchY = y; boost(); } public void boost() { mBoosting = true; dv = -PARAMS.BOOST_DV; animate().cancel(); ViewCompat.animate(this).scaleX(1.25f).scaleY(1.25f).translationZ(PARAMS.PLAYER_Z_BOOST) .setDuration(100); setScaleX(1.25f); setScaleY(1.25f); } public void unboost() { mBoosting = false; mTouchX = mTouchY = -1; animate().cancel(); ViewCompat.animate(this).scaleX(1f).scaleY(1f).translationZ(PARAMS.PLAYER_Z).setDuration(200); } public void die() { mAlive = false; if (mScoreField != null) { //mScoreField.setTextColor(0xFFFFFFFF); //mScoreField.getBackground().setColorFilter(0xFF666666, PorterDuff.Mode.SRC_ATOP); //mScoreField.setBackgroundResource(R.drawable.scorecard_gameover); } } public void start() { mAlive = true; } } private class Obstacle extends View implements GameView { public float h; public final Rect hitRect = new Rect(); public Obstacle(Context context, float h) { super(context); setBackgroundColor(0xFFFF0000); this.h = h; } public boolean intersects(Player p) { final int N = p.corners.length / 2; for (int i = 0; i < N; i++) { final int x = (int) p.corners[i * 2]; final int y = (int) p.corners[i * 2 + 1]; if (hitRect.contains(x, y)) return true; } return false; } public boolean cleared(Player p) { final int N = p.corners.length / 2; for (int i = 0; i < N; i++) { final int x = (int) p.corners[i * 2]; if (hitRect.right >= x) return false; } return true; } @Override public void step(long t_ms, long dt_ms, float t, float dt) { setTranslationX(getTranslationX() - PARAMS.TRANSLATION_PER_SEC * dt); getHitRect(hitRect); } } static final int[] ANTENNAE = new int[] { R.drawable.mm_antennae, R.drawable.mm_antennae2 }; static final int[] EYES = new int[] { R.drawable.mm_eyes, R.drawable.mm_eyes2 }; static final int[] MOUTHS = new int[] { R.drawable.mm_mouth1, R.drawable.mm_mouth2, R.drawable.mm_mouth3, R.drawable.mm_mouth4 }; private class Pop extends Obstacle { int mRotate; int cx, cy, r; // The marshmallow illustration and hitbox is 2/3 the size of its container. Drawable antenna, eyes, mouth; public Pop(Context context, float h) { super(context, h); setBackground(Utility.getCompatDrawable(context, R.drawable.mm_head)); antenna = Utility.getCompatDrawable(context, pick(ANTENNAE)); if (frand() > 0.5f) { eyes = Utility.getCompatDrawable(context, pick(EYES)); if (frand() > 0.8f) { mouth = Utility.getCompatDrawable(context, pick(MOUTHS)); } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { setOutlineProvider(new ViewOutlineProvider() { @TargetApi(Build.VERSION_CODES.M) @Override public void getOutline(View view, Outline outline) { final int pad = (int) (getWidth() * 1f / 6); outline.setOval(pad, pad, getWidth() - pad, getHeight() - pad); } }); } } public boolean intersects(Player p) { final int N = p.corners.length / 2; for (int i = 0; i < N; i++) { final int x = (int) p.corners[i * 2]; final int y = (int) p.corners[i * 2 + 1]; if (Math.hypot(x - cx, y - cy) <= r) return true; } return false; } @Override public void step(long t_ms, long dt_ms, float t, float dt) { super.step(t_ms, dt_ms, t, dt); if (mRotate != 0) { setRotation(getRotation() + dt * 45 * mRotate); } cx = (hitRect.left + hitRect.right) / 2; cy = (hitRect.top + hitRect.bottom) / 2; r = getWidth() / 3; // see above re 2/3 container size } @Override public void onDraw(Canvas c) { super.onDraw(c); if (antenna != null) { antenna.setBounds(0, 0, c.getWidth(), c.getHeight()); antenna.draw(c); } if (eyes != null) { eyes.setBounds(0, 0, c.getWidth(), c.getHeight()); eyes.draw(c); } if (mouth != null) { mouth.setBounds(0, 0, c.getWidth(), c.getHeight()); mouth.draw(c); } } } private class Stem extends Obstacle { Paint mPaint = new Paint(); Path mShadow = new Path(); GradientDrawable mGradient = new GradientDrawable(); boolean mDrawShadow; Path mJandystripe; Paint mPaint2; int id; // use this to track which pipes have been cleared public Stem(Context context, float h, boolean drawShadow) { super(context, h); id = mCurrentPipeId; mDrawShadow = drawShadow; setBackground(null); mGradient.setOrientation(GradientDrawable.Orientation.LEFT_RIGHT); mPaint.setColor(0xFF000000); mPaint.setColorFilter(new PorterDuffColorFilter(0x22000000, PorterDuff.Mode.MULTIPLY)); if (frand() < 0.01f) { mGradient.setColors(new int[] { 0xFFFFFFFF, 0xFFDDDDDD }); mJandystripe = new Path(); mPaint2 = new Paint(); mPaint2.setColor(0xFFFF0000); mPaint2.setColorFilter(new PorterDuffColorFilter(0xFFFF0000, PorterDuff.Mode.MULTIPLY)); } else { //mPaint.setColor(0xFFA1887F); mGradient.setColors(new int[] { 0xFFBCAAA4, 0xFFA1887F }); } } @Override public void onAttachedToWindow() { super.onAttachedToWindow(); setWillNotDraw(false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { setOutlineProvider(new ViewOutlineProvider() { @TargetApi(Build.VERSION_CODES.M) @Override public void getOutline(View view, Outline outline) { outline.setRect(0, 0, getWidth(), getHeight()); } }); } } @Override public void onDraw(Canvas c) { final int w = c.getWidth(); final int h = c.getHeight(); mGradient.setGradientCenter(w * 0.75f, 0); mGradient.setBounds(0, 0, w, h); mGradient.draw(c); if (mJandystripe != null) { mJandystripe.reset(); mJandystripe.moveTo(0, w); mJandystripe.lineTo(w, 0); mJandystripe.lineTo(w, 2 * w); mJandystripe.lineTo(0, 3 * w); mJandystripe.close(); for (int y = 0; y < h; y += 4 * w) { c.drawPath(mJandystripe, mPaint2); mJandystripe.offset(0, 4 * w); } } if (!mDrawShadow) return; mShadow.reset(); mShadow.moveTo(0, 0); mShadow.lineTo(w, 0); mShadow.lineTo(w, PARAMS.OBSTACLE_WIDTH * 0.4f + w * 1.5f); mShadow.lineTo(0, PARAMS.OBSTACLE_WIDTH * 0.4f); mShadow.close(); c.drawPath(mShadow, mPaint); } } private class Scenery extends FrameLayout implements GameView { public float z; public float v; public int h, w; public Scenery(Context context) { super(context); } @Override public void step(long t_ms, long dt_ms, float t, float dt) { setTranslationX(getTranslationX() - PARAMS.TRANSLATION_PER_SEC * dt * v); } } private class Building extends Scenery { public Building(Context context) { super(context); w = irand(PARAMS.BUILDING_WIDTH_MIN, PARAMS.BUILDING_WIDTH_MAX); h = 0; // will be setup later, along with z } } static final int[] CACTI = { R.drawable.cactus1, R.drawable.cactus2, R.drawable.cactus3 }; private class Cactus extends Building { public Cactus(Context context) { super(context); setBackground(Utility.getCompatDrawable(context, pick(CACTI))); w = h = irand(PARAMS.BUILDING_WIDTH_MAX / 4, PARAMS.BUILDING_WIDTH_MAX / 2); } } static final int[] MOUNTAINS = { R.drawable.mountain1, R.drawable.mountain2, R.drawable.mountain3 }; private class Mountain extends Building { public Mountain(Context context) { super(context); setBackground(Utility.getCompatDrawable(context, pick(MOUNTAINS))); w = h = irand(PARAMS.BUILDING_WIDTH_MAX / 2, PARAMS.BUILDING_WIDTH_MAX); z = 0; } } private class Cloud extends Scenery { public Cloud(Context context) { super(context); setBackground( Utility.getCompatDrawable(context, frand() < 0.01f ? R.drawable.cloud_off : R.drawable.cloud)); getBackground().setAlpha(0x40); w = h = irand(PARAMS.CLOUD_SIZE_MIN, PARAMS.CLOUD_SIZE_MAX); z = 0; v = frand(0.15f, 0.5f); } } private class Star extends Scenery { public Star(Context context) { super(context); setBackground(Utility.getCompatDrawable(context, R.drawable.star)); w = h = irand(PARAMS.STAR_SIZE_MIN, PARAMS.STAR_SIZE_MAX); v = z = 0; } } }