Das Retro-Spiel Pong

1. Aufgabe

Im Jahre 1972 veröffentlichte die Firma Atari das erste populäre Computerspiel Pong. Es ist ein einfaches Videospiel, das recht ähnlich dem Tischtennis ist. Obwohl vor Pong bereits Videospiele auf dem Markt waren, gilt Pong als der Urvater aller Videospiele. Mittlerweilen gibt es sogar eine Website für das Spiel (www.ponggame.org), die neben einer Reihe von Online-Varianten des Spiels auch einen kurzen Abriss zu seiner Geschichte enthält. Ein Beispiel für die Oberfläche des Spiels in seiner ursprünglichen Version finden Sie in Abbildung 1 vor:

Das Spiel Pong in seiner Ursprungsversion.

Abbildung 1. Das Spiel Pong in seiner Ursprungsversion.


Das Spielprinzip von Pong ist simpel: Ein „Ball“ bewegt sich auf dem Bildschirm hin und her. Jeder der beiden Spieler steuert einen senkrechten Balken („Schläger“), den er in der Ursprungsversion auf einer Spielekonsole mit einem Drehknopf („Paddle“) nach oben und unten verschieben kann. Bewegt sich der „Ball“ am „Schläger“ vorbei, erhält der Gegner einen Punkt.

In der jüngsten Zeit erfreuen sich Pong und andere Spieleklassiker wieder großer Beliebtheit. Die Spielekonsolen früherer Zeiten sind modernen PCs und Smartphones gewichen. Als Drehknopf stehen nun Gaming Mäuse oder die Gestensteuerung eines Smartphones mit berührungsempfindlichem Bildschirm zur Verfügung. Auf diese Weise gewinnen die Retro-Games aus den Ursprungszeiten der Computertechnik wieder an Reiz.

In dieser Aufgabe erstellen wir mit elementaren Hilfsmitteln der Windows Presentation Foundation ein Pong-Spiel (Abbildung 2). Realisiert sind nur die Interaktionen des Spiels selbst. Die Implementierung einer Bestenliste mit Spielernamen und Punktezahlen ist Ihnen als Zusatzaufgabe überlassen. Im Lösungsvorschlag finden Sie diejenige Spielvariante vor, die aus einem Spieler besteht. Spielgegner in der Realisierung ist ihr eigenes Programm. Sie können als selbst am besten entscheiden bzw. beeinflussen, inwieweit Sie überhaupt eine Chance zum Gewinnen haben oder nicht.

Das Spiel Pong realisiert mit der Windows Presentation Foundation.

Abbildung 2. Das Spiel Pong realisiert mit der Windows Presentation Foundation.

2. Lösung

Quellcode: Siehe auch https://github.com/peterloos/Wpf_Pong.git.

Wie in den einleitenden Worten schon beschrieben, soll die grundlegende Spielidee auf möglichst einfache Weise implementiert werden. Zu diesem Zweck ordnen wir die zwei Schläger, den Ball und die Spiellogik einem einzelnen Steuerelement (WPF Custom Control) mit fester Größe zu. Das Hautfenster fällt damit sehr simpel aus, was sowohl die XAML-Anweisungen als auch den Code-Behind-Anteil anbelangt (Listing 1 und Listing 2):

01: <Window x:Class="Wpf_Pong.MainWindow"
02:         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
03:         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
04:         xmlns:local="clr-namespace:Wpf_Pong"
05:         Title="Another Pong Game" Height="550" Width="700">
06:     <DockPanel LastChildFill="True">
07:         <UniformGrid DockPanel.Dock="Top" Rows="1" Columns="2">
08:             <Button
09:                 Name="ButtonStart"
10:                 Click="Button_Click" Margin="5">Start</Button>
11:             <Button
12:                 Name="ButtonStop"
13:                 Click="Button_Click" Margin="5">Stop</Button>
14:         </UniformGrid>
15:         <local:PongCanvasControl
16:             x:Name="PongCanvas"
17:             Width="{x:Static local:PongCanvasControl.CanvasWidth}"
18:             Height="{x:Static local:PongCanvasControl.CanvasHeight}"/>
19:     </DockPanel>
20: </Window>

Beispiel 1. Hauptfenster des Pong Spiels: XAML.


01: namespace Wpf_Pong
02: {
03:     public partial class MainWindow : Window
04:     {
05:         public MainWindow()
06:         {
07:             this.InitializeComponent();
08:         }
09: 
10:         private void Button_Click(Object sender, RoutedEventArgs e)
11:         {
12:             if (sender == this.ButtonStart)
13:             {
14:                 this.PongCanvas.Start();
15:             }
16:             else if (sender == this.ButtonStop)
17:             {
18:                 this.PongCanvas.Stop();
19:             }
20:         }
21:     }
22: }

Beispiel 2. Hauptfenster des Pong Spiels: Code-Behind.


Die eigentliche Arbeit des Pong-Spiels findet in einem Steuerelement statt. Die Bewegungen des Balls sind an den Grenzen eines Canvas-Steuerelements zu beobachten und ggf. zu korrigieren. Im echten Pong-Spiel kann der Ball in einem gewissen Wertebereich unterschiedliche Winkel bzgl. der Bewegungsrichtung annehmen. Die vorliegende Realisierung kann den Ball nur im 45°-Winkel bewegen.

Der linke Schläger ist dem menschlichen Spieler zugeordnet – seine Bewegungen werden durch MouseMove-Ereignisse gesteuert. Der rechte Schläger wird durch das Programm manipuliert. Die Strategie ist vergleichsweise einfach: Hat der Spielball die Mittellinie überschritten und befindet sich auf derselben Höhe wie der Schläger des Computer-Spielers, nimmt dieser seinen Bewegungsverlauf auf. Zugegebenermaßen wird es auf diese Weise recht schwer, das Programm zu überlisten ... aber Sie können hier ja auch gewisse Ungenauigkeiten in der Bewegungsführung einbauen.

Alle weiteren Details entnehmen Sie bitte Listing 3:

001: internal enum BallDirection { RightTop, RightBottom, LeftTop, LeftBottom }
002: 
003: public class PongCanvasControl : Canvas
004: {
005:     public const double CanvasWidth = 500.0;
006:     public const double CanvasHeight = 340.0;
007: 
008:     // miscellaneous constants
009:     private const int PaddleWidth = 20;
010:     private const int PaddleHeight = 80;
011:     private const int PaddleMargin = 5;
012:     private const int BallWidth = 20;
013:     private const int BallHeight = 20;
014: 
015:     // two paddles and ball
016:     private Rectangle leftPaddle;
017:     private Rectangle rightPaddle;
018:     private Ellipse ball;
019:     private double leftPaddleTop;
020:     private double rightPaddleTop;
021:     private double ballLeft;
022:     private double ballTop;
023: 
024:     private BallDirection dir;
025: 
026:     private bool isRightPaddleMoving;
027:     private bool isRunning;
028:     private bool isGameOver;
029: 
030:     private DispatcherTimer timer;
031:     private const int TimerIntervalMSecs = 5;
032:     private const double BallStep = 3.0;
033: 
034:     public PongCanvasControl ()
035:     {
036:         // adjusting pong canvas control
037:         this.Background = Brushes.DarkGray;
038: 
039:         // setup left paddle (without location)
040:         this.leftPaddle = new Rectangle();
041:         this.leftPaddle.Fill = Brushes.White;
042:         this.leftPaddle.Width = PaddleWidth;
043:         this.leftPaddle.Height = PaddleHeight;
044:         this.Children.Add(this.leftPaddle);
045: 
046:         // setup right paddle (without location)
047:         this.rightPaddle = new Rectangle();
048:         this.rightPaddle.Fill = Brushes.White;
049:         this.rightPaddle.Width = PaddleWidth;
050:         this.rightPaddle.Height = PaddleHeight;
051:         this.Children.Add(this.rightPaddle);
052: 
053:         // setup ball (without location)
054:         this.ball = new Ellipse();
055:         this.ball.Fill = Brushes.White;
056:         this.ball.Width = BallWidth;
057:         this.ball.Height = BallHeight;
058:         this.Children.Add(this.ball);
059: 
060:         // timer setup
061:         this.timer = new DispatcherTimer();
062:         this.timer.Interval = new TimeSpan(0, 0, 0, 0, 1);
063:         this.timer.Tick += this.PongTimerTick;
064: 
065:         // register event handler
066:         this.MouseMove += this.PongCanvasControl_MouseMove;
067:         this.Loaded += this.PongCanvasControl_Loaded;
068:     }
069: 
070:     private void PongCanvasControl_Loaded(Object sender, RoutedEventArgs e)
071:     {
072:         // actual width and height are now known:
073:         // setup locations of paddles and ball
074:         this.InitControls();
075:     }
076: 
077:     private void InitControls()
078:     {
079:         this.leftPaddle.SetValue(Canvas.LeftProperty, (double)PaddleMargin);
080:         this.leftPaddleTop =
081:             (this.ActualHeight - PaddleHeight - PaddleMargin) / 2;
082:         this.leftPaddle.SetValue(Canvas.TopProperty, this.leftPaddleTop);
083: 
084:         this.rightPaddle.SetValue(Canvas.LeftProperty, 
085:             this.ActualWidth - BallWidth - PaddleMargin);
086:         this.rightPaddleTop =
087:             (this.ActualHeight - PaddleHeight - PaddleMargin) / 2;
088:         this.rightPaddle.SetValue(Canvas.TopProperty, this.rightPaddleTop);
089: 
090:         this.ballLeft = PaddleMargin + BallWidth;
091:         this.ballTop = this.ActualHeight - BallHeight - PaddleMargin;
092:         this.PaintBall();
093:     }
094: 
095:     public void Start()
096:     {
097:         this.InitControls();
098: 
099:         this.isRightPaddleMoving = false;
100:         this.isRunning = true;
101:         this.isGameOver = false;
102: 
103:         this.dir = BallDirection.RightTop;
104: 
105:         this.timer.Start(); 
106:     }
107: 
108:     public void Stop()
109:     {
110:         this.isRunning = false;
111:         this.timer.Stop();
112:     }
113: 
114:     private void PongTimerTick(Object sender, EventArgs e)
115:     {
116:         this.MoveBall();
117:         this.PaintBall();
118:     }
119: 
120:     private void MoveBall()
121:     {
122:         // move ball
123:         switch (dir)
124:         {
125:             case BallDirection.RightTop:
126:                 this.ballLeft += BallStep;
127:                 this.ballTop -= BallStep;
128:                 break;
129:             case BallDirection.RightBottom:
130:                 this.ballLeft += BallStep;
131:                 this.ballTop += BallStep;
132:                 break;
133:             case BallDirection.LeftTop:
134:                 this.ballLeft -= BallStep;
135:                 this.ballTop -= BallStep;
136:                 break;
137:             case BallDirection.LeftBottom:
138:                 this.ballLeft -= BallStep;
139:                 this.ballTop += BallStep;
140:                 break;
141:         }
142: 
143:         if (this.isGameOver)
144:         {
145:             // has ball left canvas
146:             if (this.ballLeft + BallWidth < 0)
147:             {
148:                 // stop current game
149:                 this.Stop();
150: 
151:                 // ask user for another game
152:                 MessageBoxResult result = MessageBox.Show(
153:                     "Game Over!\nAnother Game?", "Another Pong Game",
154:                     MessageBoxButton.YesNo);
155:                 if (result == MessageBoxResult.Yes)
156:                     this.Start();
157:             }
158:                 
159:             return;
160:         }
161: 
162:         // collision detection
163:         if (this.ballLeft <= (BallWidth + PaddleMargin))
164:         {
165:             bool isLeftPaddleDefending = this.IsLeftPaddleDefending();
166: 
167:             if (isLeftPaddleDefending)
168:             {
169:                 if (dir == BallDirection.LeftTop)
170:                     dir = BallDirection.RightTop;
171:                 else if (dir == BallDirection.LeftBottom)
172:                     dir = BallDirection.RightBottom;
173:             }
174:             else
175:             {
176:                 // game is over ... let ball move out of the window
177:                 this.isGameOver = true;
178:             }
179:         }
180:         else if (this.ballLeft >= (this.ActualWidth - 2 * BallWidth - PaddleMargin))
181:         {
182:             if (dir == BallDirection.RightTop)
183:                 dir = BallDirection.LeftTop;
184:             else if (dir == BallDirection.RightBottom)
185:                 dir = BallDirection.LeftBottom;
186:         }
187: 
188:         if (this.ballTop <= PaddleMargin)
189:         {
190:             if (dir == BallDirection.RightTop)
191:                 dir = BallDirection.RightBottom;
192:             else if (dir == BallDirection.LeftTop)
193:                 dir = BallDirection.LeftBottom;
194:         }
195:         else if (this.ballTop >= (this.ActualHeight - BallHeight))
196:         {
197:             if (dir == BallDirection.RightBottom)
198:                 dir = BallDirection.RightTop;
199:             else if (dir == BallDirection.LeftBottom)
200:                 dir = BallDirection.LeftTop;
201:         }
202: 
203:         this.MoveRightPaddleIfNecessary();
204:     }
205: 
206:     private bool IsLeftPaddleDefending()
207:     {
208:         double verticalBallCenter = this.ballTop + (BallHeight / 2);
209: 
210:         return (
211:             verticalBallCenter >= this.leftPaddleTop &&
212:             verticalBallCenter <= this.leftPaddleTop + PaddleHeight) ?
213:                 true : false;
214:     }
215: 
216:     private void PongCanvasControl_MouseMove(Object sender, MouseEventArgs e)
217:     {
218:         if (!this.isRunning)
219:             return;
220: 
221:         Point p = e.GetPosition(this);
222:         if ((p.Y >= PaddleMargin + (PaddleHeight / 2)) &&
223:             (p.Y <= this.ActualHeight - PaddleMargin - (PaddleHeight / 2)))
224:         {
225:             this.leftPaddleTop = p.Y - (PaddleHeight / 2);
226:             this.leftPaddle.SetValue(Canvas.TopProperty, this.leftPaddleTop);
227:         }
228:     }
229: 
230:     private void PaintBall()
231:     {
232:         this.ball.SetValue(Canvas.LeftProperty, this.ballLeft);
233:         this.ball.SetValue(Canvas.TopProperty, this.ballTop);
234:     }
235: 
236:     private void MoveRightPaddleIfNecessary()
237:     {
238:         if (dir == BallDirection.LeftTop || dir == BallDirection.LeftBottom)
239:             return;
240: 
241:         if (this.ballLeft < (this.ActualWidth / 3))
242:             return;
243: 
244:         if (!this.isRightPaddleMoving)
245:         {
246:             // activate right paddle, if ball is "on same height"
247:             if (this.ballTop >= this.rightPaddleTop &&
248:                 this.ballTop <= this.rightPaddleTop + BallHeight)
249:                     this.isRightPaddleMoving = true;
250:         }
251: 
252:         if (this.isRightPaddleMoving)
253:         {
254:             double dist = BallStep / Math.Sqrt(2.0);
255: 
256:             if (dir == BallDirection.RightTop)
257:             {
258:                 // can paddle move to top
259:                 if (this.rightPaddleTop >= (PaddleMargin + dist))
260:                     this.rightPaddleTop -= dist;
261:             }
262:             else
263:             {
264:                 // can paddle move to bottom
265:                 double bottom =
266:                     this.ActualHeight - PaddleMargin - PaddleHeight - dist;
267:                 if (this.rightPaddleTop <= bottom)
268:                     this.rightPaddleTop += dist;
269:             }
270: 
271:             this.rightPaddle.SetValue(Canvas.TopProperty, this.rightPaddleTop);
272:         }
273:     }
274: }

Beispiel 3. Wpf-Steuerelement PongCanvasControl.