diff options
| author | mithe24 <mithe24@student.sdu.dk> | 2025-05-14 12:30:20 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-05-14 12:30:20 +0200 |
| commit | e7563be943667b6aad5813090989b9e28424b052 (patch) | |
| tree | 407a699a7e6c85f745c706931e4896c4b2894127 | |
| parent | 06d91d0f24991ad47edabc09f43fcdb85facb18b (diff) | |
| download | pacman-e7563be943667b6aad5813090989b9e28424b052.tar.gz pacman-e7563be943667b6aad5813090989b9e28424b052.zip | |
feat/view-manager (#20)
* feat(View & Controller): Added screens for game, main menu and pause menu
Added screens:
- MainMenuView & MainMenuController.
- GameView & GameController.
- PauseView & PauseController.
* refactor(MainMenuController): MainMenuController constructs game
MainMenuController constructs games and allows the creation of a new game
without terminating the process:
* feat(View): Resource Manager for loading game textures
Add ResourceManager for centralized texture loading.
Implemented a singleton ResourceManager to handle loading of game textures.
Ensures each texture is loaded only once, improving performance and memory efficiency.
Includes fallback logic to return a default "missing texture" image
when a resource cannot be found.
Prevents accidental instantiation by enforcing the singleton pattern.
Diffstat (limited to '')
13 files changed, 415 insertions, 101 deletions
diff --git a/pacman/controller/src/main/java/com/gr15/pacman/controller/GameApp.java b/pacman/controller/src/main/java/com/gr15/pacman/controller/GameApp.java index ffd4493..98a8c5e 100644 --- a/pacman/controller/src/main/java/com/gr15/pacman/controller/GameApp.java +++ b/pacman/controller/src/main/java/com/gr15/pacman/controller/GameApp.java @@ -1,12 +1,18 @@ package com.gr15.pacman.controller; -import java.io.InputStream; - import com.gr15.pacman.model.GameState; import com.gr15.pacman.model.GameStateBuilder; -import com.gr15.pacman.view.GameView; +import com.gr15.pacman.view.ViewManager; +import com.gr15.pacman.view.screen.GameView; +import com.gr15.pacman.view.screen.MainMenuView; +import com.gr15.pacman.view.screen.PauseView; +import com.gr15.pacman.view.ViewManager.ViewKeys; +import com.gr15.pacman.controller.screen.GameController; +import com.gr15.pacman.controller.screen.MainMenuController; +import com.gr15.pacman.controller.screen.PauseController; import javafx.application.Application; +import javafx.scene.Scene; import javafx.stage.Stage; /** @@ -15,9 +21,8 @@ import javafx.stage.Stage; public class GameApp extends Application { - GameController gameController; - GameView gameView; - GameState gameState; + private ViewManager viewManager = new ViewManager(); + private MainMenuView mainMenuView = new MainMenuView(); @Override public void start(Stage primaryStage) throws Exception { @@ -25,18 +30,12 @@ public class GameApp primaryStage.setResizable(false); primaryStage.setFullScreen(true); - InputStream inputStream = this.getClass() - .getResourceAsStream("/testGameState.json"); - GameState gameState = GameStateBuilder.fromJson(inputStream); - inputStream.close(); - int tileWidth = gameState.getBoard().getWidth(); - int tileHeight = gameState.getBoard().getHeight(); - gameView = new GameView(gameState,tileWidth, tileHeight); - primaryStage.setScene(gameView); - - gameController = new GameController(gameState, gameView); - gameController.startGameLoop(); + viewManager.addView(ViewKeys.MAIN_MENU, mainMenuView); + viewManager.showView(ViewKeys.MAIN_MENU); + new MainMenuController(viewManager, mainMenuView); + Scene scene = new Scene(viewManager, 1920, 1080); + primaryStage.setScene(scene); primaryStage.show(); } diff --git a/pacman/controller/src/main/java/com/gr15/pacman/controller/GameController.java b/pacman/controller/src/main/java/com/gr15/pacman/controller/GameController.java deleted file mode 100644 index a334576..0000000 --- a/pacman/controller/src/main/java/com/gr15/pacman/controller/GameController.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.gr15.pacman.controller; - -import com.gr15.pacman.model.GameState; -import com.gr15.pacman.model.entities.Entity.Direction; -import com.gr15.pacman.view.GameView; - -import javafx.animation.AnimationTimer; - -/** - * GameController - */ -public class GameController { - - private GameState gameState; - private GameView gameView; - - private AnimationTimer gameLoop; - private long lastUpdate = 0; - - public GameController(GameState gameState, GameView gameView) { - this.gameState = gameState; - this.gameView = gameView; - - setupEventHandlers(); - gameLoop = new AnimationTimer() { - - @Override - public void handle(long now) { - if (lastUpdate == 0) { - lastUpdate = now; - return; /* returning early, since no time have elapsed */ - } - - double deltaSeconds = (now - lastUpdate) / 1_000_000_000.0; - lastUpdate = now; - - gameState.update(deltaSeconds); - gameView.renderGame(deltaSeconds); - } - }; - } - - private void setupEventHandlers() { - gameView.setOnKeyPressed(event -> { - switch (event.getCode()) { - case UP -> gameState.getPacman().setDirection(Direction.UP); - case DOWN -> gameState.getPacman().setDirection(Direction.DOWN); - case LEFT -> gameState.getPacman().setDirection(Direction.LEFT); - case RIGHT -> gameState.getPacman().setDirection(Direction.RIGHT); - default -> {} - } - }); - } - - public void startGameLoop() { - gameLoop.start(); - } - - public void stopGameLoop() { - lastUpdate = 0; - gameLoop.stop(); - } -} diff --git a/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/GameController.java b/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/GameController.java new file mode 100644 index 0000000..0bb4e87 --- /dev/null +++ b/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/GameController.java @@ -0,0 +1,72 @@ +package com.gr15.pacman.controller.screen; + +import com.gr15.pacman.model.GameState; +import com.gr15.pacman.model.entities.Entity.Direction; +import com.gr15.pacman.view.ViewManager; +import com.gr15.pacman.view.screen.GameView; +import com.gr15.pacman.view.ViewManager.ViewKeys; + +import javafx.animation.AnimationTimer; +import javafx.scene.input.KeyEvent; + +/** + * GameController + */ +public class GameController { + + private final ViewManager viewManager; + private final GameState gameState; + private final GameView gameView; + + private final AnimationTimer gameLoop; + private long lastUpdate = 0; + + public GameController(GameState gameState, GameView gameView, ViewManager viewManager) { + this.viewManager = viewManager; + this.gameState = gameState; + this.gameView = gameView; + + gameLoop = new AnimationTimer() { + + @Override + public void handle(long now) { + if (lastUpdate == 0) { + lastUpdate = now; + return; /* returning early, since no time have elapsed */ + } + + double deltaSeconds = (now - lastUpdate) / 1_000_000_000.0; + lastUpdate = now; + + gameState.update(deltaSeconds); + gameView.renderGame(deltaSeconds); + } + }; + } + + public void handleKeyEvent(KeyEvent event) { + switch (event.getCode()) { + case UP -> gameState.getPacman().setDirection(Direction.UP); + case DOWN -> gameState.getPacman().setDirection(Direction.DOWN); + case LEFT -> gameState.getPacman().setDirection(Direction.LEFT); + case RIGHT -> gameState.getPacman().setDirection(Direction.RIGHT); + case ESCAPE -> { + viewManager.showView(ViewKeys.PAUSE); + stopGameLoop(); + } + default -> {} + } + } + + public void startGameLoop() { + gameView.requestFocusForInput(); + gameView.setOnKeyPressed(event -> handleKeyEvent(event)); + + gameLoop.start(); + } + + public void stopGameLoop() { + lastUpdate = 0; + gameLoop.stop(); + } +} diff --git a/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/MainMenuController.java b/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/MainMenuController.java new file mode 100644 index 0000000..9477171 --- /dev/null +++ b/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/MainMenuController.java @@ -0,0 +1,47 @@ +package com.gr15.pacman.controller.screen; + +import com.gr15.pacman.model.GameState; +import com.gr15.pacman.model.GameStateBuilder; +import com.gr15.pacman.view.ViewManager; +import com.gr15.pacman.view.screen.GameView; +import com.gr15.pacman.view.screen.MainMenuView; +import com.gr15.pacman.view.screen.PauseView; +import com.gr15.pacman.view.ViewManager.ViewKeys; + +/** + * MainMenuController + */ +public class MainMenuController { + + private final ViewManager viewManager; + private final MainMenuView mainMenuView; + + public MainMenuController(ViewManager viewManager, MainMenuView mainMenuView) { + this.viewManager = viewManager; + this.mainMenuView = mainMenuView; + initializeButtons(); + } + + private void initializeButtons() { + mainMenuView.getStartButton().setOnAction((event) -> { + GameState gameState = GameStateBuilder.fromJson( + this.getClass().getResourceAsStream("/testGameState.json")); + int tileWidth = gameState.getBoard().getWidth(); + int tileHeight = gameState.getBoard().getHeight(); + GameView gameView = new GameView(gameState, tileWidth, tileHeight); + PauseView pauseView = new PauseView(); + GameController gameController = new GameController(gameState, + gameView, viewManager); + new PauseController(pauseView, gameController, viewManager); + viewManager.removeView(ViewKeys.GAME_VIEW); + viewManager.removeView(ViewKeys.PAUSE); + viewManager.addView(ViewKeys.GAME_VIEW, gameView); + viewManager.addView(ViewKeys.PAUSE, pauseView); + viewManager.showView(ViewKeys.GAME_VIEW); + gameController.startGameLoop(); + }); + mainMenuView.getQuitButton().setOnAction((event) -> { + System.exit(0); + }); + } +} diff --git a/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/PauseController.java b/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/PauseController.java new file mode 100644 index 0000000..88b60ea --- /dev/null +++ b/pacman/controller/src/main/java/com/gr15/pacman/controller/screen/PauseController.java @@ -0,0 +1,34 @@ +package com.gr15.pacman.controller.screen; + +import com.gr15.pacman.view.ViewManager; +import com.gr15.pacman.view.ViewManager.ViewKeys; +import com.gr15.pacman.view.screen.PauseView; + +/** + * PauseController + */ +public class PauseController { + + private final PauseView pauseView; + private final GameController gameController; + private final ViewManager viewManager; + + public PauseController(PauseView pauseView, GameController gameController, + ViewManager viewManager) { + this.gameController = gameController; + this.pauseView = pauseView; + this.viewManager = viewManager; + initialize(); + } + + private void initialize() { + pauseView.getQuitButton().setOnAction((e) -> { System.exit(0); }); + pauseView.getMainMenuButton().setOnAction((e) -> { + viewManager.showView(ViewKeys.MAIN_MENU); + }); + pauseView.getResumeButton().setOnAction((e) -> { + viewManager.showView(ViewKeys.GAME_VIEW); + gameController.startGameLoop(); + }); + } +} diff --git a/pacman/controller/src/main/java/module-info.java b/pacman/controller/src/main/java/module-info.java index 9df9dd5..c240f2c 100644 --- a/pacman/controller/src/main/java/module-info.java +++ b/pacman/controller/src/main/java/module-info.java @@ -6,5 +6,7 @@ module com.gr15.pacman.controller { requires javafx.graphics; requires com.gr15.pacman.view; requires com.gr15.pacman.model; + exports com.gr15.pacman.controller; + exports com.gr15.pacman.controller.screen; } diff --git a/pacman/view/src/main/java/com/gr15/pacman/view/ResourceManager.java b/pacman/view/src/main/java/com/gr15/pacman/view/ResourceManager.java new file mode 100644 index 0000000..3e7103e --- /dev/null +++ b/pacman/view/src/main/java/com/gr15/pacman/view/ResourceManager.java @@ -0,0 +1,77 @@ +package com.gr15.pacman.view; + +import java.util.HashMap; +import java.util.Map; + +import javafx.scene.image.Image; + +/** + * The {@code ResourceManager} class is a singleton responsible + * for managing {@link Image} resources used in the application. + * It provides caching to avoid loading the same {@link Image} multiple times, + * improving performance and resource efficiency. + * + * If an {@link Image} cannot be found at the specified path, + * a default "missing texture" {@link Image} is returned. + */ + +public class ResourceManager { + + /** Singleton instance of ResourceManager. */ + private static ResourceManager instance; + + /** Cache to store loaded images keyed by their file path. */ + private Map<String, Image> resources = new HashMap<>(); + + /** Image to be used when the requested image is not found. */ + private Image missingTexture = new Image( + this.getClass().getResourceAsStream("/gameAssets/missingTexture.png")); + + /** + * Private constructor to prevent external instantiation + * and enforce singleton pattern. + */ + private ResourceManager() {} + + /** + * Returns the singleton instance of {@code ResourceManager}. + * If it does not exist yet, it is created. + * + * @return the singleton instance of {@code ResourceManager}. + */ + public static ResourceManager getInstance() { + if (instance == null) { + instance = new ResourceManager(); + return instance; + } else { + return instance; + } + } + + /** + * Returns an {@link Image} corresponding to the specified resource path. + * If the {@link Image} has already been loaded before, + * it is returned from the cache. Otherwise, it is loaded + * and added to the cache. + * If loading fails (e.g., if the file does not exist), + * the default "missing texture" {@link Image} is returned. + * + * @param path the path to the image resource, relative to the classpath + * @return the loaded {@link Image}, + * or a default image if the resource could not be found + */ + public Image getTexture(String path) { + if (resources.containsKey(path)) { + return resources.get(path); + } else { + try { + Image texture = new Image( + this.getClass().getResourceAsStream(path)); + resources.put(path, texture); + return texture; + } catch (NullPointerException e) { + return missingTexture; + } + } + } +} diff --git a/pacman/view/src/main/java/com/gr15/pacman/view/ViewManager.java b/pacman/view/src/main/java/com/gr15/pacman/view/ViewManager.java new file mode 100644 index 0000000..2f1eb3a --- /dev/null +++ b/pacman/view/src/main/java/com/gr15/pacman/view/ViewManager.java @@ -0,0 +1,92 @@ +package com.gr15.pacman.view; + +import java.util.HashMap; +import java.util.Map; + +import javafx.geometry.Insets; +import javafx.scene.Parent; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; + +/** + * The {@code ViewManager} class is responsible for managing and switching between + * different views in a JavaFX application. It extends {@code StackPane} and provides + * methods to add, remove, check for, and display views identified by an enum key. + */ +public class ViewManager + extends StackPane { + + /** + * Enumeration of all possible view keys used to identify different views. + */ + public enum ViewKeys { MAIN_MENU, GAME_VIEW, PAUSE }; + + /** + * A map that stores views associated with their corresponding keys. + */ + private Map<ViewKeys, Parent> views = new HashMap<>(); + + /** + * Constructs a new {@code ViewManager} with a black background. + */ + public ViewManager() { + setBackground(new Background(new BackgroundFill( + Color.BLACK, + CornerRadii.EMPTY, + Insets.EMPTY + ))); + } + + /** + * Adds a new view to the manager. + * + * @param key the key to identify the view. + * @param view the view to be added. + * @throws IllegalArgumentException if a view with the same key already exists. + */ + public void addView(ViewKeys key, Parent view) { + if (views.containsKey(key)) { + throw new IllegalArgumentException( + "View with key " + key + " already exists."); + } else { + views.put(key, view); + } + } + + /** + * Displays the view associated with the specified key. + * + * @param key the key of the view to be shown. + * @throws IllegalArgumentException if no view exists for the specified key. + */ + public void showView(ViewKeys key) { + if (!views.containsKey(key)) { + throw new IllegalArgumentException( + "No view with key " + key + " exists."); + } else { + getChildren().setAll(views.get(key)); + } + } + + /** + * Checks whether a view exists for the specified key. + * + * @param key the key to check. + * @return {@code true} if a view with the key exists, {@code false} otherwise. + */ + public boolean hasView(ViewKeys key) { + return views.containsKey(key); + } + + /** + * Removes the view associated with the specified key. + * + * @param key the key of the view to remove. + */ + public void removeView(ViewKeys key) { + views.remove(key); + } +} diff --git a/pacman/view/src/main/java/com/gr15/pacman/view/GameView.java b/pacman/view/src/main/java/com/gr15/pacman/view/screen/GameView.java index d9235f6..33c4e2d 100644 --- a/pacman/view/src/main/java/com/gr15/pacman/view/GameView.java +++ b/pacman/view/src/main/java/com/gr15/pacman/view/screen/GameView.java @@ -1,18 +1,20 @@ -package com.gr15.pacman.view; +package com.gr15.pacman.view.screen; import java.util.HashMap; import java.util.List; import java.util.Map; +import com.gr15.pacman.view.ResourceManager; +import com.gr15.pacman.view.Sprite; + import com.gr15.pacman.model.GameState; import com.gr15.pacman.model.Board.TileType; -import javafx.scene.Scene; +import javafx.geometry.Pos; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.image.Image; -import javafx.scene.layout.StackPane; -import javafx.scene.paint.Color; +import javafx.scene.layout.VBox; import com.gr15.pacman.model.entities.Entity; import com.gr15.pacman.model.entities.Items; @@ -20,17 +22,16 @@ import com.gr15.pacman.model.entities.Items; /** * GameView */ -public class GameView - extends Scene { +public class GameView + extends VBox { /* Utils */ private Canvas canvas; private GraphicsContext gc; private GameState gameState; - private StackPane root; - private float scaleX = 4.0f; - private float scaleY = 4.0f; + private float scaleX = 3.0f; + private float scaleY = 3.0f; private int tileWidth; private int tileHeight; @@ -41,24 +42,20 @@ public class GameView /* UI elements */ private Sprite pacmanSprite; - public GameView(GameState gameState, - int tileWidth, int tileHeight) { - super(new StackPane()); - root = (StackPane)super.getRoot(); - + public GameView(GameState gameState, int tileWidth, int tileHeight) { canvas = new Canvas(tileWidth * 16 * scaleX, tileHeight * 16 * scaleY); gc = canvas.getGraphicsContext2D(); - root.getChildren().add(canvas); + getChildren().add(canvas); + setAlignment(Pos.CENTER); this.tileHeight = tileHeight; this.tileWidth = tileWidth; this.gameState = gameState; - this.setFill(Color.BLACK); - tileTextures.put(TileType.WALL, new Image( - this.getClass().getResourceAsStream("/gameAssets/wall.png"))); - pacmanSprite = new Sprite(new Image( - this.getClass().getResourceAsStream("/gameAssets/pacmanRight.png")), + tileTextures.put(TileType.WALL, ResourceManager.getInstance() + .getTexture("/gameAssets/wall.png")); + pacmanSprite = new Sprite( + ResourceManager.getInstance().getTexture("/gameAssets/pacmanRight.png"), gameState.getPacman().getPositionX() - 0.5, gameState.getPacman().getPositionY() - 0.5, 16 * scaleX, 16 * scaleY); @@ -106,8 +103,11 @@ public class GameView pos.y() * 16 * scaleY, texture.getWidth() * scaleX, texture.getHeight() * scaleY); - } }); } + + public void requestFocusForInput() { + requestFocus(); + } } diff --git a/pacman/view/src/main/java/com/gr15/pacman/view/screen/MainMenuView.java b/pacman/view/src/main/java/com/gr15/pacman/view/screen/MainMenuView.java new file mode 100644 index 0000000..c849fb9 --- /dev/null +++ b/pacman/view/src/main/java/com/gr15/pacman/view/screen/MainMenuView.java @@ -0,0 +1,27 @@ +package com.gr15.pacman.view.screen; + +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Menu; +import javafx.scene.layout.VBox; + +/** + * MainMenuView + */ +public class MainMenuView + extends VBox { + + private Button newGameButton = new Button("New Game"); + private Button resumeButton = new Button("Resume"); + private Button quitButton = new Button("Exit Game"); + + public MainMenuView() { + resumeButton.setDisable(true); + getChildren().addAll(resumeButton, newGameButton, quitButton); + setAlignment(Pos.CENTER); + } + + public Button getNewGameButton() { return this.newGameButton; } + public Button getResumeButton() { return this.resumeButton; } + public Button getQuitButton() { return this.quitButton; } +} diff --git a/pacman/view/src/main/java/com/gr15/pacman/view/screen/PauseView.java b/pacman/view/src/main/java/com/gr15/pacman/view/screen/PauseView.java new file mode 100644 index 0000000..7dd46e2 --- /dev/null +++ b/pacman/view/src/main/java/com/gr15/pacman/view/screen/PauseView.java @@ -0,0 +1,25 @@ +package com.gr15.pacman.view.screen; + +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.layout.VBox; + +/** + * PauseView + */ +public class PauseView + extends VBox { + + private Button resumeButton = new Button("Resume"); + private Button mainMenuButton = new Button("Main Menu"); + private Button quitButton = new Button("Quit"); + + public PauseView() { + getChildren().addAll(resumeButton, mainMenuButton, quitButton); + setAlignment(Pos.CENTER); + } + + public Button getResumeButton() { return this.resumeButton; } + public Button getMainMenuButton() { return this.mainMenuButton; } + public Button getQuitButton() { return this.quitButton; } +} diff --git a/pacman/view/src/main/java/module-info.java b/pacman/view/src/main/java/module-info.java index f50cafa..f91361e 100644 --- a/pacman/view/src/main/java/module-info.java +++ b/pacman/view/src/main/java/module-info.java @@ -5,5 +5,7 @@ module com.gr15.pacman.view { requires javafx.controls; requires javafx.graphics; requires com.gr15.pacman.model; + + exports com.gr15.pacman.view.screen; exports com.gr15.pacman.view; } diff --git a/pacman/view/src/main/resources/gameAssets/missingTexture.png b/pacman/view/src/main/resources/gameAssets/missingTexture.png Binary files differnew file mode 100644 index 0000000..7317870 --- /dev/null +++ b/pacman/view/src/main/resources/gameAssets/missingTexture.png |