2024-01-10 14:49:33 +01:00
|
|
|
import 'dart:async';
|
|
|
|
import 'dart:math';
|
|
|
|
|
|
|
|
import 'package:flutter/cupertino.dart';
|
2023-11-21 15:53:43 +01:00
|
|
|
import 'package:flutter/material.dart';
|
2024-01-10 14:49:33 +01:00
|
|
|
import 'package:flutter/scheduler.dart';
|
|
|
|
|
|
|
|
import 'package:pong/pong_menu.dart';
|
2023-11-21 15:53:43 +01:00
|
|
|
|
2023-11-21 15:58:06 +01:00
|
|
|
void main() {
|
2024-01-10 22:56:47 +01:00
|
|
|
// Run the PongGame
|
2023-11-21 17:28:39 +01:00
|
|
|
runApp(const PongGame());
|
2023-11-21 15:53:43 +01:00
|
|
|
}
|
|
|
|
|
2023-11-21 17:28:39 +01:00
|
|
|
class PongGame extends StatelessWidget {
|
|
|
|
const PongGame({super.key});
|
2023-11-21 15:53:43 +01:00
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2023-11-21 23:41:52 +01:00
|
|
|
return MaterialApp(
|
2024-01-10 14:49:33 +01:00
|
|
|
// Set the app title and theme
|
2023-11-21 17:28:39 +01:00
|
|
|
title: 'Pong Game',
|
2024-01-10 14:49:33 +01:00
|
|
|
theme: ThemeData(
|
|
|
|
scaffoldBackgroundColor: const Color.fromARGB(255, 41, 38, 38)),
|
|
|
|
// Set the initial screen to the StartScreen
|
2023-11-21 23:41:52 +01:00
|
|
|
home: const StartScreen(),
|
2023-11-21 15:53:43 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-21 17:28:39 +01:00
|
|
|
class StartScreen extends StatelessWidget {
|
|
|
|
const StartScreen({super.key});
|
2023-11-21 15:53:43 +01:00
|
|
|
|
|
|
|
@override
|
2023-11-21 17:28:39 +01:00
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Scaffold(
|
|
|
|
body: GestureDetector(
|
|
|
|
onTap: () {
|
2024-01-10 14:49:33 +01:00
|
|
|
// Navigate to the GameScreen when tapped
|
2023-11-21 23:41:52 +01:00
|
|
|
Navigator.of(context).push(MaterialPageRoute(
|
2023-11-21 17:28:39 +01:00
|
|
|
builder: (context) {
|
|
|
|
return const GameScreen();
|
|
|
|
},
|
2023-11-21 23:41:52 +01:00
|
|
|
));
|
2023-11-21 17:28:39 +01:00
|
|
|
},
|
2023-11-21 23:41:52 +01:00
|
|
|
child: const Center(
|
2024-01-10 14:49:33 +01:00
|
|
|
// Display a message prompting the user to touch the screen to begin
|
2023-11-21 23:41:52 +01:00
|
|
|
child: Text(
|
|
|
|
'Berühren um zu beginnen!',
|
|
|
|
style: TextStyle(
|
|
|
|
fontSize: 30,
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
color: Colors.white,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
2023-11-21 17:28:39 +01:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2023-11-21 15:53:43 +01:00
|
|
|
}
|
|
|
|
|
2023-11-21 17:28:39 +01:00
|
|
|
class GameScreen extends StatefulWidget {
|
|
|
|
const GameScreen({super.key});
|
2023-11-21 15:53:43 +01:00
|
|
|
|
2023-11-21 17:28:39 +01:00
|
|
|
@override
|
2024-01-10 14:49:33 +01:00
|
|
|
State<GameScreen> createState() => GameScreenState();
|
2023-11-21 17:28:39 +01:00
|
|
|
}
|
2023-11-21 15:53:43 +01:00
|
|
|
|
2024-01-10 14:49:33 +01:00
|
|
|
class GameScreenState extends State<GameScreen> {
|
|
|
|
// Constants defining properties of the game
|
|
|
|
final ballSize = 20.0;
|
|
|
|
final racketWidth = 100.0;
|
|
|
|
final racketHeight = 10.0;
|
2023-11-21 23:41:52 +01:00
|
|
|
final racketBottomOffset = 100.0;
|
2024-01-10 14:49:33 +01:00
|
|
|
bool isPaused = false;
|
|
|
|
|
|
|
|
// Initial speed of the ball
|
|
|
|
final initialBallSpeed = 3.0;
|
2023-11-21 23:41:52 +01:00
|
|
|
|
2024-01-10 14:49:33 +01:00
|
|
|
// Variables to track ball and racket positions, speed, score, and game state
|
|
|
|
double ballPositionX = 0;
|
|
|
|
double ballPositionY = 0;
|
|
|
|
double ballSpeedX = 0;
|
|
|
|
double ballSpeedY = 0;
|
2023-11-21 23:41:52 +01:00
|
|
|
double racketX = 20;
|
2024-01-10 14:49:33 +01:00
|
|
|
int score = 0;
|
|
|
|
|
|
|
|
// Ticker for updating the game state
|
|
|
|
late Ticker ticker;
|
|
|
|
double ballSpeedMultiplier = 1.1;
|
|
|
|
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
|
|
|
// Initialize the game state when the widget is created
|
|
|
|
startGame();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Initialize the game state
|
|
|
|
void startGame() {
|
|
|
|
final random = Random();
|
|
|
|
ballPositionX = 0;
|
|
|
|
ballPositionY = 0;
|
|
|
|
ballSpeedX = initialBallSpeed;
|
|
|
|
ballSpeedY = initialBallSpeed;
|
|
|
|
ballSpeedMultiplier = 1.1;
|
|
|
|
racketX = 200;
|
|
|
|
score = 0;
|
|
|
|
|
|
|
|
// Randomize initial ball direction
|
|
|
|
if (random.nextBool()) ballSpeedX = -ballSpeedX;
|
|
|
|
if (random.nextBool()) ballSpeedY = -ballSpeedY;
|
|
|
|
|
|
|
|
// Start the game loop after a delay
|
|
|
|
continueGame();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stop the game loop
|
|
|
|
void stopGame() {
|
|
|
|
ticker.dispose();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Continue the game loop with a delay
|
|
|
|
void continueGame() {
|
|
|
|
if (!isPaused) {
|
|
|
|
Future.delayed(const Duration(seconds: 1), () {
|
|
|
|
ticker = Ticker((elapsed) {
|
|
|
|
setState(() {
|
|
|
|
moveBall(ballSpeedMultiplier);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
ticker.start();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void moveBall(double ballSpeedMultiplier) {
|
|
|
|
// Update ball position based on speed and direction
|
|
|
|
ballPositionX += ballSpeedX * ballSpeedMultiplier;
|
|
|
|
ballPositionY += ballSpeedY * ballSpeedMultiplier;
|
|
|
|
|
|
|
|
// Get the screen size
|
|
|
|
final Size screenSize = MediaQuery.of(context).size;
|
|
|
|
|
|
|
|
// Check for collisions with horizontal walls and bounce the ball
|
|
|
|
if (ballPositionX < 0 || ballPositionX > screenSize.width - ballSize) {
|
|
|
|
ballSpeedX = -ballSpeedX;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for collision with the top wall and bounce the ball
|
|
|
|
if (ballPositionY < 0) {
|
|
|
|
ballSpeedY = -ballSpeedY;
|
|
|
|
}
|
2023-11-21 23:41:52 +01:00
|
|
|
|
2024-01-10 14:49:33 +01:00
|
|
|
// Check if the ball has touched the floor
|
|
|
|
var ballTouchesFloor = ballPositionY > screenSize.height - ballSize;
|
|
|
|
|
|
|
|
// Check if the ball collides with the racket
|
|
|
|
var ballCollidesWithRacket =
|
|
|
|
ballPositionY + ballSize >= screenSize.height - racketBottomOffset &&
|
|
|
|
ballPositionY <= screenSize.height - racketBottomOffset &&
|
|
|
|
ballPositionX + ballSize >= racketX &&
|
|
|
|
ballPositionX <= racketX + racketWidth;
|
|
|
|
|
|
|
|
// Handle collision with the racket
|
|
|
|
if (ballCollidesWithRacket) {
|
|
|
|
// Check if the ball hits the top of the racket
|
|
|
|
if (ballPositionY <= screenSize.height - racketBottomOffset) {
|
|
|
|
ballSpeedY =
|
|
|
|
-ballSpeedY * ballSpeedMultiplier; // Bounce the ball upward
|
|
|
|
setState(() {
|
|
|
|
score += 1; // Increase the score
|
|
|
|
debugPrint('ballSpeedMultiplier: $ballSpeedMultiplier');
|
|
|
|
});
|
|
|
|
|
|
|
|
ballPositionY = screenSize.height - racketBottomOffset - ballSize;
|
|
|
|
// Adjust the Y position to stay above the racket
|
|
|
|
} else {
|
|
|
|
ballSpeedX =
|
|
|
|
-ballSpeedX * ballSpeedMultiplier; // Bounce the ball backward
|
|
|
|
|
|
|
|
// Adjust the Y position to prevent the ball from going through the racket
|
|
|
|
double adjustedY = screenSize.height - racketBottomOffset - ballSize;
|
|
|
|
if (ballPositionY < adjustedY) {
|
|
|
|
ballPositionY = adjustedY;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle game over condition if the ball touches the floor
|
|
|
|
} else if (ballTouchesFloor) {
|
|
|
|
debugPrint('Game over');
|
|
|
|
stopGame();
|
|
|
|
showDialog(
|
|
|
|
context: context,
|
|
|
|
barrierDismissible: false,
|
|
|
|
builder: (BuildContext context) {
|
|
|
|
return PongMenu(
|
|
|
|
title: 'Game over!',
|
|
|
|
subTitle: 'Deine Punkte: $score',
|
|
|
|
child: CupertinoButton(
|
|
|
|
child: const Text('Nochmal spielen'),
|
|
|
|
onPressed: () {
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
startGame();
|
|
|
|
},
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void moveRacket(double x) {
|
|
|
|
// Move the racket horizontally based on the user's touch position
|
2023-11-21 23:41:52 +01:00
|
|
|
setState(() {
|
|
|
|
racketX = x - racketWidth / 2;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-11-21 15:53:43 +01:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2023-11-21 23:41:52 +01:00
|
|
|
return Scaffold(
|
2024-01-10 14:49:33 +01:00
|
|
|
body: Stack(
|
|
|
|
children: [
|
|
|
|
GestureDetector(
|
|
|
|
// Listen for horizontal drag updates to move the racket
|
|
|
|
onHorizontalDragUpdate: (details) {
|
|
|
|
moveRacket(details.globalPosition.dx);
|
|
|
|
},
|
|
|
|
child: CustomPaint(
|
|
|
|
painter: PongGamePainter(
|
|
|
|
racketHeight: racketHeight,
|
|
|
|
racketWidth: racketWidth,
|
|
|
|
racketX: racketX,
|
|
|
|
racketBottomOffset: racketBottomOffset,
|
|
|
|
ballSize: ballSize,
|
|
|
|
ballX: ballPositionX,
|
|
|
|
ballY: ballPositionY,
|
|
|
|
),
|
|
|
|
size: Size.infinite,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
Center(
|
|
|
|
// Display the player's score in the center of the screen
|
|
|
|
child: Text(
|
|
|
|
'Punkte: $score',
|
|
|
|
style: const TextStyle(
|
|
|
|
fontSize: 30,
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
Positioned(
|
|
|
|
top: 40,
|
|
|
|
right: 20,
|
|
|
|
child: IconButton(
|
|
|
|
icon: const Icon(
|
|
|
|
CupertinoIcons.pause,
|
|
|
|
size: 30,
|
|
|
|
color: Colors.white,
|
|
|
|
),
|
|
|
|
onPressed: () {
|
|
|
|
// Pause the game and show dialog
|
|
|
|
stopGame();
|
|
|
|
showDialog(
|
|
|
|
context: context,
|
|
|
|
barrierDismissible: false,
|
|
|
|
builder: (BuildContext context) {
|
|
|
|
return PongMenu(
|
|
|
|
title: 'Pause',
|
|
|
|
subTitle: 'Deine Punkte: $score',
|
|
|
|
child: CupertinoButton(
|
|
|
|
onPressed: () {
|
|
|
|
// Continues the game
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
isPaused = false;
|
|
|
|
continueGame();
|
|
|
|
},
|
|
|
|
child: const Text('Fortfahren'),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
),
|
|
|
|
)
|
|
|
|
],
|
2023-11-21 23:41:52 +01:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class PongGamePainter extends CustomPainter {
|
|
|
|
final double ballSize;
|
|
|
|
final double ballX;
|
|
|
|
final double ballY;
|
|
|
|
|
|
|
|
final double racketX;
|
|
|
|
final double racketWidth;
|
|
|
|
final double racketHeight;
|
|
|
|
final double racketBottomOffset;
|
|
|
|
|
2024-01-10 14:49:33 +01:00
|
|
|
// Constructor to initialize the painter with required properties
|
2023-11-21 23:41:52 +01:00
|
|
|
PongGamePainter({
|
|
|
|
required this.ballSize,
|
|
|
|
required this.ballX,
|
|
|
|
required this.ballY,
|
|
|
|
required this.racketX,
|
|
|
|
required this.racketWidth,
|
|
|
|
required this.racketHeight,
|
|
|
|
required this.racketBottomOffset,
|
|
|
|
});
|
|
|
|
|
|
|
|
@override
|
|
|
|
void paint(Canvas canvas, Size size) {
|
2024-01-10 14:49:33 +01:00
|
|
|
// Paint object for drawing the racket
|
2023-11-21 23:41:52 +01:00
|
|
|
final racketPaint = Paint()..color = Colors.white;
|
2024-01-10 14:49:33 +01:00
|
|
|
// Paint object for drawing the ball
|
2023-11-21 23:41:52 +01:00
|
|
|
final ballPaint = Paint()..color = Colors.white;
|
|
|
|
|
2024-01-10 14:49:33 +01:00
|
|
|
// Draw the ball as an oval on the canvas
|
2023-11-21 23:41:52 +01:00
|
|
|
canvas.drawOval(
|
|
|
|
Rect.fromLTWH(ballX, ballY, ballSize, ballSize),
|
|
|
|
ballPaint,
|
|
|
|
);
|
|
|
|
|
2024-01-10 14:49:33 +01:00
|
|
|
// Draw the racket as a rectangle on the canvas
|
2023-11-21 23:41:52 +01:00
|
|
|
canvas.drawRect(
|
|
|
|
Rect.fromLTWH(
|
|
|
|
racketX,
|
|
|
|
size.height - racketHeight - racketBottomOffset,
|
|
|
|
racketWidth,
|
|
|
|
racketHeight,
|
|
|
|
),
|
|
|
|
racketPaint,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
bool shouldRepaint(covariant PongGamePainter oldDelegate) {
|
2024-01-10 14:49:33 +01:00
|
|
|
// Repaint only if the ball or racket position changes
|
2023-11-21 23:41:52 +01:00
|
|
|
return ballX != oldDelegate.ballX ||
|
|
|
|
ballY != oldDelegate.ballY ||
|
|
|
|
racketX != oldDelegate.racketX;
|
2023-11-21 15:53:43 +01:00
|
|
|
}
|
|
|
|
}
|