diff --git a/src/main/dev/lh/Food.java b/src/main/dev/lh/Food.java new file mode 100644 index 0000000..484e21c --- /dev/null +++ b/src/main/dev/lh/Food.java @@ -0,0 +1,54 @@ +package dev.lh; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Rectangle; + +/** + * Represents a food item. + *
+ * Project: Snake
+ * File: Food.java
+ * Created: 01.07.2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Snake 1.2
+ */
+public final class Food implements Updateable {
+
+ private final Color color;
+ private final int lengthBonus;
+ private final Rectangle bounds;
+
+ /**
+ * Constructs a food item.
+ *
+ * @param color the color of the food item
+ * @param lengthBonus the length added to the snake when the food item is eaten
+ * @param bounds the bounds of the food item
+ * @since Snake 1.2
+ */
+ public Food(Color color, int lengthBonus, Rectangle bounds) {
+ this.color = color;
+ this.lengthBonus = lengthBonus;
+ this.bounds = bounds;
+ }
+
+ @Override
+ public void render(Graphics2D g) {
+ g.setColor(color);
+ g.fill(bounds);
+ }
+
+ /**
+ * @return the length added to the snake when the food item is eaten
+ * @since Snake 1.2
+ */
+ public int getLengthBonus() { return lengthBonus; }
+
+ /**
+ * @return the bounds of the food item
+ * @since Snake 1.2
+ */
+ public Rectangle getBounds() { return bounds; }
+}
diff --git a/src/main/dev/lh/FoodFactory.java b/src/main/dev/lh/FoodFactory.java
index f7c0b23..8811273 100755
--- a/src/main/dev/lh/FoodFactory.java
+++ b/src/main/dev/lh/FoodFactory.java
@@ -1,209 +1,69 @@
package dev.lh;
-import java.awt.*;
+import static java.awt.Color.*;
+
+import java.awt.Color;
+import java.awt.Rectangle;
import java.util.Random;
-import dev.lh.ui.GameWindow;
-
/**
+ * Generates food items with predefined properties at random positions and calculates the next
+ * spawning time.
+ *
* Project: Snake
* File: FoodFactory.java
* Created: 11 Mar 2020
*
* @author Leon Hofmeister
+ * @author Kai S. K. Engelbart
* @since Snake 1.0
*/
-public class FoodFactory {
+public final class FoodFactory {
+
+ private int width, height;
+ private long nextSpawnTime;
+ private Random random = new Random();
+
+ private static final Color[] FOOD_COLORS = {
+ WHITE, YELLOW, ORANGE, RED, BLUE
+ };
+ private static final int[] FOOD_LENGTH_BONUSES = {
+ 40, 15, 6, 2, 1
+ };
/**
- * This enum contains all possible variations of foods. The higher the ordinal
- * of an element, the less it is worth.
- *
- * Project: Snake
- * File: FoodFactory.java
- * Created: 11 Mar 2020
- *
- * @author Leon Hofmeister
- * @since Snake 1.0
+ * Initializes a food factory.
+ *
+ * @param width the width of the viewport
+ * @param height the height of the viewport
+ * @since Snake 1.2
*/
- public static enum Food {
- /**
- * Use if white food is wanted.
- */
- white,
-
- /**
- * Use if yellow food is wanted.
- */
- yellow,
-
- /**
- * Use if orange food is wanted.
- */
- orange,
-
- /**
- * Use if red food is wanted.
- */
- red,
-
- /**
- * Use if blue food is wanted.
- */
- blue
- }
-
- private static FoodFactory foodFactory = new FoodFactory();
-
- private long timeOfNextFood;
-
- private Point pFood;
-
- private Food nextFood = Food.white;
-
- private int rectangleSize = 6;
-
- private FoodFactory() {}
-
- /**
- * @return the (singleton) instance of FoodFactory
- * @since Snake 1.0
- */
- public static FoodFactory getInstance() { return foodFactory; }
-
- /**
- * @return a new {@link Food} object without its position
- * @since Snake 1.0
- */
- public Food generateFood() {
- nextFood = Food.values()[new Random().nextInt(Food.values().length)];
- rectangleSize = nextFood.ordinal() + 2;
- setTimeToNextFoodMillis();
- return nextFood;
+ public FoodFactory(int width, int height) {
+ this.width = width;
+ this.height = height;
}
/**
- * Generates the amount of time that needs to pass before the next food object
- * will be constructed.
- *
- * @since Snake 1.0
+ * @return a new food item
+ * @since Snake 1.2
*/
- public void setTimeToNextFoodMillis() {
- timeOfNextFood = System.currentTimeMillis() + new Random().nextInt(15000) + 1000;
+ public synchronized Food spawn() {
+ nextSpawnTime = System.currentTimeMillis() + random.nextInt(15000) + 1000;
+ int seed = random.nextInt(5);
+ return new Food(
+ FOOD_COLORS[seed],
+ FOOD_LENGTH_BONUSES[seed],
+ new Rectangle(random.nextInt(width - 100) + 50,
+ random.nextInt(height - 100) + 50,
+ 10 + seed * 5,
+ 10 + seed * 5
+ )
+ );
}
/**
- * @return the type of the next food
- * @since Snake 1.0
+ * @return the time after which a new food item should be spawned
+ * @since Snake 1.2
*/
- public Food getNextFood() { return nextFood; }
-
- /**
- * @param nextFood the type the next food should have
- * @since Snake 1.0
- */
- public void setNext(Food nextFood) { this.nextFood = nextFood; }
-
- /**
- * @return the time at which a new food object will be automatically created
- * @since Snake 1.0
- */
- public long getTimeOfNextFood() { return timeOfNextFood; }
-
- /**
- * @param width the width of the currently used {@link GameWindow}
- * @param height the height of the currently used {@link GameWindow}
- * @return the position of the new {@link Food} object
- * @since Snake 1.0
- */
- public Point generateFoodLocation(int width, int height) {
- assert (width > 100 && height > 100);
- Random r = new Random();
- return pFood = new Point(r.nextInt(width - 100) + 50, r.nextInt(height - 100) + 50);
- }
-
- /**
- * @return the size of the corresponding food (length = width)
- * @since Snake 1.0
- */
- public int getRectangleSize() { return rectangleSize; }
-
- /**
- * @return the location of the currently displayed food
- * @since Snake 1.0
- */
- public Point getFoodLocation() { return pFood; }
-
- /**
- * Sets the color of the given {@link Graphics} object according to the type of
- * food.
- *
- * @param g the graphics object to paint
- * @since Snake 1.0
- */
- public void colorOfFood(Graphics g) {
- switch (nextFood) {
- case white:
- g.setColor(Color.white);
- break;
- case yellow:
- g.setColor(Color.yellow);
- break;
- case orange:
- g.setColor(Color.orange);
- break;
- case red:
- g.setColor(Color.red);
- break;
- case blue:
- g.setColor(Color.blue);
- break;
- }
- }
-
- /**
- * @param g the {@link Graphics} object used to paint the current food object
- * @since Snake 1.0
- */
- public void paintFood(Graphics g) {
- colorOfFood(g);
- g.fillRect(pFood.x, pFood.y, 5 * rectangleSize, 5 * rectangleSize);
- }
-
- /**
- * @param snakeHead the the head of a {@link Snake} object
- * @return true if the current food intersects with the snakehead
- * @since Snake 1.0
- */
- public boolean checkCollision(Rectangle snakeHead) {
- int s = rectangleSize * 5;
- Rectangle food = new Rectangle(pFood, new Dimension(s, s));
- return food.intersects(snakeHead);
- }
-
- /**
- * @return the length that will be added to the snake
- * @since Snake 1.0
- */
- public int getAdditionalLength() {
- int snakeAdditionalLength = 0;
- switch (nextFood) {
- case white:
- snakeAdditionalLength = 40;
- break;
- case yellow:
- snakeAdditionalLength = 15;
- break;
- case orange:
- snakeAdditionalLength = 6;
- break;
- case red:
- snakeAdditionalLength = 2;
- break;
- case blue:
- snakeAdditionalLength = 1;
- break;
- }
- return snakeAdditionalLength;
- }
+ public long getNextSpawnTime() { return nextSpawnTime; }
}
diff --git a/src/main/dev/lh/Handler.java b/src/main/dev/lh/Handler.java
new file mode 100644
index 0000000..9c00d10
--- /dev/null
+++ b/src/main/dev/lh/Handler.java
@@ -0,0 +1,54 @@
+package dev.lh;
+
+import java.awt.Graphics2D;
+
+/**
+ * Manages the state of game objects.
+ *
+ * Project: Snake
* Project: Snake
+ * Project: Snake
+ * File: Handler.java
+ * Created: 01.07.2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Snake 1.2
+ */
+public final class Handler implements Updateable {
+
+ private Snake snake = new Snake(7);
+ private FoodFactory foodFactory;
+
+ private volatile Food food;
+
+ /**
+ * Constructs a handler.
+ *
+ * @param snake the snake
+ * @param foodFactory the food factory
+ * @since Snake 1.2
+ */
+ public Handler(Snake snake, FoodFactory foodFactory) {
+ this.snake = snake;
+ this.foodFactory = foodFactory;
+ food = foodFactory.spawn();
+ }
+
+ @Override
+ public void tick() {
+ snake.tick();
+ food.tick();
+
+ // Check for food collision
+ if (snake.getHead().intersects(food.getBounds())) {
+ snake.addLength(food.getLengthBonus());
+ food = foodFactory.spawn();
+ }
+ if (System.currentTimeMillis() > foodFactory.getNextSpawnTime())
+ food = foodFactory.spawn();
+ }
+
+ @Override
+ public void render(Graphics2D g) {
+ snake.render(g);
+ food.render(g);
+ }
+}
diff --git a/src/main/dev/lh/Snake.java b/src/main/dev/lh/Snake.java
index 2c41ed3..f0fa54d 100644
--- a/src/main/dev/lh/Snake.java
+++ b/src/main/dev/lh/Snake.java
@@ -1,11 +1,12 @@
package dev.lh;
-import java.awt.*;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.List;
import dev.lh.ui.Endscreen;
-import dev.lh.ui.GameWindow;
/**
* Project: Snake
@@ -31,30 +32,30 @@ public class Snake implements Updateable {
/**
* Use if the snake should head left.
*/
- Left,
+ LEFT,
/**
* Use if the snake should head right.
*/
- Right,
+ RIGHT,
/**
* Use if the snake should head up.
*/
- Up,
+ UP,
/**
* Use if the snake should head down.
*/
- Down;
+ DOWN;
}
- private static FoodFactory foodFactory = FoodFactory.getInstance();
- private static Endscreen endscreen;
- private Direction direction;
- private int length;
- private List
- *
+ * This interface contains everything that needs to be updated regularly.
+ *
* File: Updateable.java
* Created: 11 Mar 2020
@@ -18,15 +18,15 @@ public interface Updateable {
* Here should the actions be implemented that are supposed to happen when a new
* frame gets created.
*
- * @since Snake 1.0
+ * @since Snake 1.2
*/
- void nextFrame();
+ default void tick() {}
/**
* Renders the object.
*
- * @param g the {@link Graphics} object that is used to render this object
+ * @param g the graphics object that is used to render this object
* @since Snake 1.0
*/
- void render(Graphics g);
+ default void render(Graphics2D g) {}
}
diff --git a/src/main/dev/lh/Viewport.java b/src/main/dev/lh/Viewport.java
new file mode 100644
index 0000000..874c436
--- /dev/null
+++ b/src/main/dev/lh/Viewport.java
@@ -0,0 +1,98 @@
+package dev.lh;
+
+import java.awt.Canvas;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.util.Timer;
+import java.util.TimerTask;
+
+/**
+ * Implements a hardware-accelerated rendering loop.
+ *
+ * File: Viewport.java
+ * Created: 01.07.2020
+ *
+ * @author Kai S. K. Engelbart
+ * @since Snake 1.2
+ */
+public class Viewport extends Canvas {
+
+ private static final long serialVersionUID = 1L;
+
+ // Enable OpenGL hardware acceleration
+ static {
+ System.setProperty("sun.java2d.trace", "timestamp,log,count");
+ System.setProperty("sun.java2d.transaccel", "True");
+ System.setProperty("sun.java2d.opengl", "True");
+ }
+
+ private Updateable gameRoot;
+ private Timer timer = new Timer();
+ private TimerTask renderTask;
+
+ /**
+ * @param gameRoot the game object responsible for updating the rest
+ * @since Snake 1.2
+ */
+ public Viewport(Updateable gameRoot) {
+ this.gameRoot = gameRoot;
+ setIgnoreRepaint(true);
+ }
+
+ /**
+ * Starts the render task.
+ *
+ * @since Snake 1.2
+ */
+ public void start() {
+ if (renderTask != null)
+ renderTask.cancel();
+ else
+ createBufferStrategy(2);
+
+ renderTask = new TimerTask() {
+
+ private long lastTime = System.currentTimeMillis();
+
+ @Override
+ public void run() {
+ long time = System.currentTimeMillis();
+ double dt = (time - lastTime) * 1E-3;
+ lastTime = time;
+ // TODO: Delta time adjustment
+ gameRoot.tick();
+ render();
+ }
+ };
+
+ timer.schedule(renderTask, 0, 100);
+ }
+
+ /**
+ * Stops the render task.
+ *
+ * @since Snake 1.2
+ */
+ public void stop() {
+ renderTask.cancel();
+ }
+
+ private void render() {
+ Graphics2D g = (Graphics2D) getBufferStrategy().getDrawGraphics();
+
+ // Clear the screen
+ g.setColor(Color.BLACK);
+ g.fillRect(0, 0, getWidth(), getHeight());
+
+ // Perform the actual rendering
+ gameRoot.render(g);
+
+ // Flip buffers
+ g.dispose();
+ getBufferStrategy().show();
+
+ // Synchronize with display refresh rate
+ getToolkit().sync();
+ }
+}
diff --git a/src/main/dev/lh/ui/Endscreen.java b/src/main/dev/lh/ui/Endscreen.java
index be5cb30..56fdc07 100644
--- a/src/main/dev/lh/ui/Endscreen.java
+++ b/src/main/dev/lh/ui/Endscreen.java
@@ -20,9 +20,9 @@ public class Endscreen extends JDialog {
private static final long serialVersionUID = -4457484397259161063L;
- private static final int goodOrBadResult = 200;
- private final JPanel contentPanel = new JPanel();
- private final int score;
+ private static final int goodOrBadResult = 200;
+ private final JPanel contentPanel = new JPanel();
+ private final int score;
/**
* Create the dialog.
@@ -31,32 +31,33 @@ public class Endscreen extends JDialog {
*/
public Endscreen(int score) {
this.score = score;
- try {
+ setTitle("Endscreen");
+ setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
+ setBounds(100, 100, 700, 700);
+ getContentPane().setLayout(new BorderLayout());
+ contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
+ contentPanel.setLayout(new BorderLayout(0, 0));
+ getContentPane().add(contentPanel, BorderLayout.CENTER);
- setTitle("Endscreen");
- setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
- setBounds(100, 100, 700, 700);
- getContentPane().setLayout(new BorderLayout());
- contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
- contentPanel.setLayout(new BorderLayout(0, 0));
- getContentPane().add(contentPanel, BorderLayout.CENTER);
+ JButton btnNewButton = new JButton("Play again");
+ btnNewButton.setMnemonic(KeyEvent.VK_ENTER);
+ btnNewButton.addActionListener(e -> {
+ Main.startGame();
+ dispose();
+ });
+ btnNewButton.setFont(new Font("Times New Roman", Font.PLAIN, 15));
+ contentPanel.add(btnNewButton, BorderLayout.SOUTH);
- JButton btnNewButton = new JButton("Play again");
- btnNewButton.setMnemonic(KeyEvent.VK_ENTER);
- btnNewButton.addActionListener(e -> { Main.startGame(); dispose(); });
- btnNewButton.setFont(new Font("Times New Roman", Font.PLAIN, 15));
- contentPanel.add(btnNewButton, BorderLayout.SOUTH);
+ JLabel lblDeinPunktestand = new JLabel("Dein Punktestand: " + String.valueOf(score));
+ lblDeinPunktestand.setFont(new Font("Times New Roman", Font.PLAIN, 25));
+ contentPanel.add(lblDeinPunktestand, BorderLayout.NORTH);
- JLabel lblDeinPunktestand = new JLabel("Dein Punktestand: " + String.valueOf(score));
- lblDeinPunktestand.setFont(new Font("Times New Roman", Font.PLAIN, 25));
- contentPanel.add(lblDeinPunktestand, BorderLayout.NORTH);
-
- Image resultImage = Toolkit.getDefaultToolkit()
- .getImage(this.getClass().getResource((score < goodOrBadResult) ? "/Try_Again.jpg" : "/1211548-200.png"));
- resultImage.flush();
- } catch (Exception e) {
- e.printStackTrace();
- }
+ Image resultImage = Toolkit.getDefaultToolkit()
+ .getImage(
+ this.getClass()
+ .getResource((score < goodOrBadResult) ? "/Try_Again.jpg" : "/1211548-200.png")
+ );
+ resultImage.flush();
}
/**
diff --git a/src/main/dev/lh/ui/GameWindow.java b/src/main/dev/lh/ui/GameWindow.java
index f315436..d190736 100755
--- a/src/main/dev/lh/ui/GameWindow.java
+++ b/src/main/dev/lh/ui/GameWindow.java
@@ -1,15 +1,14 @@
package dev.lh.ui;
-import java.awt.*;
+import java.awt.Dimension;
+import java.awt.Rectangle;
+import java.awt.Toolkit;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import javax.swing.JFrame;
-import javax.swing.JPanel;
-import javax.swing.Timer;
-import dev.lh.FoodFactory;
-import dev.lh.Snake;
+import dev.lh.*;
import dev.lh.Snake.Direction;
/**
@@ -22,21 +21,20 @@ import dev.lh.Snake.Direction;
*/
public class GameWindow extends JFrame {
- private static final long serialVersionUID = 1L;
- private Snake s = new Snake(7);
- private FoodFactory foodFactory = FoodFactory.getInstance();
- private Timer timer;
+ private static final long serialVersionUID = 1L;
+
+ private Viewport viewport;
/**
* @param title the title of the frame
* @since Snake 1.0
*/
public GameWindow(String title) {
+ // Initialize window
super(title);
Dimension size = Toolkit.getDefaultToolkit().getScreenSize();
setBounds(new Rectangle(size));
setLocation(0, 0);
- setLocationRelativeTo(null);
setMinimumSize(size);
setPreferredSize(size);
setMaximumSize(size);
@@ -44,19 +42,14 @@ public class GameWindow extends JFrame {
setResizable(false);
setDefaultCloseOperation(EXIT_ON_CLOSE);
- add(new JPanel() {
+ // Initialize game objects
+ Snake snake = new Snake(7);
+ FoodFactory foodFactory = new FoodFactory(getWidth(), getHeight());
+ Handler handler = new Handler(snake, foodFactory);
- private static final long serialVersionUID = 1L;
-
- @Override
- protected void paintComponent(Graphics g) {
- super.paintComponent(g);
- g.setColor(Color.black);
- g.fillRect(0, 0, super.getWidth(), super.getHeight());
- s.render(g);
- foodFactory.paintFood(g);
- }
- });
+ // Initialize viewport
+ viewport = new Viewport(handler);
+ add(viewport);
addKeyListener(new KeyAdapter() {
@@ -66,56 +59,40 @@ public class GameWindow extends JFrame {
switch (e.getKeyCode()) {
case KeyEvent.VK_W:
case KeyEvent.VK_UP:
- if (!s.getRichtung().equals(Direction.Down)) s.setDirection(Direction.Up);
+ if (!snake.getDirection().equals(Direction.DOWN))
+ snake.setDirection(Direction.UP);
break;
case KeyEvent.VK_A:
case KeyEvent.VK_LEFT:
- if (!s.getRichtung().equals(Direction.Right)) s.setDirection(Direction.Left);
+ if (!snake.getDirection().equals(Direction.RIGHT))
+ snake.setDirection(Direction.LEFT);
break;
case KeyEvent.VK_S:
case KeyEvent.VK_DOWN:
- if (!s.getRichtung().equals(Direction.Up)) s.setDirection(Direction.Down);
+ if (!snake.getDirection().equals(Direction.UP))
+ snake.setDirection(Direction.DOWN);
break;
case KeyEvent.VK_D:
case KeyEvent.VK_RIGHT:
- if (!s.getRichtung().equals(Direction.Left)) s.setDirection(Direction.Right);
+ if (!snake.getDirection().equals(Direction.LEFT))
+ snake.setDirection(Direction.RIGHT);
break;
}
}
});
- newFood();
- timer = new Timer(
- 50,
- evt -> {
- s.nextFrame();
- if (System.currentTimeMillis() >= foodFactory.getTimeOfNextFood())
- newFood();
- repaint();
- }
- );
- timer.start();
-
setVisible(true);
+ viewport.start();
}
/**
- * Generates new food.
- *
- * @since Snake 1.1
- */
- public void newFood() {
- foodFactory.generateFood();
- foodFactory.generateFoodLocation(getWidth(), getHeight());
- }
-
- /**
- * Disposes this frame
+ * Disposes this frame.
*
* @since Snake 1.1
*/
public void close() {
- timer.stop();
+ viewport.stop();
+ setVisible(false);
dispose();
}
}
diff --git a/src/main/dev/lh/ui/StartScreen.java b/src/main/dev/lh/ui/StartScreen.java
index c8d3cb9..0b908d7 100755
--- a/src/main/dev/lh/ui/StartScreen.java
+++ b/src/main/dev/lh/ui/StartScreen.java
@@ -21,13 +21,14 @@ import dev.lh.Main;
*/
public class StartScreen extends JFrame {
- private static final long serialVersionUID = 6055940532003735543L;
- private JPanel contentPane;
+ private static final long serialVersionUID = 6055940532003735543L;
/**
* Closes the application.
*/
- public static void close() { System.exit(0); }
+ public static void close() {
+ System.exit(0);
+ }
/**
* Launches Snake.
@@ -43,31 +44,27 @@ public class StartScreen extends JFrame {
* Create the frame.
*/
public StartScreen() {
- try {
- // readInHighscores();
- setTitle("Snake - Startscreen");
- setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
- setBounds(500, 200, 550, 550);
- contentPane = new JPanel();
- contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
- setContentPane(contentPane);
+ setTitle("Snake - Startscreen");
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ setBounds(500, 200, 550, 550);
- JButton buPlay = new JButton("Start Game");
- buPlay.setBounds(158, 197, 190, 131);
- buPlay.setText("Play Again");
- buPlay.setMnemonic(KeyEvent.VK_ENTER);
- buPlay.setFont(new Font("Times New Roman", Font.PLAIN, 16));
- buPlay.addActionListener(a -> {
- Main.startGame();
- setVisible(false);
- dispose();
- System.gc();
- });
- contentPane.add(buPlay);
- contentPane.setLayout(null);
- setVisible(true);
- } catch (Exception e) {
- e.printStackTrace();
- }
+ JPanel contentPane = new JPanel();
+ contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
+ setContentPane(contentPane);
+
+ JButton buPlay = new JButton("Start Game");
+ buPlay.setBounds(158, 197, 190, 131);
+ buPlay.setText("Play Again");
+ buPlay.setMnemonic(KeyEvent.VK_ENTER);
+ buPlay.setFont(new Font("Times New Roman", Font.PLAIN, 16));
+ buPlay.addActionListener(a -> {
+ Main.startGame();
+ setVisible(false);
+ dispose();
+ System.gc();
+ });
+ contentPane.add(buPlay);
+ contentPane.setLayout(null);
+ setVisible(true);
}
}