/* 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.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import javax.imageio.ImageIO; import java.io.Serializable; import java.util.ArrayList; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.UnsupportedAudioFileException; import javax.swing.*; public class GamePanel extends JPanel implements Runnable, KeyListener, Serializable { //dimensions of window public static final int GAME_WIDTH = 1225; public static final int GAME_HEIGHT = 630; public transient JPanel gameFrame; public transient Thread gameThread; public transient Image image; public transient Graphics graphics; public Player player; public BackgroundImage background; public int playerFrame, enemyFrame; // keeps track of how many ticks has elapsed since last frame change public int playerFrameCounter = 0; public int enemyFrameCounter = 0; public boolean isPaused; public PauseMenu pauseMenu; public BufferedImageWrapper[][][] playerSpriteArray = new BufferedImageWrapper[2][2][11]; public BufferedImageWrapper[][][] slimeSpriteArray = new BufferedImageWrapper[2][2][3]; public BufferedImageWrapper[] explosionArray = new BufferedImageWrapper[9]; //public static ArrayListmap = new ArrayList(); public Tile[][]map = new Tile[300][18]; public ArrayList middlewareArray = new ArrayList(); public ArrayListparticleTiles = new ArrayList(); public ArrayListenemy = new ArrayList(); public ArrayListbombs = new ArrayList<>(); public BombDirectionShow bombDir = null; public ArrayListparticles = new ArrayList(); public Camera camera; // image imports begin here public BufferedImageWrapper backgroundImage = new BufferedImageWrapper(getImage("img/backgrounds/pointyMountains.png")); public BufferedImageWrapper box = new BufferedImageWrapper(getImage("img/tiles/boxes/box.png")); public BufferedImageWrapper boxCoin = new BufferedImageWrapper(getImage("img/tiles/boxes/boxCoin.png")); public BufferedImageWrapper bomb; public GamePanel(JPanel gameFrame) throws IOException, SpriteException, UnsupportedAudioFileException, LineUnavailableException { this.gameFrame = gameFrame; camera = new Camera(0); background = new BackgroundImage(0, 0, backgroundImage, GAME_WIDTH, GAME_HEIGHT, 10, camera); pauseMenu = new PauseMenu(GAME_HEIGHT/2, 100, 400, 400, GAME_WIDTH, new Font(Font.MONOSPACED, Font.BOLD, 60), "Paused"); try { // load player sprites from disk here for (int i = 0; i < 11; i++) { BufferedImage sprite = getImage(String.format("img/walk/p1_walk%s.png", String.format("%1$2s", i+1).replace(' ', '0'))); playerSpriteArray[1][0][i] = new BufferedImageWrapper(sprite); playerSpriteArray[1][1][i] = new BufferedImageWrapper(sprite); playerSpriteArray[0][0][i] = new BufferedImageWrapper(flipImageHorizontally(sprite)); playerSpriteArray[0][1][i] = new BufferedImageWrapper(flipImageHorizontally(sprite)); } for (int i = 0; i < 9; i++) { explosionArray[i] = new BufferedImageWrapper(getImage("img/misc/bomb/sonicExplosion0" + i + ".png")); } // load slime sprites from disk here // these variables were not defined above because they are temporary variables BufferedImageWrapper[] temporarySlimeArray = {new BufferedImageWrapper(getImage("img/enemy/slime/slimeWalk1.png")), new BufferedImageWrapper(getImage("img/enemy/slime/slimeWalk2.png")), new BufferedImageWrapper(getImage("img/enemy/slime/slimeDead.png"))}; BufferedImageWrapper[] flippedTemporarySlimeArray = {new BufferedImageWrapper(flipImageHorizontally(getImage("img/enemy/slime/slimeWalk1.png"))), new BufferedImageWrapper(flipImageHorizontally(getImage("img/enemy/slime/slimeWalk2.png"))), new BufferedImageWrapper(flipImageHorizontally(getImage("img/enemy/slime/slimeDead.png")))}; // please note that these sprites are reversed compared to the player sprites slimeSpriteArray[0][0] = temporarySlimeArray; slimeSpriteArray[0][1] = temporarySlimeArray; slimeSpriteArray[1][0] = flippedTemporarySlimeArray; slimeSpriteArray[1][1] = flippedTemporarySlimeArray; // load bomb sprites bomb = new BufferedImageWrapper(getImage("img/misc/bomb.png")); } catch (IOException e) { e.printStackTrace(); } player = new Player(GAME_WIDTH/2, GAME_HEIGHT/2, playerSpriteArray); //create a player controlled player, set start location to middle of screenk // the height of 35 is set because it is half of the original tile height (i.e., 70px) this.setFocusable(true); //make everything in this class appear on the screen this.addKeyListener(this); //start listening for keyboard input // request focus when the CardLayout selects this game this.addComponentListener(new ComponentAdapter() { @Override public void componentShown(ComponentEvent cEvt) { Component src = (Component) cEvt.getSource(); src.requestFocusInWindow(); } }); //add the MousePressed method from the MouseAdapter - by doing this we can listen for mouse input. We do this differently from the KeyListener because MouseAdapter has SEVEN mandatory methods - we only need one of them, and we don't want to make 6 empty methods addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { try { player.mousePressed(e); } catch (SpriteException | IOException ex) { throw new RuntimeException(ex); } } public void mouseReleased(MouseEvent e) { player.mouseReleased(e); } }); addMouseMotionListener(new MouseAdapter() { public void mouseDragged(MouseEvent e) { player.mouseDragged(e); } public void mouseMoved(MouseEvent e) { player.mouseMoved(e); } }); this.setPreferredSize(new Dimension(GAME_WIDTH, GAME_HEIGHT)); } // startThread is to be called after the game has started to avoid any issues TODO: better explanation public void startThread() { //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(); System.out.println(gameFrame); } //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(); try { draw(graphics, playerFrame, enemyFrame);//update the positions of everything on the screen } catch (IOException e) { throw new RuntimeException(e); } 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, int playerFrame, int enemyFrame) throws IOException { background.draw(g); if (isPaused) { // set player frame to 0 to prevent frame from changing when player moves playerFrame = 0; Graphics2D g2d = (Graphics2D)(g); // remove extraneous details from game when paused g2d.setPaint(Color.white); } //Don't want to draw off screen items int xMin = Math.max(0,((this.camera.x+GAME_WIDTH)/Tile.length)-(GAME_WIDTH/(2*Tile.length))-5); int xMax = Math.min(map.length, 7+xMin + GAME_WIDTH/Tile.length); for(int i=xMin; i= GAME_HEIGHT - Player.PLAYER_HEIGHT) { player.y = GAME_HEIGHT - Player.PLAYER_HEIGHT; player.yVelocity = 0; player.isGrounded = true; } if (player.x <= 0) { player.x = 0; } if (player.x + Player.PLAYER_WIDTH >= GAME_WIDTH) { player.x = GAME_WIDTH - Player.PLAYER_WIDTH; } for (NonPlayer n: enemy) { if (n.y >= GAME_HEIGHT - n.npcHeight) { n.y = GAME_HEIGHT - n.npcHeight; n.yVelocity = 0; n.isGrounded = true; } } } //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(){ LevelManager.setLevel(1); //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 = 0; long now; while(true){ //this is the infinite game loop now = System.nanoTime(); delta = delta + (now-lastTime)/ns; lastTime = now; //only move objects around and update screen if enough time has passed if(delta >= 1){ if (!isPaused && MenuPanel.gameStart) { // only perform game functions if game is not paused try { move(); } catch (IOException e) { throw new RuntimeException(e); } checkCollision(); updateEnemy(); try { updateParticle(); } catch (IOException e) { throw new RuntimeException(e); } if (playerFrameCounter > 5) { // increment sprite image to be used and keeps it below 12 playerFrame = (playerFrame + 1) % 11; playerFrameCounter -= 5; // if the player has moved enough to justify a frame change, a new save will also be made // try { // FileManager.writeObjectToFile("local/game_state", this); // } catch (IOException e) { // e.printStackTrace(); // } } } repaint(); delta--; } } } public void updateEnemy(){ for(int i=0; i