diff --git a/src/main/dev/lh/Food.java b/src/main/dev/lh/Food.java new file mode 100644 index 0000000..560ed93 --- /dev/null +++ b/src/main/dev/lh/Food.java @@ -0,0 +1,40 @@ +package dev.lh; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Rectangle; + +/** + * Project: Snake
+ * File: Food.java
+ * Created: 01.07.2020
+ * + * @author Kai S. K. Engelbart + * @since Snake 1.1 + */ +public final class Food implements Updateable { + + private final Color color; + private final int lengthBonus; + private final Rectangle bounds; + + public Food(Color color, int lengthBonus, Rectangle bounds) { + this.color = color; + this.lengthBonus = lengthBonus; + this.bounds = bounds; + } + + public void checkCollision(Snake snake) { + if (bounds.intersects(snake.getHead())) {} + } + + @Override + public void render(Graphics2D g) { + g.setColor(color); + g.fill(bounds); + } + + public int getLengthBonus() { return lengthBonus; } + + public Rectangle getBounds() { return bounds; } +} diff --git a/src/main/dev/lh/FoodFactory.java b/src/main/dev/lh/FoodFactory.java index 9e597e0..7de2c2c 100755 --- a/src/main/dev/lh/FoodFactory.java +++ b/src/main/dev/lh/FoodFactory.java @@ -2,202 +2,68 @@ package dev.lh; import static java.awt.Color.*; -import java.awt.*; +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.1 */ - public enum Food { + public FoodFactory(int width, int height) { + this.width = width; + this.height = height; + } - /** - * Use if white food is wanted. - */ - white( - WHITE, 40 - ), - - /** - * Use if yellow food is wanted. - */ - yellow( - YELLOW, 15 - ), - - /** - * Use if orange food is wanted. - */ - orange( - ORANGE, 6 - ), - - /** - * Use if red food is wanted. - */ - red( - RED, 2 - ), - - /** - * Use if blue food is wanted. - */ - blue( - BLUE, 1 + /** + * @return a new food item + * @since Snake 1.1 + */ + 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, + seed * 10, + seed * 10 + ) ); - - /** - * The color of the food item. - */ - public final Color color; - - /** - * The length bonus of the food item. - */ - public final int lengthBonus; - - private Food(Color color, int lengthBonus) { - this.color = color; - this.lengthBonus = lengthBonus; - } - } - - 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; } /** - * Generates the amount of time that needs to pass before the next food object - * will be constructed. - * - * @since Snake 1.0 + * @return the time after which a new food item should be spawned + * @since Snake 1.1 */ - public void setTimeToNextFoodMillis() { - timeOfNextFood = System.currentTimeMillis() + new Random().nextInt(15000) + 1000; - } - - /** - * @return the type of the next food - * @since Snake 1.0 - */ - 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) { - g.setColor(nextFood.color); - } - - /** - * @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 snake head - * @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() { - return nextFood.lengthBonus; - } + 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..c1fe515 --- /dev/null +++ b/src/main/dev/lh/Handler.java @@ -0,0 +1,46 @@ +package dev.lh; + +import java.awt.Graphics2D; + +/** + * Manages the state of game objects. + *

+ * Project: Snake
+ * File: Handler.java
+ * Created: 01.07.2020
+ * + * @author Kai S. K. Engelbart + * @since Snake 1.1 + */ +public final class Handler implements Updateable { + + private Snake snake = new Snake(7); + private FoodFactory foodFactory; + + private volatile Food food; + + 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 6fda5aa..a774a9b 100644 --- a/src/main/dev/lh/Snake.java +++ b/src/main/dev/lh/Snake.java @@ -7,7 +7,6 @@ import java.util.ArrayList; import java.util.List; import dev.lh.ui.Endscreen; -import dev.lh.ui.GameWindow; /** * Project: Snake
@@ -51,7 +50,6 @@ public class Snake implements Updateable { DOWN; } - private static FoodFactory foodFactory = FoodFactory.getInstance(); private static Endscreen endscreen; private Direction direction = Direction.RIGHT; private int length; @@ -103,14 +101,8 @@ public class Snake implements Updateable { Main.getGame().close(); } - /** - * @return the current {@link Direction} of the snake - * @since Snake 1.0 - */ - public Direction getRichtung() { return direction; } - @Override - public void nextFrame() { + public void tick() { int velX = 0, velY = 0; switch (direction) { case UP: @@ -140,18 +132,11 @@ public class Snake implements Updateable { return; } // TODO: Test on Linux - if (!Main.getGame().getBounds().contains(tiles.get(0))) { + if (!Main.getGame().getBounds().contains(getHead())) { gameOver(); System.out.println("Snake went out of bounds."); return; } - // TODO: Move to Food class - // Case if snake eats food - if (foodFactory.checkCollision(tiles.get(0))) { - addLength(foodFactory.getAdditionalLength()); - GameWindow game = Main.getGame(); - game.newFood(); - } } @Override @@ -160,9 +145,21 @@ public class Snake implements Updateable { tiles.forEach(g::fill); } + /** + * @return the current {@link Direction} of the snake + * @since Snake 1.0 + */ + public Direction getDirection() { return direction; } + /** * @param direction the new {@link Direction} of the snake * @since Snake 1.0 */ public void setDirection(Direction direction) { this.direction = direction; } + + /** + * @return a rectangle representing the head of the snake + * @since Snake 1.1 + */ + public Rectangle getHead() { return tiles.get(0); } } diff --git a/src/main/dev/lh/Updateable.java b/src/main/dev/lh/Updateable.java index 272d812..1daa80d 100755 --- a/src/main/dev/lh/Updateable.java +++ b/src/main/dev/lh/Updateable.java @@ -20,7 +20,7 @@ public interface Updateable { * * @since Snake 1.0 */ - void nextFrame(); + default void tick() {} /** * Renders the object. @@ -28,5 +28,5 @@ public interface Updateable { * @param g the graphics object that is used to render this object * @since Snake 1.0 */ - void render(Graphics2D 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..9c5b6f2 --- /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. + *

+ * Project: Snake
+ * File: Viewport.java
+ * Created: 01.07.2020
+ * + * @author Kai S. K. Engelbart + * @since Snake 1.0 + */ +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.0 + */ + public Viewport(Updateable gameRoot) { + this.gameRoot = gameRoot; + setIgnoreRepaint(true); + } + + /** + * Starts the render task. + * + * @since Snake 1.1 + */ + 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; + + gameRoot.tick(); + render(); + } + }; + + timer.schedule(renderTask, 0, 100); + } + + /** + * Stops the render task. + * + * @since Snake 1.1 + */ + 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/GameWindow.java b/src/main/dev/lh/ui/GameWindow.java index 211771d..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, getWidth(), getHeight()); - s.render((Graphics2D) 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(); } }