/* * 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.games.gumball; import android.app.Activity; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.graphics.Color; import android.graphics.Typeface; import android.graphics.drawable.AnimationDrawable; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.media.AudioManager; import android.media.MediaPlayer; import android.media.SoundPool; import android.os.Build; import android.os.Bundle; import android.support.graphics.drawable.VectorDrawableCompat; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.Surface; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.Animation.AnimationListener; import android.view.animation.AnimationUtils; import android.view.animation.TranslateAnimation; import android.widget.Button; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; import com.google.android.apps.santatracker.R; import com.google.android.apps.santatracker.games.common.PlayGamesActivity; import com.google.android.apps.santatracker.games.matching.CircleView; import com.google.android.apps.santatracker.games.matching.LevelTextView; import com.google.android.apps.santatracker.games.matching.MatchingGameConstants; import com.google.android.apps.santatracker.invites.AppInvitesFragment; import com.google.android.apps.santatracker.util.ImmersiveModeHelper; import org.jbox2d.callbacks.ContactImpulse; import org.jbox2d.callbacks.ContactListener; import org.jbox2d.collision.Manifold; import org.jbox2d.common.Vec2; import org.jbox2d.dynamics.Body; import org.jbox2d.dynamics.BodyType; import org.jbox2d.dynamics.contacts.Contact; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.LinkedList; import java.util.Queue; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * Gumball game fragment. */ public class TiltGameFragment extends Fragment implements SensorEventListener, ContactListener, AnimationListener, OnClickListener { /** * Bounce rate of objects in the physics world. */ public static final float WORLD_OBJECT_BOUNCE = 0.2f; /** * Density of objects in the physics world. */ public static final float WORLD_OBJECT_DENSITY = 185.77f; /** * Friction of objects in the physics world. */ public static final float WORLD_OBJECT_FRICTION = 0.2f; /** * Friction of floor objects in the physics world. */ public static final float WORLD_FLOOR_FRICTION = 0.8f; /** * Initial X position of the floor and pipes in the physics world. */ public static final float WORLD_FLOOR_X = 3.37f; /* * Initial Y position of the floor and pipes in the physics world. */ public static final float WORLD_FLOOR_Y = 0f; /** View that contains the main game. */ private TiltGameView mGameView; /** * Box2D physics world for this game. */ private PhysicsWorld mWorld; /** * Current rotation of the device. Used to adjust sensor readings if the screen is rotate in * portrait or landscape. * * @see android.view.Display#getRotation() */ private int mRotation; /** * Main game thread. */ private Runnable mGameThread; /** * Previous value of the sensor's Y reading. Used to calculate the rotational offset between * sensor events. */ private float mPreviousSensorY = 0f; /** * MediaPlayer that plays the background music. */ private MediaPlayer mBackgroundMusic; /** * Index of loaded sound effect in sound pool for small bounce. */ private int mSoundBounceSmall = -1; /** * Index of loaded sound effect in sound pool for medium bounce. */ private int mSoundBounceMed = -1; /** * Index of loaded sound effect in sound pool for large bounce. */ private int mSoundBounceLarge = -1; /** * Index of loaded sound effect in sound pool for ball in machine. */ private int mSoundBallInMachine = -1; /** * Index of loaded sound effect in sound pool for failed ball. */ private int mSoundBallFail = -1; /** * Index of loaded sound effect in sound pool for dropped ball. */ private int mSoundBallDrop = -1; /** * Index of loaded sound effect in sound pool for game over. */ private int mSoundGameOver = -1; /** * Scale down animation for level. */ private Animation mAnimationScaleLevelDown; /** * Fading out animation for level. */ private Animation mAnimationLevelFadeOut; /** * Scaling up animation for level. */ private Animation mAnimationLevelScaleUp; /** * Outlet animation for balls. */ private Animation mAnimationOutlet; /** * Alpha animation for timer updates. */ private Animation mAnimationTimerAlpha; /** * View for end of level circle overlay. */ private CircleView mEndLevelCircle; /** * View that shows the current level number. */ private LevelTextView mLevelNumberText; /** * Sound pool from which all sounds are played back. */ private SoundPool mSoundPool; /** * Holder for sound pool id to handle playbacks, connects and disconnects. */ private final HashMap<UUID, Boolean> mSoundPoolId = new HashMap<>(); /** * Number of balls left in the game. */ private int mGameBallsLeft = 2; /** * Current play level. Zero indexed, first level is 0. */ private int mCurrentLevelNum = 0; /** * View for the ball outlet at the top of the screen. */ private View mGameOutlet; /** * Root view of the game layout. */ private View mRootView; /** * Gumballs that are queued to be dropped through the outlet. */ private Queue<Gumball> mGumballQueue; /** * The current, active gumball on screen. */ private Gumball mCurrentGumball; /** * X position of outlet in the last animation. */ private float mOutletPreviousXPos = 0; /** * Array of the ball indicator views at the bottom of the screen. */ private ImageView mViewIndicators[] = new ImageView[6]; /** * Number of gumballs collected in the current game. */ private int mNumberCollected = 0; /** * Refresh rate for the game countdown timer. * * @see com.google.android.apps.santatracker.games.gumball.TiltGameFragment.GameCountdown */ private int mFramesPerSecond = 60; /** * Time left in the current game. Value in milliseconds. */ private long mTimeLeftInMillis = MatchingGameConstants.GUMBALL_INIT_TIME; /** * Countdown timer for the current game. */ private GameCountdown mCountDownTimer = null; /** * Countdown timer text. */ private TextView mViewCountdown; /** * Score text. */ private TextView mViewScore; /** * Total score of current game. */ private int mMatchScore = 0; /** * Number of balls that have respawned in the current level. Used to calculate the total * game score. */ private int mCountLevelBallRespawns = 0; /** * Flag indicating if the game is paused. */ private boolean wasPaused = false; private ImageView mViewPlayButton; private ImageView mViewPauseButton; private ImageButton mViewBigPlayButton; private ImageView mViewCancelBar; private ImageView mViewInviteButton; private View mViewMatchPauseOverlay; private View mViewPlayAgainBackground; private View mViewPlayAgainMain; private Button mViewPlayAgainButton; private TextView mViewPlayAgainScore; private TextView mViewPlayAgainLevel; private Animation mAnimationPlayAgainBackground; private Animation mAnimationPlayAgainMain; /** * Display offset on X axis for outlet in pixels. */ private int mOutletOffset; /** * View that displays the instructions from {@link #mDrawableTransition} */ private ImageView mViewInstructions; /** * Drawable that contains all images for the instructions. */ private AnimationDrawable mDrawableTransition; private SharedPreferences mSharedPreferences; private ImageView mViewGPlusSignIn; private View mViewGPlusLayout; private ImageButton mViewMainMenuButton; private AppInvitesFragment mInvitesFragment; /** * Gets an instance of this fragment */ public static TiltGameFragment newInstance() { TiltGameFragment fragment = new TiltGameFragment(); return fragment; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { mRootView = inflater.inflate(R.layout.fragment_gumball, container, false); mRootView.setKeepScreenOn(true); // Use a lower resolution background image to conserve memory below ICS if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { View matchScoreLayout = mRootView.findViewById(R.id.tilt_score_layout); matchScoreLayout.setBackgroundResource(R.drawable.score_background_gingerbread); } mViewPlayAgainScore = (TextView) mRootView.findViewById(R.id.play_again_score); mViewPlayAgainScore.setText(String.valueOf(mMatchScore)); mViewPlayAgainLevel = (TextView) mRootView.findViewById(R.id.play_again_level); mViewPlayAgainLevel.setText(String.valueOf(mCurrentLevelNum)); mViewPlayAgainBackground = mRootView.findViewById(R.id.play_again_bkgrd); mViewPlayAgainMain = mRootView.findViewById(R.id.play_again_main); mViewPlayAgainButton = (Button) mRootView.findViewById(R.id.play_again_btn); mViewPlayAgainButton.setOnClickListener(this); mViewGPlusSignIn = (ImageView) mRootView.findViewById(R.id.gplus_button); mViewGPlusSignIn.setOnClickListener(this); mViewGPlusLayout = mRootView.findViewById(R.id.play_again_gplus); mViewGPlusLayout.setVisibility(View.GONE); // Initialise all animations // Construct an animation to blink the timer indefinitely mAnimationTimerAlpha = new AlphaAnimation(0.0f, 1.0f); mAnimationTimerAlpha.setDuration(1000); mAnimationTimerAlpha.setRepeatMode(Animation.REVERSE); mAnimationTimerAlpha.setRepeatCount(Animation.INFINITE); // Load all other animations mAnimationPlayAgainBackground = AnimationUtils .loadAnimation(getActivity(), R.anim.play_again_bkgrd_anim); mAnimationPlayAgainBackground.setFillAfter(true); mAnimationPlayAgainBackground.setAnimationListener(this); mAnimationPlayAgainMain = AnimationUtils .loadAnimation(getActivity(), R.anim.play_again_main_anim); mAnimationPlayAgainMain.setFillAfter(true); mAnimationPlayAgainMain.setAnimationListener(this); mAnimationScaleLevelDown = AnimationUtils .loadAnimation(getActivity(), R.anim.scale_level_anim_down); mAnimationScaleLevelDown.setAnimationListener(this); mAnimationLevelFadeOut = AnimationUtils .loadAnimation(getActivity(), R.anim.level_fade_out_anim); mAnimationLevelFadeOut.setAnimationListener(this); mAnimationLevelScaleUp = AnimationUtils .loadAnimation(getActivity(), R.anim.scale_up_level_anim); mAnimationLevelScaleUp.setAnimationListener(this); mViewMainMenuButton = (ImageButton) mRootView.findViewById(R.id.main_menu_button); mViewMainMenuButton.setVisibility(View.GONE); mViewMainMenuButton.setOnClickListener(this); // App Invites Button mViewInviteButton = (ImageView) mRootView.findViewById(R.id.invite_button); mViewInviteButton.setVisibility(View.GONE); mViewInviteButton.setOnClickListener(this); mGameOutlet = mRootView.findViewById(R.id.tiltGameOutlet); mOutletOffset = getResources().getInteger(R.integer.outlet_offset); mViewIndicators[0] = (ImageView) mRootView.findViewById(R.id.indicator1); mViewIndicators[1] = (ImageView) mRootView.findViewById(R.id.indicator2); mViewIndicators[2] = (ImageView) mRootView.findViewById(R.id.indicator3); mViewIndicators[3] = (ImageView) mRootView.findViewById(R.id.indicator4); mViewIndicators[4] = (ImageView) mRootView.findViewById(R.id.indicator5); mViewIndicators[5] = (ImageView) mRootView.findViewById(R.id.indicator6); mViewCountdown = (TextView) mRootView.findViewById(R.id.tiltTimer); mLevelNumberText = (LevelTextView) mRootView.findViewById(R.id.tilt_end_level_number); mLevelNumberText.setVisibility(View.GONE); mEndLevelCircle = (CircleView) mRootView.findViewById(R.id.tilt_end_level_circle); mEndLevelCircle.setVisibility(View.GONE); mViewPlayButton = (ImageView) mRootView.findViewById(R.id.tilt_play_button); mViewPlayButton.setOnClickListener(this); mViewPlayButton.setVisibility(View.GONE); mViewPauseButton = (ImageView) mRootView.findViewById(R.id.tilt_pause_button); mViewPauseButton.setOnClickListener(this); mViewPauseButton.setVisibility(View.VISIBLE); mViewMatchPauseOverlay = mRootView.findViewById(R.id.tilt_pause_overlay); mViewMatchPauseOverlay.setVisibility(View.GONE); mViewBigPlayButton = (ImageButton) mRootView.findViewById(R.id.tilt_big_play_button); mViewBigPlayButton.setOnClickListener(this); mViewCancelBar = (ImageView) mRootView.findViewById(R.id.tilt_cancel_bar); mViewCancelBar.setOnClickListener(this); mViewCancelBar.setVisibility(View.GONE); mViewScore = (TextView) mRootView.findViewById(R.id.tilt_score); mViewScore.setText(String.valueOf(mMatchScore)); mGameView = (TiltGameView) mRootView.findViewById(R.id.tiltGameView); // Create the Box2D physics world. mWorld = new PhysicsWorld(); Vec2 gravity = new Vec2(0.0f, 0.0f); mWorld.create(gravity); mGameView.setModel(mWorld); mWorld.getWorld().setContactListener(this); mGumballQueue = new LinkedList<>(); // Initialise the sound pool and audio playback mSoundPool = new SoundPool(5, AudioManager.STREAM_MUSIC, 0); mSoundBounceSmall = mSoundPool.load(getActivity(), R.raw.gbg_ball_bounce_1, 1); mSoundBounceMed = mSoundPool.load(getActivity(), R.raw.gbg_ball_bounce_2, 1); mSoundBounceLarge = mSoundPool.load(getActivity(), R.raw.gbg_ball_bounce_3, 1); mSoundBallInMachine = mSoundPool.load(getActivity(), R.raw.gbg_ball_into_machine, 1); mSoundBallFail = mSoundPool.load(getActivity(), R.raw.gbg_ball_fall_out, 1); mSoundBallDrop = mSoundPool.load(getActivity(), R.raw.gbg_new_ball_bounce_drop, 1); mSoundGameOver = mSoundPool.load(getActivity(), R.raw.gameover, 1); // Display the instructions if they haven't been seen before mSharedPreferences = getActivity().getSharedPreferences(MatchingGameConstants.PREFERENCES_FILENAME, getActivity().MODE_PRIVATE); if (!mSharedPreferences.getBoolean(MatchingGameConstants.GUMBALL_INSTRUCTIONS_VIEWED, false)) { mDrawableTransition = new AnimationDrawable(); mDrawableTransition.addFrame(VectorDrawableCompat.create(getResources(), R.drawable.instructions_shake_1, null), 300); mDrawableTransition.addFrame(VectorDrawableCompat.create(getResources(), R.drawable.instructions_shake_2, null), 300); mDrawableTransition.addFrame(VectorDrawableCompat.create(getResources(), R.drawable.instructions_shake_3, null), 300); mDrawableTransition.setOneShot(false); mViewInstructions = (ImageView) mRootView.findViewById(R.id.instructions); if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { mViewInstructions.setImageResource(R.drawable.instructions_shake_1); } else { mViewInstructions.setImageDrawable(mDrawableTransition); mViewInstructions.post(new Runnable() { public void run() { mDrawableTransition.start(); } }); } // Hide the instructions after 2 seconds mViewInstructions.postDelayed(new HideInstructionsRunnable(), 2200); } return mRootView; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mInvitesFragment = AppInvitesFragment.getInstance(getActivity()); } @Override public void onResume() { super.onResume(); // Resume the game play if the game was not paused if (!wasPaused) { mRotation = getActivity().getWindowManager().getDefaultDisplay().getRotation(); SensorManager sensorManager = (SensorManager) getActivity() .getSystemService(Activity.SENSOR_SERVICE); Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); if (sensor != null) { sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_GAME); } mCountDownTimer = new GameCountdown(mFramesPerSecond, mTimeLeftInMillis); mCountDownTimer.start(); mGameView.setGameCountDown(mCountDownTimer); } // Start the game loop if it is not initialised yet if (mGameThread == null) { mGameThread = new Runnable() { public void run() { synchronized (mWorld) { if (!wasPaused) { if (mCurrentLevelNum == 0) { mCurrentLevelNum++; loadLevel(mCurrentLevelNum); } mWorld.update(); mGameView.invalidate(); } } getActivity().getWindow().getDecorView().postDelayed(mGameThread, 10); } }; } getActivity().getWindow().getDecorView().postDelayed(mGameThread, 1000); loadBackgroundMusic(); updateSignInButtonVisibility(); } @Override public void onPause() { super.onPause(); pauseGame(); if (mBackgroundMusic != null) { mBackgroundMusic.stop(); mBackgroundMusic.release(); } getActivity().getWindow().getDecorView().removeCallbacks(mGameThread); } private void loadBackgroundMusic() { mBackgroundMusic = MediaPlayer.create(getActivity(), R.raw.santatracker_musicloop); mBackgroundMusic.setLooping(true); mBackgroundMusic.setVolume(.2f, .2f); mBackgroundMusic.start(); } /** * Hide the sign in button if sign in was successful. */ public void onSignInSucceeded() { setSignInButtonVisibility(false); } public void onSignInFailed() { } @Override public void onClick(View view) { if (view.equals(mViewPauseButton)) { // Pause the game pauseGame(); } else if (view.equals(mViewPlayButton) || view.equals(mViewBigPlayButton)) { // Continue the game unPauseGame(); } else if (view.equals(mViewPlayAgainButton)) { // Reload the background music for a new game if (mBackgroundMusic != null) { mBackgroundMusic.stop(); mBackgroundMusic.release(); } loadBackgroundMusic(); // Reset the game variables mCurrentLevelNum = 0; mTimeLeftInMillis = MatchingGameConstants.GUMBALL_INIT_TIME; mMatchScore = 0; mViewScore.setText(String.valueOf(mMatchScore)); wasPaused = false; // Hide the pause screen mViewPlayAgainBackground.clearAnimation(); mViewPlayAgainMain.clearAnimation(); mViewPlayAgainBackground.setVisibility(View.GONE); mViewPlayAgainMain.setVisibility(View.GONE); mViewGPlusLayout.setVisibility(View.GONE); mViewMainMenuButton.setVisibility(View.GONE); mViewInviteButton.setVisibility(View.GONE); } else if (view.equals(mViewGPlusSignIn)) { // Start sign-in flow. PlayGamesActivity act = Utils.getPlayGamesActivity(this); if (act != null) { act.startSignIn(); } } else if (view.equals(mViewCancelBar) || (view.equals(mViewMainMenuButton))) { // Exit and return to previous Activity. returnToBackClass(); } else if (view.equals(mViewInviteButton)) { // Send App Invite mInvitesFragment.sendGameInvite( getString(R.string.gumball), getString(R.string.gumball_game_id), mMatchScore); } } private void returnToBackClass() { getActivity().finish(); } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } @Override public void onSensorChanged(SensorEvent event) { float x, y; if (getActivity() != null) { // Store the current screen rotation (used to offset the readings of the sensor). mRotation = getActivity().getWindowManager().getDefaultDisplay().getRotation(); } // Handle screen rotations by interpreting the sensor readings here if (mRotation == Surface.ROTATION_0) { x = -event.values[0]; y = -event.values[1]; } else if (mRotation == Surface.ROTATION_90) { x = event.values[1]; y = -event.values[0]; } else if (mRotation == Surface.ROTATION_180) { x = event.values[0]; y = event.values[1]; } else { x = -event.values[1]; y = event.values[0]; } // keep y low to simulate gravity if (mPreviousSensorY == 0f) { mPreviousSensorY = -9; } else if (mPreviousSensorY > y) { mPreviousSensorY = y; } // restrict x to ~+-45 degrees x *= 0.5; mWorld.getWorld().setGravity(new Vec2(x, mPreviousSensorY)); } @Override public void beginContact(Contact contact) { } /** * Handle contact with objects in the Box 2D world. * Here the main game logic is implemented: When a ball hits the bottom pipe, it is removed * and the next level or ball is started. * When the ball goes over the edge, it is removed and a new ball is dropped from the pipe * again. */ @Override public void endContact(Contact contact) { // If the gumball goes in the pipe, remove it from the scene (Case 1/2) if (contact.getFixtureA().getBody().getUserData() != null && !(contact.getFixtureA() .getBody().getUserData() instanceof Gumball) && ( contact.getFixtureA().getBody().getUserData().equals(TiltGameView.PIPE_BOTTOM) || contact.getFixtureA().getBody().getUserData() .equals(TiltGameView.PIPE_SIDES))) { mWorld.mBodiesToBeRemoved.add(contact.getFixtureB().getBody()); mSoundPoolId .remove(((Gumball) contact.getFixtureB().getBody().getUserData()).mSoundPoolId); onBallInPipe(); } else if (contact.getFixtureB().getBody().getUserData() != null && !(contact.getFixtureB() .getBody().getUserData() instanceof Gumball) && ( // If the gumball goes in the pipe, remove it from the scene (Case 2/2) contact.getFixtureA().getBody().getUserData().equals(TiltGameView.PIPE_BOTTOM) || contact.getFixtureA().getBody().getUserData() .equals(TiltGameView.PIPE_SIDES))) { mWorld.mBodiesToBeRemoved.add(contact.getFixtureA().getBody()); mSoundPoolId .remove(((Gumball) contact.getFixtureA().getBody().getUserData()).mSoundPoolId); onBallInPipe(); } else if (contact.getFixtureA().getBody().getUserData() != null && !(contact.getFixtureA() .getBody().getUserData() instanceof Gumball) && contact.getFixtureA().getBody() .getUserData().equals(TiltGameView.GAME_FLOOR)) { // If the gumball goes over the edge, remove it and respawn (Case 1/2) Gumball gumball = ((Gumball) contact.getFixtureB().getBody().getUserData()); mWorld.mBodiesToBeRemoved.add(contact.getFixtureB().getBody()); mSoundPoolId.remove(gumball.mSoundPoolId); mSoundPool.play(mSoundBallFail, 1, 1, 0, 0, 1.0f); mWorld.getWorld().step(1.0f / 60.0f, 10, 10); moveOutlet((mCurrentGumball.mXInitPos)); mCountLevelBallRespawns++; } else if (contact.getFixtureB().getBody().getUserData() != null && !(contact.getFixtureB() .getBody().getUserData() instanceof Gumball) && contact.getFixtureB().getBody() .getUserData().equals(TiltGameView.GAME_FLOOR)) { // If the gumball goes over the edge, remove it and respawn (Case 2/2) Gumball gumball = ((Gumball) contact.getFixtureB().getBody().getUserData()); mWorld.mBodiesToBeRemoved.add(contact.getFixtureA().getBody()); mSoundPoolId.remove(gumball.mSoundPoolId); mSoundPool.play(mSoundBallFail, 1, 1, 0, 0, 1.0f); mWorld.getWorld().step(1.0f / 60.0f, 10, 10); moveOutlet((mCurrentGumball.mXInitPos)); mCountLevelBallRespawns++; } } /** * Successfully dropped a ball in the pipe. * Add the next ball and go to the next level if no balls are left in this level. */ private void onBallInPipe() { mSoundPool.play(mSoundBallInMachine, 1, 1, 0, 0, 1.0f); mGameBallsLeft--; mNumberCollected++; changeIndicator(); mMatchScore += 50 * Math.max(1f, (mCurrentLevelNum - mCountLevelBallRespawns)); mViewScore.setText(String.valueOf(mMatchScore)); if (mGameBallsLeft == 0 && mViewPlayAgainBackground.getVisibility() != View.VISIBLE) { // No balls are left in this level, go to the next one mCurrentLevelNum++; mLevelNumberText.setLevelNumber(mCurrentLevelNum); mLevelNumberText.startAnimation(mAnimationLevelScaleUp); mEndLevelCircle.startAnimation(mAnimationScaleLevelDown); } } /* * (non-Javadoc) * * @see * org.jbox2d.callbacks.ContactListener#postSolve(org.jbox2d.dynamics.contacts * .Contact, org.jbox2d.callbacks.ContactImpulse) */ /** * Play a sound on impact (when a ball is dropped). * The sound depends on the severity of the impact. * * @see #playBounceSound(float) */ @Override public void postSolve(Contact contact, ContactImpulse impulse) { // Get both collision objects Object dataA = contact.getFixtureA().getBody().getUserData(); Object dataB = contact.getFixtureB().getBody().getUserData(); // Check if one of the objects is NOT a gumball, but a candy cane. boolean hitCane = false; if (dataA != null && !(dataA instanceof Gumball) && (Integer) dataA > TiltGameView.GUMBALL_PURPLE) { hitCane = true; } else if (dataB != null && !(dataB instanceof Gumball) && (Integer) dataB > TiltGameView.GUMBALL_PURPLE) { hitCane = true; } if (hitCane && impulse.normalImpulses[0] > 80) { playBounceSound(impulse.normalImpulses[0]); } } /** * Plays a 'bounce' sound through the sound pool, depending on the impulse. */ private void playBounceSound(float impulse) { if (impulse > 80) { mSoundPool.play(mSoundBounceLarge, 1, 1, 0, 0, 1.0f); } else if (impulse > 60) { mSoundPool.play(mSoundBounceMed, 1, 1, 0, 0, 1.0f); } else if (impulse > 30) { mSoundPool.play(mSoundBounceSmall, 1, 1, 0, 0, 1.0f); } } @Override public void preSolve(Contact contact, Manifold arg1) { } /** * Add a gumball to the game and play the ball drop sound. */ private void addGumball(float xPos, float yPos) { Gumball gumball = new Gumball(); gumball.mXInitPos = xPos; gumball.mYInitPos = yPos; gumball.mSoundPoolId = UUID.randomUUID(); mSoundPoolId.put(gumball.mSoundPoolId, false); mGameView.addGumball(gumball); mSoundPool.play(mSoundBallDrop, 1, 1, 0, 0, 1); } private JSONObject readLevelFile(int levelNumber) throws IOException, JSONException { // load the appropriate levels file from a raw resource. InputStream is = getResources() .openRawResource(Utils.getLevelRawFile(mCurrentLevelNum)); int size = is.available(); byte[] buffer = new byte[size]; is.read(buffer); is.close(); String json = new String(buffer, "UTF-8"); JSONObject level = new JSONObject(json); return level; } /** * Loads a level from the levels json file and sets up the game world. */ private void loadLevel(int levelNumber) { // Reset the current game state if (mCountDownTimer != null) { mCountDownTimer.cancel(); } mCountLevelBallRespawns = 0; mNumberCollected = 0; mViewPlayAgainLevel.setText(String.valueOf(levelNumber)); Body body = mWorld.getWorld().getBodyList(); while (body != null) { if (body.m_userData == null) { body = body.getNext(); continue; } mWorld.mBodiesToBeRemoved.add(body); body = body.getNext(); } mWorld.getWorld().step(1.0f / 60.0f, 10, 10); try { // Read the level file and extract the candy cane positions JSONObject level = readLevelFile(levelNumber); JSONArray canes = level.getJSONArray("candycanes"); for (int i = 0; i < canes.length(); i++) { JSONObject canePart = canes.getJSONObject(i); int type = canePart.getInt("type"); float xPos = (float) canePart.getDouble("xPos"); float yPos = (float) canePart.getDouble("yPos"); // Add the candy cane to the game world, the values represent the mWorld.addItem(xPos, yPos, Edges.getEdges(type), WORLD_OBJECT_BOUNCE, type, WORLD_OBJECT_DENSITY, WORLD_OBJECT_FRICTION, BodyType.STATIC); } // Add the sides and floor to the game world to catch dropped balls. // Note that the WORLD_FRICTION is used as the bounce rate of the floors. mWorld.addItem(WORLD_FLOOR_X, WORLD_FLOOR_Y, Edges.getPipeSideEdges(), WORLD_OBJECT_BOUNCE, TiltGameView.PIPE_SIDES, WORLD_OBJECT_DENSITY, WORLD_OBJECT_FRICTION, BodyType.STATIC); mWorld.addFloor(WORLD_FLOOR_X, WORLD_FLOOR_Y, TiltGameView.GAME_FLOOR, WORLD_OBJECT_DENSITY, WORLD_OBJECT_FRICTION, WORLD_FLOOR_FRICTION, BodyType.STATIC); mWorld.addPipeBottom(WORLD_FLOOR_X, WORLD_FLOOR_Y, TiltGameView.PIPE_BOTTOM, WORLD_OBJECT_DENSITY, WORLD_OBJECT_FRICTION, WORLD_FLOOR_FRICTION, BodyType.STATIC); // Add the gumballs JSONArray gumballs = level.getJSONArray("gumballs"); mGameBallsLeft = gumballs.length(); setIndicators(mGameBallsLeft); for (int j = 0; j < gumballs.length(); j++) { JSONObject gumball = gumballs.getJSONObject(j); float xPos = (float) gumball.getDouble("xPos"); float yPos = (float) gumball.getDouble("yPos"); Gumball gumballObject = new Gumball(); gumballObject.mXInitPos = xPos; gumballObject.mYInitPos = yPos; mGumballQueue.add(gumballObject); } mCurrentGumball = mGumballQueue.poll(); // Start the timer if (mCurrentGumball != null) { if (mCurrentLevelNum > 1) { // Do not include gumball dropping time in countdown calculation. mTimeLeftInMillis += MatchingGameConstants.GUMBALL_ADDED_TIME; } mCountDownTimer = new GameCountdown(mFramesPerSecond, mTimeLeftInMillis); mCountDownTimer.start(); mGameView.setGameCountDown(mCountDownTimer); // Move the outlet to its initial position moveOutlet((mCurrentGumball.mXInitPos)); } } catch (IOException e) { } catch (JSONException e) { } } /** * Update the state of the indicators at the bottom of the screen to the number of balls * collected. */ private void setIndicators(int numGumballs) { for(int i=0; i < mViewIndicators.length ; i++){ int stateResource = R.drawable.gbg_gumball_indicator_collected_disabled; if(i+1 <= numGumballs){ stateResource = R.drawable.gbg_gumball_indicator_pending; } mViewIndicators[i].setImageResource(stateResource); } } /** * Mark the last indicator for which a ball was collected in the 'collected' state. */ private void changeIndicator() { mViewIndicators[mNumberCollected - 1].setImageResource( R.drawable.gbg_gumball_indicator_collected); } @Override public void onAnimationEnd(Animation animation) { if (animation == mAnimationScaleLevelDown) { // After the level scale down animation, fade out the level number and end circle mLevelNumberText.startAnimation(mAnimationLevelFadeOut); mEndLevelCircle.startAnimation(mAnimationLevelFadeOut); } else if (animation == mAnimationLevelFadeOut) { // After the level fade out animation reset and hide all other end level views mEndLevelCircle.clearAnimation(); mLevelNumberText.clearAnimation(); mLevelNumberText.setVisibility(View.GONE); mEndLevelCircle.setVisibility(View.GONE); } else if (animation == mAnimationOutlet) { // After the outlet has moved to the correct position, add gumball addGumball(mCurrentGumball.mXInitPos, mCurrentGumball.mYInitPos); if (mGumballQueue.peek() != null) { // Move it to the next position if there is a gumball left in the queue mCurrentGumball = mGumballQueue.poll(); moveOutlet(mCurrentGumball.mXInitPos); } } } @Override public void onAnimationRepeat(Animation arg0) { // do nothing } @Override public void onAnimationStart(Animation animation) { if (animation == mAnimationScaleLevelDown) { // Show the circle level end and level text views when the animation starts mEndLevelCircle.setVisibility(View.VISIBLE); mLevelNumberText.setVisibility(View.VISIBLE); } else if (animation == mAnimationLevelFadeOut) { // Load the next level after the end level animation is over loadLevel(mCurrentLevelNum); } else if (animation == mAnimationPlayAgainBackground) { // Show the 'play again' screen when the animation starts and cancel the timer mViewPlayAgainBackground.setVisibility(View.VISIBLE); if (mCountDownTimer != null) { mCountDownTimer.cancel(); } } else if (animation == mAnimationPlayAgainMain) { mViewPlayAgainMain.setVisibility(View.VISIBLE); setSignInButtonVisibility(true); } } /** * Set the visibility of the sign in button if the user is not already signed in. */ private void setSignInButtonVisibility(boolean show) { mViewGPlusLayout.setVisibility(show && !Utils.isSignedIn(this) ? View.VISIBLE : View.GONE); } /** * Hide the sign in button when the user signs in and the button is still visible on screen. */ private void updateSignInButtonVisibility() { if (mViewGPlusLayout.getVisibility() == View.VISIBLE && Utils.isSignedIn(this)) { setSignInButtonVisibility(false); } } /** * Start an animation to move the outlet to the x position in pixels. */ private void moveOutlet(float xPos) { float scale = mRootView.getWidth() / 10.0f; mAnimationOutlet = new TranslateAnimation(mOutletPreviousXPos, (scale * xPos) - mOutletOffset, 0, 0); mAnimationOutlet.setDuration(700); mAnimationOutlet.setFillAfter(true); mAnimationOutlet.setStartOffset(400); mAnimationOutlet.setAnimationListener(this); mGameOutlet.startAnimation(mAnimationOutlet); mOutletPreviousXPos = (scale * xPos) - mOutletOffset; } /** * Countdown for the main game. * Updates the countdown on screen and stops the game when the timer runs out. */ public class GameCountdown { private Boolean animationStarted = false; private final long mMillisDuration; private final long mMillisTickDuration; private long mTicksLeft; private boolean mStarted = false; private long mSecondsTextValue = -1; /** * @param framesPerSecond assumed frame rate * @param millisInFuture duration of game at this frame rate */ public GameCountdown(int framesPerSecond, long millisInFuture) { mMillisDuration = millisInFuture; mMillisTickDuration = 1000 / framesPerSecond; mTicksLeft = mMillisDuration / mMillisTickDuration; } /** * Stop the timer. */ public void cancel() { mTicksLeft = 0; mStarted = false; } /** * Starts the timer. */ public void start() { mStarted = true; mSecondsTextValue = -1; long seconds = TimeUnit.MILLISECONDS.toSeconds(mTicksLeft * mMillisTickDuration); if (seconds >= 6) { animationStarted = false; mViewCountdown.clearAnimation(); mViewCountdown.setTextColor(Color.WHITE); mViewCountdown.setTypeface(Typeface.DEFAULT); } } /** * Update the displayed timer. * When the timer is below 6s the text color changes to red. */ public void tick() { if (mStarted) { --mTicksLeft; mTimeLeftInMillis = mTicksLeft * mMillisTickDuration; if (mTimeLeftInMillis < 6000 && !animationStarted) { animationStarted = true; mViewCountdown.setTextColor(Color.RED); mViewCountdown.setTypeface(Typeface.DEFAULT_BOLD); mViewCountdown.clearAnimation(); mViewCountdown.startAnimation(mAnimationTimerAlpha); } if (mSecondsTextValue != mTimeLeftInMillis / 1000) { mViewCountdown.setText( String.format("%d:%02d", TimeUnit.MILLISECONDS.toMinutes(mTimeLeftInMillis), TimeUnit.MILLISECONDS.toSeconds(mTimeLeftInMillis))); mSecondsTextValue = mTimeLeftInMillis / 1000; } if (mTimeLeftInMillis == 0) { finished(); } } } /** * Shut down the count down timer. * Cancel all pending animations and display the 'play again' screen. */ private void finished() { mViewCountdown.clearAnimation(); animationStarted = false; mViewCountdown.setTextColor(Color.WHITE); mViewCountdown.setTypeface(Typeface.DEFAULT); if (mViewPlayAgainBackground.getVisibility() != View.VISIBLE && !wasPaused) { wasPaused = true; submitScore(MatchingGameConstants.LEADERBOARDS_GUMBALL, mMatchScore); if (mBackgroundMusic != null) { mBackgroundMusic.stop(); mBackgroundMusic.release(); mBackgroundMusic = null; } mViewPlayAgainScore.setText(String.valueOf(mMatchScore)); mViewPlayAgainBackground.startAnimation(mAnimationPlayAgainBackground); mViewPlayAgainMain.startAnimation(mAnimationPlayAgainMain); mViewPlayAgainBackground.setVisibility(View.VISIBLE); mViewPlayAgainMain.setVisibility(View.VISIBLE); mViewMainMenuButton.setVisibility(View.VISIBLE); mViewInviteButton.setVisibility(View.VISIBLE); setSignInButtonVisibility(true); mSoundPool.play(mSoundGameOver, .2f, .2f, 0, 0, 1.0f); } cancel(); } } /** * Pause the game when the back key is pressed. */ public void onBackKeyPressed() { if (mViewPlayAgainMain.getVisibility() == View.VISIBLE) { returnToBackClass(); } else { if (mViewPauseButton.getVisibility() != View.GONE) {// check if already handled pauseGame(); } else { // Exit and return to previous Activity. returnToBackClass(); } } } /** * Pause the game and display the pause game screen. */ private void pauseGame() { mViewPauseButton.setVisibility(View.GONE); mViewPlayButton.setVisibility(View.VISIBLE); if (mCountDownTimer != null) { mCountDownTimer.cancel(); wasPaused = true; } mViewMatchPauseOverlay.setVisibility(View.VISIBLE); mViewCancelBar.setVisibility(View.VISIBLE); SensorManager sensorManager = (SensorManager) getActivity() .getSystemService(Activity.SENSOR_SERVICE); sensorManager.unregisterListener(this); if (Utils.hasKitKat()) { ImmersiveModeHelper.setImmersiveStickyWithActionBar(getActivity().getWindow()); } } /** * Continue the paused game. * Restart the countdown timer and hide the pause game screen. */ private void unPauseGame() { mViewPauseButton.setVisibility(View.VISIBLE); mViewPlayButton.setVisibility(View.GONE); mViewMatchPauseOverlay.setVisibility(View.GONE); mViewCancelBar.setVisibility(View.GONE); mCountDownTimer = new GameCountdown(mFramesPerSecond, mTimeLeftInMillis); mCountDownTimer.start(); mGameView.setGameCountDown(mCountDownTimer); wasPaused = false; SensorManager sensorManager = (SensorManager) getActivity() .getSystemService(Activity.SENSOR_SERVICE); Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); if (sensor != null) { sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_GAME); } if (Utils.hasKitKat()) { ImmersiveModeHelper.setImmersiveSticky(getActivity().getWindow()); } } /** * Submit score to play games services */ private void submitScore(int resId, int score) { PlayGamesActivity act = Utils.getPlayGamesActivity(this); if (act != null) { act.postSubmitScore(resId, score); } } /** * Hide the instructions and mark them as viewed. */ private class HideInstructionsRunnable implements Runnable { @Override public void run() { mDrawableTransition.stop(); wasPaused = false; mViewInstructions.setVisibility(View.GONE); Editor edit = mSharedPreferences.edit(); edit.putBoolean(MatchingGameConstants.GUMBALL_INSTRUCTIONS_VIEWED, true); edit.apply(); } } }