First commit

master
dogé 2022-05-30 14:36:18 -04:00 committed by John
commit 9d154bbbe6
11 changed files with 531 additions and 0 deletions

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

6
.idea/misc.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="openjdk-18" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/final.iml" filepath="$PROJECT_DIR$/final.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

11
final.iml Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

21
src/Ball.java Normal file
View File

@ -0,0 +1,21 @@
/* Eric Li, ICS4U, Completed 5/29/2022
Ball class defines behaviours for the pong ball; it also adds no new functions and instead inherits its main functions from GenericObject */
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
public class Ball extends GenericObject {
public int INITIAL_SPEED = 6;
public static final int BALL_DIAMETER = 15;
public Ball(int x, int y, int xDirection) {
super(x, y, BALL_DIAMETER, BALL_DIAMETER);
// set initial speed
super.setXDirection(INITIAL_SPEED * xDirection);
}
public void draw(Graphics g) {
g.setColor(Color.YELLOW);
g.fillRect(x, y, BALL_DIAMETER, BALL_DIAMETER);
}
}

24
src/GameFrame.java Normal file
View File

@ -0,0 +1,24 @@
/* Eric Li, ICS4U, Completed 5/29/2022
GameFrame class establishes the frame (window) for the game
It is a child of JFrame because JFrame manages frames
Runs the constructor in GamePanel class */
import java.awt.*;
import javax.swing.*;
public class GameFrame extends JFrame{
GamePanel panel;
public GameFrame(){
panel = new GamePanel(); //run GamePanel constructor
this.add(panel);
this.setTitle("Pong... 2.0!"); //set title for frame
this.setResizable(false); //frame can't change size
this.setBackground(Color.black);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //X button will stop program execution
this.pack();//makes components fit in window - don't need to set JFrame size, as it will adjust accordingly
this.setVisible(true); //makes window visible to user
this.setLocationRelativeTo(null);//set window in middle of screen
}
}

308
src/GamePanel.java Normal file
View File

@ -0,0 +1,308 @@
/* Eric Li, ICS4U, Completed 5/29/2022
GamePanel class acts as the main "game loop" - continuously runs the game and calls whatever needs to be called
Child of JPanel because JPanel contains methods for drawing to the screen
Implements KeyListener interface to listen for keyboard input
Implements Runnable interface to use "threading" - let the game do two things at once
*/
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.NoSuchElementException;
import java.util.Scanner;
import javax.swing.*;
public class GamePanel extends JPanel implements Runnable, KeyListener{
//dimensions of window
public static final int GAME_WIDTH = 1200;
public static final int GAME_HEIGHT = 600;
// score needed to win
public static final int WIN_SCORE = 5;
public Thread gameThread;
public Image image;
public Graphics graphics;
public Ball ball;
public Paddle leftPaddle, rightPaddle;
public boolean leftWin, rightWin;
public int leftScore, rightScore;
public String saveFile = "save.txt";
public boolean resumeGame = false;
public int paintNumber = 0;
// constructor initializes one Ball, two Paddles, and starts the Thread
public GamePanel(){
ball = new Ball((GAME_WIDTH-Ball.BALL_DIAMETER)/2, (GAME_HEIGHT-Ball.BALL_DIAMETER)/2, 1); //create a player controlled ball, set start location to middle of screen
// starting position of paddles are randomized
leftPaddle = new Paddle(10, (int)(Math.random() * GAME_HEIGHT), KeyEvent.VK_W, KeyEvent.VK_S);
rightPaddle = new Paddle(GAME_WIDTH - Paddle.PADDLE_WIDTH - 10, (int)(Math.random() * GAME_HEIGHT), KeyEvent.VK_UP, KeyEvent.VK_DOWN);
this.setFocusable(true); //make everything in this class appear on the screen
this.addKeyListener(this); //start listening for keyboard input
this.setPreferredSize(new Dimension(GAME_WIDTH, GAME_HEIGHT));
//make this class run at the same time as other classes (without this each class would "pause" while another class runs). By using threading we can remove lag, and also allows us to do features like display timers in real time!
gameThread = new Thread(this);
gameThread.start();
}
//paint is a method in java.awt library that we are overriding. It is a special method - it is called automatically in the background in order to update what appears in the window. You NEVER call paint() yourself
public void paint(Graphics g){
//we are using "double buffering here" - if we draw images directly onto the screen, it takes time and the human eye can actually notice flashes of lag as each pixel on the screen is drawn one at a time. Instead, we are going to draw images OFF the screen, then simply move the image on screen as needed.
image = createImage(GAME_WIDTH, GAME_HEIGHT); //draw off screen
graphics = image.getGraphics();
draw(graphics);//update the positions of everything on the screen
g.drawImage(image, 0, 0, this); //move the image on the screen
}
//call the draw methods in each class to update positions as things move
public void draw(Graphics g){
// set style of line (i.e., dashed)
Stroke dashed = new BasicStroke(3, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[]{9}, 0);
// call draw methods of two Paddles and Ball
ball.draw(g);
leftPaddle.draw(g);
rightPaddle.draw(g);
// if the game hasn't started yet, set color of middle line and score to gray
if (paintNumber <= 3) {
g.setColor(Color.gray);
}
// draw dashed line
((Graphics2D)g).setStroke(dashed);
g.drawLine(GAME_WIDTH/2, 0, GAME_WIDTH/2, GAME_HEIGHT);
// set font to Windows default (Segoe)
Font font = new Font("Segoe UI", Font.PLAIN, 64);
g.setFont(font);
// draw score
drawCenteredString(g, 100, 0, GAME_WIDTH/2, Integer.toString(leftScore));
drawCenteredString(g, 100, GAME_WIDTH/2, GAME_WIDTH/2, Integer.toString(rightScore));
// draw game over text on win
if (leftWin) {
drawCenteredString(g, 200, 0, GAME_WIDTH/2, "Winner");
drawCenteredString(g, 200, GAME_WIDTH/2, GAME_WIDTH/2, "Loser");
g.setFont(new Font("Segoe UI", Font.ITALIC, 24));
drawCenteredString(g, 400, 0, GAME_WIDTH, "Press ENTER to play again");
} else if (rightWin) {
drawCenteredString(g, 200, 0, GAME_WIDTH/2, "Loser");
drawCenteredString(g, 200, GAME_WIDTH/2, GAME_WIDTH/2, "Winner");
g.setFont(new Font("Segoe UI", Font.ITALIC, 24));
drawCenteredString(g, 400, 0, GAME_WIDTH, "Press ENTER to play again");
}
// if game hasn't started yet, draw game start text
g.setColor(Color.white);
g.setFont(new Font("Segoe UI", Font.BOLD, 64));
if (paintNumber == 0) {
if (resumeGame) {
drawCenteredString(g, 200, 0, GAME_WIDTH, "Resuming...");
} else {
drawCenteredString(g, 200, 0, GAME_WIDTH, "Starting in...");
}
} else if (paintNumber == 1) {
drawCenteredString(g, 200, 0, GAME_WIDTH, "3");
} else if (paintNumber == 2) {
drawCenteredString(g, 200, 0, GAME_WIDTH, "2");
} else if (paintNumber == 3) {
drawCenteredString(g, 200, 0, GAME_WIDTH, "1");
}
}
// draws a centered string
private void drawCenteredString(Graphics g, int y, int xOffset, int xWidth, String text) {
int x;
// get font size
FontMetrics metrics = g.getFontMetrics();
// determine x for the text
x = xOffset + (xWidth - metrics.stringWidth(text)) / 2;
// draw centered string
g.drawString(text, x, y);
}
//call the move methods in other classes to update positions
//this method is constantly called from run(). By doing this, movements appear fluid and natural. If we take this out the movements appear sluggish and laggy
public void move(){
ball.move();
leftPaddle.move();
rightPaddle.move();
}
//handles all collision detection and responds accordingly
public void checkCollision(){
// reverses ball y direction if ball hits top or bottom of screen
if(ball.y <= 0 || ball.y >= GAME_HEIGHT - Ball.BALL_DIAMETER){
ball.yVelocity *= -1;
}
// updates score if ball hits border of screen
if(ball.x <= 0){
// prevents ball from glitching through the paddles
// it doesn't look great but it prevents the player from being unhappy
wait(3, 0.0);
if (!leftPaddle.checkCollision(ball, 'L')) {
// increment score
rightScore += 1;
// write score to file to allow resuming
try {
writeScoreToFile();
} catch (IOException ignored) {}
regenerateBall('L');
}
}
if(ball.x + Ball.BALL_DIAMETER >= GAME_WIDTH){
wait(3, 0.0);
if (!rightPaddle.checkCollision(ball, 'R')) {
leftScore += 1;
try {
writeScoreToFile();
} catch (IOException ignored) {}
regenerateBall('R');
}
}
// delegates paddle remaining paddle checks to
leftPaddle.checkCollision(ball, 'L');
rightPaddle.checkCollision(ball, 'R');
}
//run() method is what makes the game continue running without end. It calls other methods to move objects, check for collision, and update the screen
public void run(){
double delta = 0;
Scanner fileReader;
// load score of previous game if it exists
try {
File playedFile = new File(saveFile);
if (!playedFile.createNewFile()) {
fileReader = new Scanner(playedFile);
leftScore = Integer.parseInt(fileReader.next());
rightScore = Integer.parseInt(fileReader.next());
// display resume text only if both scores are not equal to zero
resumeGame = leftScore + rightScore != 0;
// fileReader is never used again, so it is closed here
fileReader.close();
}
} catch (IOException | NoSuchElementException | NumberFormatException ignored) {}
// text to be painted before game starts
repaint(); // Resuming/starting text
try {
Thread.sleep(2000);
} catch (InterruptedException ignored) {}
paintNumber++; // pN = 1
repaint(); // 3...
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {}
paintNumber++; // pN = 2
repaint(); // 2...
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {}
paintNumber++; // pN = 3
repaint(); // 1...
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {}
paintNumber++; // pN no longer matters
repaint();
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {}
while (true) {
// wait 0.5 ticks per cycle to prevent the game from running too fast
// 0.5 was used instead of 1 to reduce lag
delta = wait(0.5, delta);
// only repaint if game is resumed after win
if (leftWin || rightWin) {
continue;
}
//only move objects around and update screen if enough time has passed
move();
checkCollision();
checkForWin();
repaint();
}
}
// sets leftWin or rightWin to true if their score is greater than WIN_SCORE
private void checkForWin() {
if (leftScore >= WIN_SCORE) {
leftWin = true;
}
else if (rightScore >= WIN_SCORE) {
rightWin = true;
}
}
//if a key is pressed, we'll send it over to the PlayerBall class for processing
public void keyPressed(KeyEvent e){
// resumes game and resets win booleans and scores when enter is pressed
if (e.getKeyCode() == KeyEvent.VK_ENTER && (leftWin || rightWin)) {
leftWin = false;
leftScore = 0;
rightWin = false;
rightScore = 0;
}
leftPaddle.keyPressed(e);
rightPaddle.keyPressed(e);
}
//if a key is released, we'll send it over to the PlayerBall class for processing
public void keyReleased(KeyEvent e){
leftPaddle.keyReleased(e);
rightPaddle.keyReleased(e);
}
//left empty because we don't need it; must be here because it is required to be overridded by the KeyListener interface
public void keyTyped(KeyEvent e){
}
// regenerates ball after it scores
public void regenerateBall(char winner) {
// overwrite old ball object with new ball that starts in middle
ball = new Ball((GAME_WIDTH-Ball.BALL_DIAMETER)/2, (GAME_HEIGHT-Ball.BALL_DIAMETER)/2, winner == 'L' ? -1:1);
}
// write score to file after each goal
public void writeScoreToFile() throws IOException {
File playedFile = new File(saveFile);
FileWriter fileWriter = new FileWriter(playedFile);
fileWriter.write(((leftScore < 5 && rightScore < 5) ? leftScore:0) + " " + ((leftScore < 5 && rightScore < 5) ? rightScore:0));
fileWriter.close();
}
// public static function that waits for deltaAmount - previousTime ticks
// returns additional ticks if wait() took longer than deltaAmount - previousTime
// this allows it to run faster in response to lag
public static double wait(double deltaAmount, double previousTime) {
//the CPU runs our game code too quickly - we need to slow it down! The following lines of code "force" the computer to get stuck in a loop for short intervals between calling other methods to update the screen.
long lastTime = System.nanoTime();
double amountOfTicks = 60;
double ns = 1000000000 / amountOfTicks;
double delta = previousTime;
long now;
while (true) { // this is the delay loop
now = System.nanoTime();
delta = delta + (now - lastTime) / ns;
lastTime = now;
if (delta >= deltaAmount) {
return delta - deltaAmount;
}
}
}
}

42
src/GenericObject.java Normal file
View File

@ -0,0 +1,42 @@
/* Eric Li, ICS4U, Completed 5/29/2022
GenericObject class provides template that Ball and Paddle inherits */
import java.awt.*;
import java.awt.event.*;
public class GenericObject extends Rectangle{
public int yVelocity;
public int xVelocity;
//constructor creates ball at given location with given dimensions
public GenericObject(int x, int y, int width, int height){
super(x, y, width, height);
}
public void keyPressed (KeyEvent e) {}
public void keyReleased (KeyEvent e) {}
//called whenever the movement of the ball changes in the y-direction (up/down)
public void setYDirection(int yDirection){
yVelocity = yDirection;
}
//called whenever the movement of the ball changes in the x-direction (left/right)
public void setXDirection(int xDirection){
xVelocity = xDirection;
}
//called frequently from both PlayerBall class and GamePanel class
//updates the current location of the ball
public void move(){
y = y + yVelocity;
x = x + xVelocity;
}
//called frequently from the GamePanel class
//draws the current location of the ball to the screen
public void draw(Graphics g) {}
}

15
src/Main.java Normal file
View File

@ -0,0 +1,15 @@
/* Eric Li, ICS4U, Completed 5/29/2022
Main class starts the game
All it does is run the constructor in GameFrame class
This is a common technique among coders to keep things organized (and handy when coding in repl.it since we're forced to call a class Main, which isn't always very descriptive)
*/
class Main {
public static void main(String[] args) {
new GameFrame();
}
}

82
src/Paddle.java Normal file
View File

@ -0,0 +1,82 @@
/* Eric Li, ICS4U, Completed 5/29/2022
Paddle class defines behaviours for the left and right player-controlled paddles */
import java.awt.*;
import java.awt.event.KeyEvent;
public class Paddle extends GenericObject {
public final int SPEED = 5;
public static final int PADDLE_WIDTH = 8;
public static final int PADDLE_HEIGHT = 80;
public int upKey, downKey;
public Paddle(int x, int y, int upKey, int downKey) {
super(x, y, PADDLE_WIDTH, PADDLE_HEIGHT);
this.upKey = upKey;
this.downKey = downKey;
}
// this is private because it's only intended to be called from checkCollision
// this checks whether the ball has collided with the paddle in either the 'L' or the 'R' direction, and returns a boolean
private boolean hasCollided(Ball ball, char direction) {
// while this.x and this.y were not needed, they were added to prevent confusion with ball.x and ball.y
if ((Math.abs(ball.x - this.x - this.xVelocity) <= Paddle.PADDLE_WIDTH && direction == 'L' && Math.signum(ball.xVelocity) == -1.0) ||
(Math.abs(ball.x - this.x - this.xVelocity) <= Ball.BALL_DIAMETER && direction == 'R' && Math.signum(ball.xVelocity) == 1.0)) {
return (this.y - ball.y <= Ball.BALL_DIAMETER && this.y - ball.y >= 0) ||
(ball.y - this.y <= Paddle.PADDLE_HEIGHT && ball.y - this.y >= 0);
}
return false;
}
// this uses hasCollided() to check if a collision has happened; if it has, the xDirection is reversed and the speed is changed
// returns a boolean which is used for sanity checking in GamePanel.checkCollision()
public boolean checkCollision(Ball ball, char direction) {
if (hasCollided(ball, direction)) {
// reverse x direction and add a portion of paddle speed
// also adds random spray
ball.yVelocity += this.yVelocity / 4 + (int)(Math.random() * 3 - 1);
ball.xVelocity = -(ball.xVelocity) + (int)(Math.random() * 3 - 1);
return true;
}
return false;
}
// moves paddle when key is pressed
public void keyPressed(KeyEvent e) {
if(e.getKeyCode() == upKey){
setYDirection(SPEED*-1);
move();
}
if(e.getKeyCode() == downKey){
setYDirection(SPEED);
move();
}
}
// stops moving paddle when key is released
public void keyReleased(KeyEvent e) {
if(e.getKeyCode() == upKey){
setYDirection(0);
move();
}
if(e.getKeyCode() == downKey){
setYDirection(0);
move();
}
}
// calls parent then does sanity checking to ensure that the paddle does not leave the screen
public void move() {
super.move();
// collisions logic for paddles to prevent them from going into the borders
if (y + PADDLE_HEIGHT + 5 > GamePanel.GAME_HEIGHT) {
y = GamePanel.GAME_HEIGHT - PADDLE_HEIGHT - 5;
} else if (y < 5) {
y = 5;
}
}
public void draw(Graphics g) {
g.setColor(Color.WHITE);
g.fillRect(x, y, PADDLE_WIDTH, PADDLE_HEIGHT);
}
}