Das Retro-Spiel Pong als Android-App

1. Aufgabe

Über das Retro-Spiel Pong muss man nicht mehr viel schreiben, unter www.ponggame.org ist eigentlich alles gesagt bzw. geschrieben worden. Sollte dort tatsächlich noch etwas fehlen, so könnte man auch hierher hierher noch einen Blick werfen: unter der Rubrik Windows Presentation Foundation findet sich selbst in diesem Blog eine einfache Umsetzung dieses Spiels. Nichtsdestotrotz: Was liegt im Zeitalter von Smartphone-Apps näher, eine Variante des Pong-Spiels als Android-App vorzustellen? Denkbar wäre zum Beispiel, dass man auf einem Startscreen (Eröffnungs-Aktivität) den gewünschten Modus des Spiels anwählen kann:

Startscreen der Pong-App.

Abbildung 1. Startscreen der Pong-App.


In Abbildung 1 finden wir drei Spielevarianten „Single Player“, „Multi Player“ und „Demo“ vor. Je nach Auswahl geht es dann mit einer zweiten Android-Aktivität weiter, auf der die beiden Pong-Spieler gegeneinander antreten können:

Spielfläche der Pong-App.

Abbildung 2. Spielfläche der Pong-App.


Nicht vorhanden, aber zur selbstständigen Weiterarbeit empfohlen, die Ergänzung einer Rangliste (Highscore-Liste). Diese könnte in einer einfachen bzw. auch aufwandsarmen Realisierung alle Spiele (und Spieler) lokal auf dem Device verwalten. In einer anspruchsvolleren Realisierung könnte man dann eine Integration der Google Play Services angehen. Die Rangliste wäre auf diese Weise zentral im Netz abgelegt, es können sich alle Spieler, die die App geladen haben, miteinander messen.

2. Lösung

Quellcode: Siehe auch github.com/peterloos/Android_Pong.

Der komplexeste Teil in der Realisierung des Pong-Spiels liegt meines Erachtens im Modus „Multi Player“ verborgen, und hier speziell im Bereich der Auswertung von Multi-Touch Gestiken. In der Android Online Developers Dokumentation finden sich hierzu wertvolle Hinweise in den Abschnitten „Handling Multi-Touch Gestures“ und im weiteren Verlauf unter „Track Multiple Pointers“ vor. Mit dem Begriff Pointer bezeichnet Android an dieser Stelle den Verlauf einer einzelnen Berührungsgestik, also im Prinzip das Berühren, Bewegen und Loslassen des Bildschirms mit dem Finger. Android kann bis zu fünf gleichzeitig durchgeführte Berührungsgestiken verwalten. Speziell im Umfeld von Pong besteht die Kunst also darin,

  • ein oder zwei Berührungsgestiken zu erkennen und diese dem linken (ersten) und/oder rechten (zweiten) Spieler zuzuordnen sowie

  • weitere Berührungsgestiken, die durch versehentliches Berühren des Bildschirms entstehen, zu ignorieren.

Aber zunächst zu den Grundlagen des Lösungsansatzes. Um zentrale Paradigmen der objekt-orientierten Programmierung in die Lösung mit einfließen zu lassen, haben wir die grafischen Primitive Ball und Schläger auf folgende Konstrukte aufgeteilt:

  • Schnittstelle Shape – Beschreibung einer allgemeinen Pong-Figur.

  • Abstrakte Klasse PongShape – Unvollständige Realisierung der Shape-Schnittstelle.

  • Konkrete Klasse Ball – Realisierung des Pong-Spielballs.

  • Konkrete Klasse Paddle – Realisierung eines Schlägers.

Zwei sehr elementare Aufzählungstypen BallDirection und PaddleType begleiten die Implementierung:

01: public enum BallDirection {
02:     Leftwards, Rightwards
03: }

bzw.

01: public enum PaddleType {
02:     LeftPaddle, RightPaddle
03: }

Ebenfalls in der Klassenbibliothek nicht vorhanden – bzw. nicht in der von mir angestrebten Android-Version – ist ein Klassentyp für Größenangaben, wie er in den meisten anderen Klassenbibliotheken anzutreffen ist:

01: public class Size {
02: 
03:     private int width;
04:     private int height;
05: 
06:     public Size(int width, int height) {
07:         this.setWidth(width);
08:         this.setHeight(height);
09:     }
10: 
11:     public int getWidth() {
12:         return this.width;
13:     }
14: 
15:     public void setWidth(int width) {
16:         this.width = (width >= 0) ? width : 0;
17:     }
18: 
19:     public int getHeight() {
20:         return this.height;
21:     }
22: 
23:     public void setHeight(int height) {
24:         this.height = (height >= 0) ? height : 0;
25:     }
26: }

Damit kommen wir zu den bereits angesprochenen Schnittstellen- und Klassentypen in Listing 1, Listing 2, Listing 3 und Listing 4:

01: public interface Shape {
02: 
03:     /**
04:      * center of shape
05:      * paddle: upper left corner
06:      * ball:   center point
07:      */
08:     Point getOrigin();
09:     void setOrigin(Point p);
10:     int getOriginX();
11:     int getOriginY();
12:     void moveOrigin(int dx, int dy);
13: 
14:     /* size of surrounding view */
15:     void setParentSize(Size size);
16:     int getParentWidth();
17:     int getParentHeight();
18: 
19:     /* background color of shape */
20:     int getColor();
21:     void setColor(int color);
22: 
23:     /* drawing method */
24:     void draw(Canvas canvas);
25: }

Beispiel 1. Datei Shape.java: Schnittstelle Shape .


01: abstract public class PongShape implements Shape {
02: 
03:     private Point origin;
04:     private int color;
05:     private Size size;
06: 
07:     public PongShape () {
08:         this.origin = new Point();
09:         this.color = Color.GRAY;
10:         this.size = new Size(0, 0);
11:     }
12: 
13:     @Override
14:     public Point getOrigin() {
15:         return this.origin;
16:     }
17: 
18:     @Override
19:     public void setOrigin(Point origin) {
20:         this.origin = origin;
21:     }
22: 
23:     @Override
24:     public int getOriginX() {
25:         return this.origin.x;
26:     }
27: 
28:     @Override
29:     public int getOriginY() {
30:         return this.origin.y;
31:     }
32: 
33:     @Override
34:     public void moveOrigin (int dx, int dy) {
35:         this.origin.offset(dx, dy);
36:     }
37: 
38:     @Override
39:     public void setParentSize(Size size) {
40:         this.size = size;
41:     }
42: 
43:     @Override
44:     public int getParentWidth() {
45:         return this.size.getWidth();
46:     }
47: 
48:     @Override
49:     public int getParentHeight() {
50:         return this.size.getHeight();
51:     }
52: 
53:     @Override
54:     public int getColor() {
55:         return this.color;
56:     }
57: 
58:     @Override
59:     public void setColor(int color) {
60:         this.color = color;
61:     }
62: 
63:     @Override
64:     public abstract void draw(Canvas canvas);
65: }

Beispiel 2. Datei PongShape.java: Abstrakte Klasse PongShape.


01: public class Ball extends PongShape {
02: 
03:     protected Paint ballPaint;
04:     protected BallDirection direction;
05:     protected float xOfs;
06:     protected float yOfs;
07: 
08:     // c'tor
09:     public Ball() {
10: 
11:         this.setColor(Globals.BallColor);
12: 
13:         this.ballPaint = new Paint();
14:         this.ballPaint.setColor(this.getColor());
15: 
16:         this.setOrigin(new Point(0, 0));
17: 
18:         this.xOfs = Globals.BallSpeed;
19:         this.yOfs = -Globals.BallSpeed;
20:         this.direction = BallDirection.Rightwards;
21:     }
22: 
23:     // getter/setter
24:     public BallDirection getDirection() {
25:         return this.direction;
26:     }
27: 
28:     // public interface
29:     public void init() {
30: 
31:         /* position ball in center of view */
32:         this.setOrigin(new Point(this.getParentWidth() / 2, this.getParentHeight() / 2));
33:     }
34: 
35:     public void move() {
36: 
37:         if (this.getOriginY() <= Globals.BallRadius) {
38:             this.yOfs *= -1;
39:         } else if (this.getOriginY() >= this.getParentHeight() - Globals.BallRadius) {
40:             this.yOfs *= -1;
41:         }
42: 
43:         // this.origin.offset((int) this.xOfs, (int) this.yOfs);
44:         this.moveOrigin((int) this.xOfs, (int) this.yOfs);
45:     }
46: 
47:     public void changeDirection() {
48:         this.xOfs *= -1;
49:         this.direction = (this.direction == BallDirection.Leftwards) ?
50:                 BallDirection.Rightwards : BallDirection.Leftwards;
51:     }
52: 
53:     public boolean ballIsInCriticalArea(int x) {
54: 
55:         if (x < Globals.PaddleLeft + Globals.PaddleWidth
56:                 + Globals.BallRadius + Globals.ToleranceDistance) {
57:             return true;
58:         } else if (x > this.getParentWidth() - Globals.PaddleLeft - Globals.PaddleWidth
59:                 - Globals.BallRadius - Globals.ToleranceDistance) {
60:             return true;
61:         } else {
62:             return false;
63:         }
64:     }
65: 
66:     @Override
67:     public void draw(Canvas canvas) {
68:         canvas.drawCircle(
69:             this.getOriginX(),
70:             this.getOriginY(),
71:             Globals.BallRadius,
72:             this.ballPaint);
73:     }
74: }

Beispiel 3. Datei Ball.java: Konkrete Klasse Ball.


001: public class Paddle extends PongShape {
002: 
003:     private PaddleType type;
004:     private Rect paddleRect;
005:     private Rect hitBallRect;
006:     private Paint paddlePaint;
007: 
008:     public Paddle(PaddleType type) {
009: 
010:         this.type = type;
011:         this.setColor(Globals.PaddleColor);
012: 
013:         this.paddleRect = new Rect(
014:             Globals.PaddleLeft,
015:             Globals.PaddleTop,
016:             Globals.PaddleLeft + Globals.PaddleWidth,
017:             Globals.PaddleTop + Globals.PaddleHeight
018:         );
019:         this.hitBallRect = new Rect();
020:         this.paddlePaint = new Paint();
021:         this.paddlePaint.setColor(this.getColor());
022:     }
023: 
024:     public void init() {
025: 
026:         // compute new position of paddle
027:         int x = (this.type == PaddleType.LeftPaddle) ?
028:             Globals.PaddleLeft :
029:             this.getParentWidth() - Globals.PaddleLeft - Globals.PaddleWidth;
030:         int y = (this.getParentHeight() - Globals.PaddleHeight) / 2;
031: 
032:         // adjust paddle's origin
033:         this.setOrigin(new Point(x, y));
034: 
035:         // adjust paddle's rectangle values
036:         this.paddleRect.left = this.getOriginX();
037:         this.paddleRect.right = this.getOriginX() + Globals.PaddleWidth;
038:         this.paddleRect.top = this.getOriginY();
039:         this.paddleRect.bottom = this.paddleRect.top + Globals.PaddleHeight;
040:     }
041: 
042:     public void move(int y) {
043: 
044:         // is distance between touch event and paddle small
045:         if (Math.abs(this.paddleRect.centerY() - y) < Globals.PaddleScrollDistance) {
046:             // small distance
047:             this.paddleRect.offset(0, y - this.paddleRect.centerY());
048:         } else {
049:             // large distance
050:             int dy = (this.paddleRect.centerY() < y) ?
051:                 Globals.PaddleScrollDistance :
052:                 -Globals.PaddleScrollDistance;
053: 
054:             this.paddleRect.offset(0, dy);
055:         }
056: 
057:         // take care of upper and lower border of surrounding container
058:         int left = (this.type == PaddleType.LeftPaddle) ?
059:             Globals.PaddleLeft :
060:             this.getParentWidth() - Globals.PaddleLeft - Globals.PaddleWidth;
061: 
062:         int maxTop = this.getParentHeight() - Globals.PaddleTop - Globals.PaddleHeight;
063: 
064:         if (this.paddleRect.top < Globals.PaddleTop) {
065:             this.paddleRect.offsetTo(left, Globals.PaddleTop);
066:         }
067: 
068:         if (this.paddleRect.top > maxTop) {
069:             this.paddleRect.offsetTo(left, maxTop);
070:         }
071:     }
072: 
073:     public boolean hitsBall(Point point) {
074: 
075:         // hitting rectangle should be a bit smaller than the ball
076:         int offset = Globals.BallRadius - 2;
077: 
078:         if (this.type == PaddleType.LeftPaddle) {
079: 
080:             if (point.x < Globals.PaddleLeft + Globals.PaddleWidth
081:                     + Globals.BallRadius + Globals.ToleranceDistance) {
082: 
083:                 this.hitBallRect.left = 0;
084:                 this.hitBallRect.right = point.x + offset;
085:                 this.hitBallRect.top = point.y - offset;
086:                 this.hitBallRect.bottom = point.y + offset;
087: 
088:                 if (Rect.intersects(this.hitBallRect, this.paddleRect))
089:                     return true;
090:             }
091:         } else if (this.type == PaddleType.RightPaddle) {
092: 
093:             if (point.x > this.getParentWidth() - Globals.PaddleLeft
094:                     - Globals.PaddleWidth - Globals.BallRadius - Globals.ToleranceDistance) {
095: 
096:                 this.hitBallRect.left = point.x - offset;
097:                 this.hitBallRect.right = Integer.MAX_VALUE;
098:                 this.hitBallRect.top = point.y - offset;
099:                 this.hitBallRect.bottom = point.y + offset;
100: 
101:                 if (Rect.intersects(this.hitBallRect, this.paddleRect))
102:                     return true;
103:             }
104:         }
105: 
106:         return false;
107:     }
108: 
109:     @Override
110:     public void draw(Canvas canvas) {
111:         canvas.drawRect(this.paddleRect, this.paddlePaint);
112:     }
113: }

Beispiel 4. Datei Paddle.java: Konkrete Klasse Paddle.


Es geht weiter in Richtung der Aktivitäten: Zahlreiche globale Variablen des Spiels fasst man am besten in einer separaten Klasse zusammen, sie lautet Globals.java:

01: public class Globals {
02: 
03:     // paddle constants
04:     public static final int PaddleTop = 20;
05:     public static final int PaddleLeft = 10;
06:     public static final int PaddleHeight = 100;
07:     public static final int PaddleWidth = 30;
08:     public static final int PaddleColor = Color.YELLOW;
09: 
10:     // ball constants
11:     public static final int BallRadius = 15;
12:     public static final int BallSpeed = 5;
13:     public static final int BallColor = Color.BLACK;
14: 
15:     // putExtra and getStringExtra support
16:     public static final String GameType = "de.peterloos.anotherpong.GAMETYPE";
17: 
18:     // miscellaneous constants
19:     public static final int ToleranceDistance = 5;  // should be similar to Ball.Speed
20:     public static final int PaddleScrollDistance = 5;
21:     public static final int BallFadingOutMaxSteps = 30;
22:     public static final int MaxPlays = 5;
23: }

Die verschiedenen Spielevarianten lassen sich am besten durch einen Aufzählungstyp beschreiben:

01: public enum GameType {
02:     SinglePlayerGame, MultiPlayerGame, DemoPlayerGame
03: }

Damit fehlen noch die zwei Aktivitäten StartActivity (siehe auch Abbildung 1) und PongActivity (siehe wiederum Abbildung 2). Mit der Klasse StartActivity geht es weiter in Listing 5:

01: public class StartActivity extends AppCompatActivity {
02: 
03:     @Override
04:     protected void onCreate(Bundle savedInstanceState) {
05:         super.onCreate(savedInstanceState);
06:         this.setContentView(R.layout.activity_start);
07: 
08:         Button buttonSinglePlayer = (Button) this.findViewById(R.id.button_single_player);
09:         Button buttonMultiPlayer = (Button) this.findViewById(R.id.button_multi_player);
10:         Button buttonDemoPlayer = (Button) this.findViewById(R.id.button_demo_player);
11: 
12:         if (buttonSinglePlayer != null) {
13:             buttonSinglePlayer.setOnClickListener(new View.OnClickListener() {
14:                 @Override
15:                 public void onClick(View v) {
16:                     Intent intent = new Intent(StartActivity.this, PongActivity.class);
17:                     intent.putExtra(Globals.GameType, "Single");
18:                     startActivity(intent);
19:                 }
20:             });
21:         }
22: 
23:         if (buttonMultiPlayer != null) {
24:             buttonMultiPlayer.setOnClickListener(new View.OnClickListener() {
25:                 @Override
26:                 public void onClick(View v) {
27:                     Intent intent = new Intent(StartActivity.this, PongActivity.class);
28:                     intent.putExtra(Globals.GameType, "Multi");
29:                     startActivity(intent);
30:                 }
31:             });
32:         }
33: 
34:         if (buttonDemoPlayer != null) {
35:             buttonDemoPlayer.setOnClickListener(new View.OnClickListener() {
36:                 @Override
37:                 public void onClick(View v) {
38:                     Intent intent = new Intent(StartActivity.this, PongActivity.class);
39:                     intent.putExtra(Globals.GameType, "Demo");
40:                     startActivity(intent);
41:                 }
42:             });
43:         }
44:     }
45: }

Beispiel 5. Datei StartActivity.java: Start-Aktivität der Android-App.


Die Aktivität zum eigentlichen Spielen besteht zum einen aus einer Ableitung der Klasse AppCompatActivity, also der Basisklasse für Aktivitäten, und heißt PongActivity. Die Funktionalität des animierten Bewegens von Ball und Schläger ist in ein benutzerdefiniertes Android-Steuerelement ausgelagert worden. Diese Klasse haben wir PongView genannt, sie leitet sich bezeichnenderweise von der Basisklasse View ab. Auf diese Weise lassen sich die etwas sensiblen Code-Anteile der App, zum Beispiel die Erkennung von Multi-Touch Gestiken, sehr schön isolieren und vom Rest der App abtrennen. Es folgen damit noch zwei Quellcodetexte, einer für die Klasse PongActivity (Listing 6) und abschließend das Steuerelement PongView (Listing 7):

01: public class PongActivity extends AppCompatActivity implements View.OnClickListener {
02: 
03:     private PongView pongView;
04:     private Button buttonStart;
05:     private Button buttonStop;
06: 
07:     @Override
08:     protected void onCreate(Bundle savedInstanceState) {
09:         super.onCreate(savedInstanceState);
10:         this.setContentView(R.layout.activity_pong);
11: 
12:         this.pongView = (PongView) this.findViewById(R.id.pong_view);
13:         this.buttonStart = (Button) this.findViewById(R.id.button_start);
14:         this.buttonStop = (Button) this.findViewById(R.id.button_stop);
15:         TextView textviewScoreboard = (TextView) this.findViewById(R.id.textview_gamescore);
16:         this.pongView.setScoreboardView(textviewScoreboard);
17: 
18:         this.buttonStart.setOnClickListener(this);
19:         this.buttonStop.setOnClickListener(this);
20: 
21:         // retrieve game mode
22:         Intent intent = this.getIntent();
23:         String mode = intent.getStringExtra(Globals.GameType);
24:         if (mode.equals("Single"))
25:             this.pongView.setGameType(GameType.SinglePlayerGame);
26:         else if (mode.equals("Multi"))
27:             this.pongView.setGameType(GameType.MultiPlayerGame);
28:         else if (mode.equals("Demo"))
29:             this.pongView.setGameType(GameType.DemoPlayerGame);
30:     }
31: 
32:     @Override
33:     public boolean onCreateOptionsMenu(Menu menu) {
34: 
35:         MenuInflater inflater = this.getMenuInflater();
36:         inflater.inflate(R.menu.menu_main, menu);
37:         return true;
38:     }
39: 
40: 
41:     @Override
42:     public boolean onOptionsItemSelected(MenuItem item) {
43: 
44:         int id = item.getItemId();
45:         if (id == R.id.action_highscore) {
46:             Toast.makeText(this, "Pressed Highscore", Toast.LENGTH_LONG).show();
47:         }
48: 
49:         return super.onOptionsItemSelected(item);
50:     }
51: 
52:     @Override
53:     public void onClick(View view) {
54:         if (view == this.buttonStart) {
55:             this.pongView.Start();
56: 
57:         } else if (view == this.buttonStop) {
58:             this.pongView.Stop();
59:         }
60:     }
61: }

Beispiel 6. Datei PongActivity.java: Pong-Aktivität der Android-App.


001: public class PongView extends View implements View.OnTouchListener {
002: 
003:     private Paddle leftPaddle;      // left paddle
004:     private Paddle rightPaddle;     // right paddle
005:     private Ball ball;              // ball
006:     private TextView textviewScoreboard;   // score board
007: 
008:     private int pointerIds[];
009:     private int pointerIndexes[];
010:     private PointF pointerPositions[];
011: 
012:     private Size parent;
013:     private int fadingOutCount;
014:     private boolean isActive;
015:     private int playersTouchDistance;
016: 
017:     private GameType type;
018: 
019:     private int scoresLeftplayer;
020:     private int scoresRightplayer;
021: 
022:     // c'tor
023:     public PongView(Context context, AttributeSet set) {
024:         super(context, set);
025: 
026:         this.setBackgroundColor(Color.BLUE);
027: 
028:         this.pointerIds = new int[2];
029:         this.pointerIds[0] = -1;  // left pointer
030:         this.pointerIds[1] = -1;  // right pointer
031:         this.pointerIndexes = new int[2];
032: 
033:         this.pointerPositions = new PointF[2];
034:         this.pointerPositions[0] = null;  // left pointer position, if any
035:         this.pointerPositions[1] = null;  // right pointer position, if any
036: 
037:         this.leftPaddle = new Paddle(PaddleType.LeftPaddle);
038:         this.rightPaddle = new Paddle(PaddleType.RightPaddle);
039:         this.ball = new Ball();
040: 
041:         this.setOnTouchListener(this);
042: 
043:         this.isActive = false;
044:         this.type = GameType.DemoPlayerGame;
045: 
046:         // need to be updated in onSizeChanged
047:         this.parent = new Size(0, 0);
048:         this.playersTouchDistance = 0;
049:     }
050: 
051:     // getter/setter
052:     public void setGameType(GameType type) {
053:         this.type = type;
054:     }
055: 
056:     // public interface
057:     public void setScoreboardView(TextView scoreboard) {
058:         this.textviewScoreboard = scoreboard;
059:     }
060: 
061:     public void Start() {
062:         if (this.isActive == false) {
063:             this.resetGame();
064:             this.isActive = true;
065:             this.invalidate();
066:         }
067:     }
068: 
069:     public void Stop() {
070:         this.isActive = false;
071:         this.textviewScoreboard.setText("0:0");
072:     }
073: 
074:     @Override
075:     protected void onSizeChanged(int xNew, int yNew, int xOld, int yOld) {
076:         super.onSizeChanged(xNew, yNew, xOld, yOld);
077: 
078:         this.parent = new Size(xNew, yNew);
079: 
080:         this.playersTouchDistance = xNew / 3;
081: 
082:         this.leftPaddle.setParentSize(this.parent);
083:         this.rightPaddle.setParentSize(this.parent);
084:         this.ball.setParentSize(this.parent);
085:     }
086: 
087:     @Override
088:     protected void onDraw(Canvas canvas) {
089:         super.onDraw(canvas);
090: 
091:         if (this.isActive) {
092:             this.drawShapes(canvas);
093:             this.moveShapes();
094:             boolean successful = this.handlePaddleHits();
095:             if (!successful) {
096:                 this.updateScores();
097:                 this.resetPlay();
098:             }
099:             this.invalidate();
100:         }
101:     }
102: 
103:     @Override
104:     protected void onDetachedFromWindow() {
105:         super.onDetachedFromWindow();
106: 
107:         // View is now detached, and about to be destroyed
108:         this.isActive = false;
109:     }
110: 
111:     // private helper methods
112:     private void drawShapes(Canvas canvas) {
113:         this.leftPaddle.draw(canvas);
114:         this.rightPaddle.draw(canvas);
115:         this.ball.draw(canvas);
116:     }
117: 
118:     private void moveShapes() {
119: 
120:         // ball
121:         this.ball.move();
122: 
123:         // left paddle
124:         if (this.type == GameType.DemoPlayerGame) {
125:             // set left paddle programmatically
126:             if (this.ball.getDirection() == BallDirection.Leftwards) {
127:                 if (this.ball.getOriginX() < this.parent.getWidth() / 2) {
128:                     this.leftPaddle.move(this.ball.getOriginY());
129:                 }
130:             } else {
131:                 int y = this.parent.getHeight() / 2;
132:                 this.leftPaddle.move(y);
133:             }
134:         } else {
135:             // set left paddle according to user's interaction
136:             if (this.pointerPositions[0] != null) {
137:                 this.leftPaddle.move((int) this.pointerPositions[0].y);
138:             }
139:         }
140: 
141:         // right paddle
142:         if (this.type == GameType.MultiPlayerGame) {
143:             // set right paddle according to user's interaction
144:             if (this.pointerPositions[1] != null) {
145:                 this.rightPaddle.move((int) this.pointerPositions[1].y);
146:             }
147:         } else {
148:             // set right paddle programmatically
149:             if (this.ball.getDirection() == BallDirection.Rightwards) {
150:                 if (this.ball.getOriginX() > this.parent.getWidth() / 2) {
151:                     this.rightPaddle.move(this.ball.getOriginY());
152:                 }
153:             } else {
154:                 int y = this.parent.getHeight() / 2;
155:                 this.rightPaddle.move(y);
156:             }
157:         }
158:     }
159: 
160:     /**
161:      * Returns true, if either the ball is far away from the paddles
162:      * or the player succeeded to hit the ball.
163:      * If the player failed to hit the ball, the method will return false
164:      * after a delay of 'fading out' invocations.
165:      * In this case a 'game over' scenario should be handled and a
166:      * new game could be started.
167:      */
168:     private boolean handlePaddleHits() {
169: 
170:         if (this.fadingOutCount > 0) {
171:             this.fadingOutCount--;
172:             return (this.fadingOutCount == 1) ? false : true;
173:         }
174: 
175:         Point center = this.ball.getOrigin();
176: 
177:         if (!this.ball.ballIsInCriticalArea(center.x))
178:             return true;
179: 
180:         // check collision of ball with left paddle
181:         if (this.leftPaddle.hitsBall(center)) {
182:             this.ball.changeDirection();
183:             return true;
184:         }
185: 
186:         // check collision of ball with right paddle
187:         if (this.rightPaddle.hitsBall(center)) {
188:             this.ball.changeDirection();
189:             return true;
190:         }
191: 
192:         // paddle didn't hit ball, return game over (delayed)
193:         this.fadingOutCount = Globals.BallFadingOutMaxSteps;
194:         return true;
195:     }
196: 
197:     private void updateScores() {
198: 
199:         if (this.ball.getDirection() == BallDirection.Leftwards) {
200:             this.scoresRightplayer++;
201:         } else if (this.ball.getDirection() == BallDirection.Rightwards) {
202:             this.scoresLeftplayer++;
203:         }
204: 
205:         String text = String.format(
206:             Locale.getDefault(), "%d:%d", this.scoresLeftplayer, this.scoresRightplayer);
207:         this.textviewScoreboard.setText(text);
208: 
209:         if (this.scoresLeftplayer >= Globals.MaxPlays) {
210:             this.isActive = false;
211:             Context context = this.getContext();
212:             Toast.makeText(context, "Left Player has won !", Toast.LENGTH_LONG).show();
213:         } else if (this.scoresRightplayer >= Globals.MaxPlays) {
214:             this.isActive = false;
215:             Context context = this.getContext();
216:             Toast.makeText(context, "Right Player has won !", Toast.LENGTH_LONG).show();
217:         }
218:     }
219: 
220:     private void resetPlay() {
221:         this.ball.init();
222:         this.fadingOutCount = 0;
223:     }
224: 
225:     private void resetGame() {
226:         this.leftPaddle.init();
227:         this.rightPaddle.init();
228:         this.ball.init();
229: 
230:         this.scoresLeftplayer = 0;
231:         this.scoresRightplayer = 0;
232: 
233:         this.textviewScoreboard.setText("0:0");
234:         this.fadingOutCount = 0;
235:     }
236: 
237:     // implementation of interface 'OnTouchListener'
238:     @Override
239:     public boolean onTouch(View view, MotionEvent event) {
240: 
241:         int action = MotionEventCompat.getActionMasked(event);
242:         switch (action) {
243: 
244:             case MotionEvent.ACTION_DOWN:
245:             case MotionEvent.ACTION_POINTER_DOWN: {
246:                 int index = MotionEventCompat.getActionIndex(event);
247:                 int id = MotionEventCompat.getPointerId(event, index);
248:                 PointF point = new PointF(event.getX(index), event.getY(index));
249: 
250:                 if (point.x < this.playersTouchDistance) {
251: 
252:                     // should be left paddle
253:                     if (this.pointerIds[0] == -1) {
254: 
255:                         // left paddle gets active
256:                         this.pointerIndexes[0] = index;
257:                         this.pointerIds[0] = id;
258:                         this.pointerPositions[0] = point;
259:                     } else {
260:                         Log.v("PONG", "Ignoring second left touch event ...");
261:                     }
262:                 } else if (point.x >= this.parent.getWidth() - this.playersTouchDistance) {
263: 
264:                     // should be right paddle
265:                     if (this.pointerIds[1] == -1) {
266: 
267:                         // right paddle gets active
268:                         this.pointerIndexes[1] = index;
269:                         this.pointerIds[1] = id;
270:                         this.pointerPositions[1] = point;
271:                     } else {
272:                         Log.v("PONG", "Ignoring second right touch event ...");
273:                     }
274: 
275:                 } else {
276:                     Log.v("PONG", "Ignoring middle touch event ...");
277:                 }
278:             }
279:             break;
280: 
281:             case MotionEvent.ACTION_MOVE: {
282:                 // update left paddle position, if data is available
283:                 if (this.pointerIds[0] != -1) {
284:                     int leftIndex = event.findPointerIndex(this.pointerIds[0]);
285:                     if (leftIndex != -1) {
286:                         // update current touch position of left player
287:                         this.pointerPositions[0] =
288:                             new PointF(event.getX(leftIndex), event.getY(leftIndex));
289:                     }
290:                 }
291: 
292:                 // update right paddle position, if data is available
293:                 if (this.pointerIds[1] != -1) {
294:                     int rightIndex = event.findPointerIndex(this.pointerIds[1]);
295:                     if (rightIndex != -1) {
296:                         // update current touch position of right player
297:                         this.pointerPositions[1] =
298:                             new PointF(event.getX(rightIndex), event.getY(rightIndex));
299:                     }
300:                 }
301:             }
302:             break;
303: 
304:             case MotionEvent.ACTION_UP:
305:             case MotionEvent.ACTION_POINTER_UP: {
306:                 int index = MotionEventCompat.getActionIndex(event);
307:                 int id = MotionEventCompat.getPointerId(event, index);
308: 
309:                 if (this.pointerIds[0] == id) {
310:                     // releasing left paddle
311:                     this.pointerIds[0] = -1;
312:                     this.pointerPositions[0] = null;
313: 
314:                 } else if (this.pointerIds[1] == id) {
315:                     // releasing right paddle
316:                     this.pointerIds[1] = -1;
317:                     this.pointerPositions[1] = null;
318:                 } else {
319:                     Log.v("PONG", "releasing unused pointer !");
320:                 }
321:             }
322:             break;
323: 
324:             case MotionEvent.ACTION_OUTSIDE:
325:                 Log.v("PONG", "ACTION_OUTSIDE");
326:                 break;
327: 
328:             case MotionEvent.ACTION_CANCEL:
329:                 Log.v("PONG", "ACTION_CANCEL");
330:                 break;
331:         }
332: 
333:         return true;
334:     }
335: }

Beispiel 7. Datei PongView.java: Steuerelement für einen Ball und zwei Schläger.


Die Klassen der Android-Aktivitäten werden jeweils von deklarativen XML-Dateien zur Oberflächengestaltung begleitet:

01: <?xml version="1.0" encoding="utf-8"?>
02: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
03:     xmlns:tools="http://schemas.android.com/tools"
04:     android:layout_width="match_parent"
05:     android:layout_height="match_parent"
06:     android:orientation="vertical"
07:     android:paddingBottom="@dimen/activity_vertical_margin"
08:     android:paddingLeft="@dimen/activity_horizontal_margin"
09:     android:paddingRight="@dimen/activity_horizontal_margin"
10:     android:paddingTop="@dimen/activity_vertical_margin"
11:     tools:context="de.peterloos.pong.StartActivity">
12: 
13:     <Button
14:         android:id="@+id/button_single_player"
15:         android:layout_width="match_parent"
16:         android:layout_height="wrap_content"
17:         android:text="@string/single_player" />
18: 
19:     <Button
20:         android:id="@+id/button_multi_player"
21:         android:layout_width="match_parent"
22:         android:layout_height="wrap_content"
23:         android:text="@string/multi_player" />
24: 
25:     <Button
26:         android:id="@+id/button_demo_player"
27:         android:layout_width="match_parent"
28:         android:layout_height="wrap_content"
29:         android:text="@string/demo_player" />
30: 
31: </LinearLayout>

und

01: <?xml version="1.0" encoding="utf-8"?>
02: 
03: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
04:     xmlns:tools="http://schemas.android.com/tools"
05:     android:layout_width="match_parent"
06:     android:layout_height="match_parent"
07:     android:paddingBottom="@dimen/activity_vertical_margin"
08:     android:paddingLeft="@dimen/activity_horizontal_margin"
09:     android:paddingRight="@dimen/activity_horizontal_margin"
10:     android:paddingTop="@dimen/activity_vertical_margin"
11:     tools:context=".PongActivity">
12: 
13:     <LinearLayout
14:         android:id="@+id/header"
15:         android:layout_width="match_parent"
16:         android:layout_height="wrap_content"
17:         android:orientation="horizontal">
18: 
19:         <Button
20:             android:id="@+id/button_start"
21:             android:layout_width="0dp"
22:             android:layout_height="wrap_content"
23:             android:layout_weight="1"
24:             android:text="@string/game_start" />
25: 
26:         <Button
27:             android:id="@+id/button_stop"
28:             android:layout_width="0dp"
29:             android:layout_height="wrap_content"
30:             android:layout_weight="1"
31:             android:text="@string/game_stop" />
32:     </LinearLayout>
33: 
34:     <de.peterloos.pong.PongView
35:         android:id="@+id/pong_view"
36:         android:layout_width="match_parent"
37:         android:layout_height="match_parent"
38:         android:layout_below="@id/header" />
39: 
40:     <TextView
41:         android:id="@+id/textview_gamescore"
42:         android:layout_width="wrap_content"
43:         android:layout_height="wrap_content"
44:         android:layout_alignParentBottom="true"
45:         android:layout_centerHorizontal="true"
46:         android:fontFamily="monospace"
47:         android:textSize="30sp"
48:         android:textStyle="bold" />
49: </RelativeLayout>

Natürlich besitzt die App im Augenblick noch eine Reihe von offenen Punkten, die zur kreativen Weiterarbeit einladen. Im Augenblick bewegt sich der Spielball stets in einem 45°-Winkel zu den Seitenflächen des Spielfelds. Hier bietet es sich an, abhängig von der Aufprallposition des Balls auf dem Schläger eine flexiblere Bewegungsbahn vorzunehmen. Auch ist der Aspekt Highscore-Verwaltung im Quellcode nur andeutungsweise vorhanden.