Objective: To create a mobile game: CannonGame App
1. Create a new Android Project called CannonGame with package mdad.game and the activity name as CannonController.
PS: For those using the latest version of Android Studio, CannonController.java refers to MainActivity.java
2. Open the strings.xml file and add the following strings: (DO NOT modify the ones in italics)
<r <resources> <string name="app_name">CannonGame</string> <string name="hello_world">Hello world!</string> <string name="action_settings">Settings</string> <string name="results_format">Shots fired: %1$d\nTotal time: %2$.1f</string> <string name="reset_game">Reset Game</string> <string name="win">You win!</string> <string name="lose">You lose!</string> <string name="time_remaining_format">Time remaining: %.1f seconds</string> </resources>
3. Add the highlighted codes in the layout activity_cannon_controller.xml or activity_main.xml
PS: For those using the latest version of Android Studio, activity_cannon_controller.xml refers to activity_main.xml
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.example.cannonbalgame2.CannonView android:background="@android:color/transparent"
android:id="@+id/cannonView" android:layout_height="match_parent"
android:layout_width="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"/>
</androidx.constraintlayout.widget.ConstraintLayout>
activity_cannon_controller.xml
<? <?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <mdad.game.CannonView android:background="@android:color/transparent" android:id="@+id/cannonView" android:layout_height="match_parent" android:layout_width="match_parent" xmlns:android="http://schemas.android.com/apk/res/android"/> </android.support.constraint.ConstraintLayout>
Take note of this line:
<mdad.game.CannonView android:background="@android:color/transparent"
Check if the package name is correct. If your package name is game.mdad.cannonball
then it should be
<game.mdad.cannonball.CannonView android:background="@android:color/transparent"
4. Create new folder raw in res folder and drag the 3 sound (.wav) files into it.
The sound files can be downloaded here
5. Create a new Java class CannonModel, in the same package, as shown: (this is the class to create lines and other shapes). Fill in the missing codes (non-italics portion)
pa package mdad.game; import android.graphics.Point; public class CannonModel { public Point start; // starting Point public Point end; // ending Point // default constructor initializes Points to (0, 0) public CannonModel() { start = new Point(0, 0); // start Point end = new Point(0, 0); // end Point } // end constructor }// end class
6. Create a new class called CannonView (same package name).
pa package mdad.game; import java.util.HashMap; import java.util.Map; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Point; import android.graphics.Rect; import android.media.AudioManager; import android.media.SoundPool; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; public class CannonView extends SurfaceView implements SurfaceHolder.Callback { private CannonThread cannonThread; // controls the game loop private Activity activity; // to display Game Over dialog in GUI thread private boolean dialogIsDisplayed = false; // constants for game play public static final int TARGET_PIECES = 7; // sections in the target public static final int MISS_PENALTY = 2; // seconds deducted on a miss public static final int HIT_REWARD = 3; // seconds added on a hit // variables for the game loop and tracking statistics private boolean gameOver; // is the game over? private double timeLeft; // the amount of time left in seconds private int shotsFired; // the number of shots the user has fired private double totalElapsedTime; // the number of seconds elapsed // variables for the blocker and target private CannonModel blocker; // start and end points of the blocker private int blockerDistance; // blocker distance from left private int blockerBeginning; // blocker distance from top private int blockerEnd; // blocker bottom edge distance from top private int initialBlockerVelocity; // initial blocker speed multiplier private float blockerVelocity; // blocker speed multiplier during game private CannonModel target; // start and end points of the target private int targetDistance; // target distance from left private int targetBeginning; // target distance from top private double pieceLength; // length of a target piece private int targetEnd; // target bottom's distance from top private int initialTargetVelocity; // initial target speed multiplier private float targetVelocity; // target speed multiplier during game private int lineWidth; // width of the target and blocker private boolean[] hitStates; // is each target piece hit? private int targetPiecesHit; // number of target pieces hit (out of 7) // variables for the cannon and cannonball private Point cannonball; // cannonball image's upper-left corner private int cannonballVelocityX; // cannonball's x velocity private int cannonballVelocityY; // cannonball's y velocity private boolean cannonballOnScreen; // is the cannonball on the screen private int cannonballRadius; // cannonball radius private int cannonballSpeed; // cannonball speed private int cannonBaseRadius; // cannon base radius private int cannonLength; // cannon barrel length private Point barrelEnd; // the endpoint of the cannon's barrel private int screenWidth; // width of the screen private int screenHeight; // height of the screen // constants and variables for managing sounds private static final int TARGET_SOUND_ID = 0; private static final int CANNON_SOUND_ID = 1; private static final int BLOCKER_SOUND_ID = 2; private SoundPool soundPool; // plays sound effects private Map<Integer, Integer> soundMap; // maps IDs to SoundPool // Paint variables used when drawing each item on the screen private Paint textPaint; // Paint used to draw text private Paint cannonballPaint; // Paint used to draw the cannonball private Paint cannonPaint; // Paint used to draw the cannon private Paint blockerPaint; // Paint used to draw the blocker private Paint targetPaint; // Paint used to draw the target private Paint backgroundPaint; // Paint used to clear the drawing area // public constructor public CannonView(Context context, AttributeSet attrs) { super(context, attrs); // call super's constructor activity = (Activity) context; // register SurfaceHolder.Callback listener getHolder().addCallback(this); // initialize Lines and points representing game items blocker = new CannonModel(); // create the blocker as a Line target = new CannonModel(); // create the target as a Line cannonball = new Point(); // create the cannonball as a point // initialize hitStates as a boolean array hitStates = new boolean[TARGET_PIECES]; // initialize SoundPool to play the app's three sound effects soundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0); // create Map of sounds and pre-load sounds soundMap = new HashMap<Integer, Integer>(); // create new HashMap soundMap.put(TARGET_SOUND_ID, soundPool.load(context, R.raw.target_hit, 1)); soundMap.put(CANNON_SOUND_ID, soundPool.load(context, R.raw.cannon_fire, 1)); soundMap.put(BLOCKER_SOUND_ID, soundPool.load(context, R.raw.blocker_hit, 1)); // construct Paints for drawing text, cannonball, cannon, // blocker and target; these are configured in method onSizeChanged textPaint = new Paint(); // Paint for drawing text cannonPaint = new Paint(); // Paint for drawing the cannon cannonballPaint = new Paint(); // Paint for drawing a cannonball blockerPaint = new Paint(); // Paint for drawing the blocker targetPaint = new Paint(); // Paint for drawing the target backgroundPaint = new Paint(); // Paint for drawing the target } // end CannonView constructor // called by surfaceChanged when the size of the SurfaceView changes, // such as when it's first added to the View hierarchy @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); screenWidth = w; // store the width screenHeight = h; // store the height cannonBaseRadius = h / 18; // cannon base radius 1/18 screen height cannonLength = w / 8; // cannon length 1/8 screen width cannonballRadius = w / 36; // cannonball radius 1/36 screen width cannonballSpeed = w * 3 / 2; // cannonball speed multiplier lineWidth = w / 24; // target and blocker 1/24 screen width // configure instance variables related to the blocker blockerDistance = w * 5 / 8; // blocker 5/8 screen width from left blockerBeginning = h / 8; // distance from top 1/8 screen height blockerEnd = h * 3 / 8; // distance from top 3/8 screen height initialBlockerVelocity = h / 2; // initial blocker speed multiplier blocker.start = new Point(blockerDistance, blockerBeginning); blocker.end = new Point(blockerDistance, blockerEnd); // configure instance variables related to the target targetDistance = w * 7 / 8; // target 7/8 screen width from left targetBeginning = h / 8; // distance from top 1/8 screen height targetEnd = h * 7 / 8; // distance from top 7/8 screen height pieceLength = (targetEnd - targetBeginning) / TARGET_PIECES; initialTargetVelocity = -h / 4; // initial target speed multiplier target.start = new Point(targetDistance, targetBeginning); target.end = new Point(targetDistance, targetEnd); // endpoint of the cannon's barrel initially points horizontally barrelEnd = new Point(cannonLength, h / 2); // configure Paint objects for drawing game elements textPaint.setTextSize(w / 20); // text size 1/20 of screen width textPaint.setAntiAlias(true); // smoothes the text cannonPaint.setStrokeWidth(lineWidth * 1.5f); // set line thickness blockerPaint.setStrokeWidth(lineWidth); // set line thickness targetPaint.setStrokeWidth(lineWidth); // set line thickness backgroundPaint.setColor(Color.WHITE); // set background color newGame(); // set up and start a new game } // end method onSizeChanged // reset all the screen elements and start a new game public void newGame() { // set every element of hitStates to false--restores target pieces for (int i = 0; i < TARGET_PIECES; ++i) hitStates[i] = false; targetPiecesHit = 0; // no target pieces have been hit blockerVelocity = initialBlockerVelocity; // set initial velocity targetVelocity = initialTargetVelocity; // set initial velocity timeLeft = 10; // start the countdown at 10 seconds cannonballOnScreen = false; // the cannonball is not on the screen shotsFired = 0; // set the initial number of shots fired totalElapsedTime = 0.0; // set the time elapsed to zero blocker.start.set(blockerDistance, blockerBeginning); blocker.end.set(blockerDistance, blockerEnd); target.start.set(targetDistance, targetBeginning); target.end.set(targetDistance, targetEnd); if (gameOver) { gameOver = false; // the game is not over cannonThread = new CannonThread(getHolder()); cannonThread.start(); } // end if } // end method newGame // called repeatedly by the CannonThread to update game elements private void updatePositions(double elapsedTimeMS) { double interval = elapsedTimeMS / 1000.0; // convert to seconds if (cannonballOnScreen) // if there is currently a shot fired { // update cannonball position cannonball.x += interval * cannonballVelocityX; cannonball.y += interval * cannonballVelocityY; // check for collision with blocker if (cannonball.x + cannonballRadius > blockerDistance && cannonball.x - cannonballRadius < blockerDistance && cannonball.y + cannonballRadius > blocker.start.y && cannonball.y - cannonballRadius < blocker.end.y) { cannonballVelocityX *= -1; // reverse cannonball's direction timeLeft -= MISS_PENALTY; // penalize the user // play blocker sound soundPool.play(soundMap.get(BLOCKER_SOUND_ID), 1, 1, 1, 0, 1f); } // end if // check for collisions with left and right walls else if (cannonball.x + cannonballRadius > screenWidth || cannonball.x - cannonballRadius < 0) cannonballOnScreen = false; // remove cannonball from screen // check for collisions with top and bottom walls else if (cannonball.y + cannonballRadius > screenHeight || cannonball.y - cannonballRadius < 0) cannonballOnScreen = false; // make the cannonball disappear // check for cannonball collision with target else if (cannonball.x + cannonballRadius > targetDistance && cannonball.x - cannonballRadius < targetDistance && cannonball.y + cannonballRadius > target.start.y && cannonball.y - cannonballRadius < target.end.y) { // determine target section number (0 is the top) int section = (int) ((cannonball.y - target.start.y) / pieceLength); // check if the piece hasn't been hit yet if ((section >= 0 && section < TARGET_PIECES) && !hitStates[section]) { hitStates[section] = true; // section was hit cannonballOnScreen = false; // remove cannonball timeLeft += HIT_REWARD; // add reward to remaining time // play target hit sound soundPool.play(soundMap.get(TARGET_SOUND_ID), 1, 1, 1, 0, 1f); // if all pieces have been hit if (++targetPiecesHit == TARGET_PIECES) { cannonThread.setRunning(false); showGameOverDialog(R.string.win); // show winning dialog gameOver = true; // the game is over } // end if } // end if } // end else if } // end if // update the blocker's position double blockerUpdate = interval * blockerVelocity; blocker.start.y += blockerUpdate; blocker.end.y += blockerUpdate; // update the target's position double targetUpdate = interval * targetVelocity; target.start.y += targetUpdate; target.end.y += targetUpdate; // if the blocker hit the top or bottom, reverse direction if (blocker.start.y < 0 || blocker.end.y > screenHeight) blockerVelocity *= -1; // if the target hit the top or bottom, reverse direction if (target.start.y < 0 || target.end.y > screenHeight) targetVelocity *= -1; timeLeft -= interval; // subtract from time left // if the timer reached zero if (timeLeft <= 0.0) { timeLeft = 0.0; gameOver = true; // the game is over cannonThread.setRunning(false); showGameOverDialog(R.string.lose); // show the losing dialog } // end if } // end method updatePositions // fires a cannonball public void fireCannonball(MotionEvent event) { if (cannonballOnScreen) // if a cannonball is already on the screen return; // do nothing double angle = alignCannon(event); // get the cannon barrel's angle // move the cannonball to be inside the cannon cannonball.x = cannonballRadius; // align x-coordinate with cannon cannonball.y = screenHeight / 2; // centers ball vertically // get the x component of the total velocity cannonballVelocityX = (int) (cannonballSpeed * Math.sin(angle)); // get the y component of the total velocity cannonballVelocityY = (int) (-cannonballSpeed * Math.cos(angle)); cannonballOnScreen = true; // the cannonball is on the screen ++shotsFired; // increment shotsFired // play cannon fired sound soundPool.play(soundMap.get(CANNON_SOUND_ID), 1, 1, 1, 0, 1f); } // end method fireCannonball // aligns the cannon in response to a user touch public double alignCannon(MotionEvent event) { // get the location of the touch in this view Point touchPoint = new Point((int) event.getX(), (int) event.getY()); // compute the touch's distance from center of the screen // on the y-axis double centerMinusY = (screenHeight / 2 - touchPoint.y); double angle = 0; // initialize angle to 0 // calculate the angle the barrel makes with the horizontal if (centerMinusY != 0) // prevent division by 0 angle = Math.atan((double) touchPoint.x / centerMinusY); // if the touch is on the lower half of the screen if (touchPoint.y > screenHeight / 2) angle += Math.PI; // adjust the angle // calculate the endpoint of the cannon barrel barrelEnd.x = (int) (cannonLength * Math.sin(angle)); barrelEnd.y = (int) (-cannonLength * Math.cos(angle) + screenHeight / 2); return angle; // return the computed angle } // end method alignCannon // draws the game to the given Canvas public void drawGameElements(Canvas canvas) { // clear the background canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), backgroundPaint); // display time remaining canvas.drawText(getResources().getString( R.string.time_remaining_format, timeLeft), 30, 50, textPaint); // if a cannonball is currently on the screen, draw it if (cannonballOnScreen) canvas.drawCircle(cannonball.x, cannonball.y, cannonballRadius, cannonballPaint); // draw the cannon barrel canvas.drawLine(0, screenHeight / 2, barrelEnd.x, barrelEnd.y, cannonPaint); // draw the cannon base canvas.drawCircle(0, (int) screenHeight / 2, (int) cannonBaseRadius, cannonPaint); // draw the blocker canvas.drawLine(blocker.start.x, blocker.start.y, blocker.end.x, blocker.end.y, blockerPaint); Point currentPoint = new Point(); // start of current target section // initialize curPoint to the starting point of the target currentPoint.x = target.start.x; currentPoint.y = target.start.y; // draw the target for (int i = 1; i <= TARGET_PIECES; ++i) { // if this target piece is not hit, draw it if (!hitStates[i - 1]) { // alternate coloring the pieces yellow and blue if (i % 2 == 0) targetPaint.setColor(Color.YELLOW); else targetPaint.setColor(Color.BLUE); canvas.drawLine(currentPoint.x, currentPoint.y, target.end.x, (int) (currentPoint.y + pieceLength), targetPaint); } // end if // move curPoint to the start of the next piece currentPoint.y += pieceLength; } // end for } // end method drawGameElements // display an AlertDialog when the game ends private void showGameOverDialog(int messageId) { // create a dialog displaying the given String final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext()); dialogBuilder.setTitle(getResources().getString(messageId)); dialogBuilder.setCancelable(false); // display number of shots fired and total time elapsed // add the additional targetPiecesHit variable dialogBuilder.setMessage(getResources().getString( R.string.results_format, shotsFired, totalElapsedTime)); dialogBuilder.setPositiveButton( R.string.reset_game, new DialogInterface.OnClickListener() { // called when "Reset Game" Button is pressed @Override public void onClick(DialogInterface dialog, int which) { dialogIsDisplayed = false; newGame(); // set up and start a new game } // end method onClick } // end anonymous inner class ); // end call to setPositiveButton activity.runOnUiThread( new Runnable() { public void run() { dialogIsDisplayed = true; dialogBuilder.show(); // display the dialog } // end method run } // end Runnable ); // end call to runOnUiThread } // end method showGameOverDialog // stops the game public void stopGame() { if (cannonThread != null) cannonThread.setRunning(false); } // end method stopGame // releases resources; called by CannonGame's onDestroy method public void releaseResources() { soundPool.release(); // release all resources used by the SoundPool soundPool = null; } // end method releaseResources // called when surface changes size @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } // end method surfaceChanged // called when surface is first created @Override public void surfaceCreated(SurfaceHolder holder) { if (!dialogIsDisplayed) { cannonThread = new CannonThread(holder); cannonThread.setRunning(true); cannonThread.start(); // start the game loop thread } // end if } // end method surfaceCreated // called when the surface is destroyed @Override public void surfaceDestroyed(SurfaceHolder holder) { // ensure that thread terminates properly boolean retry = true; cannonThread.setRunning(false); while (retry) { try { cannonThread.join(); retry = false; } // end try catch (InterruptedException e) { } // end catch } // end while } // end method surfaceDestroyed // Thread subclass to control the game loop private class CannonThread extends Thread { private SurfaceHolder surfaceHolder; // for manipulating canvas private boolean threadIsRunning = true; // running by default // initializes the surface holder public CannonThread(SurfaceHolder holder) { surfaceHolder = holder; setName("CannonThread"); } // end constructor // changes running state public void setRunning(boolean running) { threadIsRunning = running; } // end method setRunning // controls the game loop @Override public void run() { Canvas canvas = null; // used for drawing long previousFrameTime = System.currentTimeMillis(); while (threadIsRunning) { try { canvas = surfaceHolder.lockCanvas(null); // lock the surfaceHolder for drawing synchronized(surfaceHolder) { long currentTime = System.currentTimeMillis(); double elapsedTimeMS = currentTime - previousFrameTime; totalElapsedTime += elapsedTimeMS / 1000.00; updatePositions(elapsedTimeMS); // update game state drawGameElements(canvas); // draw previousFrameTime = currentTime; // update previous time } // end synchronized block } // end try finally { if (canvas != null) surfaceHolder.unlockCanvasAndPost(canvas); } // end finally } // end while } // end method run } // end nested class CannonThread } // end class CannonView
7.1. In the CannonController class, add the following statements and methods in the appropriate places.
PS: For those using the latest version of Android Studio, CannonController.java refers to MainActivity.java
Pls make changes according to the picture below esp on the import statement and extends the correct superclass
pa package mdad.game; import android.app.Activity; import android.media.AudioManager; import android.os.Bundle; import android.view.GestureDetector; import android.view.GestureDetector.SimpleOnGestureListener; import android.view.MotionEvent; public class CannonController extends Activity { private GestureDetector gestureDetector; // listens for double taps private CannonView cannonView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_cannon_controller); cannonView = (CannonView) findViewById(R.id.cannonView); // get the CannonView gestureDetector = new GestureDetector(this, gestureListener); setVolumeControlStream(AudioManager.STREAM_MUSIC); } SimpleOnGestureListener gestureListener = new SimpleOnGestureListener() { // called when the user double taps the screen public boolean onDoubleTap(MotionEvent e) { cannonView.fireCannonball(e); return true; } // }; // end gestureListener public void onPause() { super.onPause(); cannonView.stopGame(); }
protected void onDestroy() { super.onDestroy(); cannonView.releaseResources(); }
public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); // the user user touched the screen or dragged along the screen if (action == MotionEvent.ACTION_DOWN ||action == MotionEvent.ACTION_MOVE) { cannonView.alignCannon(event); } return gestureDetector.onTouchEvent(event); } }
LAB 5: Mobile Device Application (Part 2)
To increase the play
time to 30 sec:
In CannonView class:
newGame() method:
timeLeft = 30; // change the countdown to 30 seconds
instead of 10
(1)
To add
a hits value that will show the number of
targets destroyed at the end of the game:
targets destroyed at the end of the game:
In
CannonView class:
showGameOverDialog() method:
// add the additional targetPiecesHit variable
dialogBuilder.setMessage(getResources().getString(
R.string.results_format, shotsFired, totalElapsedTime, targetPiecesHit));
In the strings.xml
file:
Add the additional highlighted format string to
results_format
<string name="results_format">Shots fired:
%1$d\nTotal time: %2$.1f\nHits: %3$d\nDeveloped by Your Name </string>
How to distribute your game to your friends or the community?
You can generate signed APK and send email to your friends by following the steps below:
Apk location in New Android Studio
How to run on your phone via USB cable and USB debug mode?
No comments:
Post a Comment
Note: only a member of this blog may post a comment.