Stap 10: Het toevoegen van de vogel
In deze stap zullen we de speler van de vogel op het scherm en de gerelateerde mutatie logica toevoegen. Dit niet gepaard gaat met veel nieuwe code - de wijzigingen zal worden gezien door het toevoegen van een KeyListener zodat de gebruiker om de vogel springen en begin een nieuw spel, het maken van een paar toevoegingen in gameScreen toevoegen van logica om te werken van de game score en het finaliseren van de PlayGameScreen klasse.
Een paar nieuwe globale variabelen zijn ingevoerd in deze stap: BIRD_WIDTH, BIRD_HEIGHT, BIRD_X_LOCATION, BIRD_JUMP_DIFF, BIRD_FALL_DIFF en BIRD_JUMP_HEIGHT voor de constanten; gamePlay, birdThrust, birdFired, uitgebracht, en birdYTracker voor de globale variabelen.
Om te beginnen, gaan we de belangrijkste luisteraar toevoegen, zo importeert u de klassen KeyEvent en KeyListener, toevoegen van KeyListener aan één van de geïmplementeerde klassen die TopClass wordt gebruikt, en maken van de standaard keyPressed(...), keyReleased(...) en keyTyped(...) methoden. Alleen het maken van het skelet voor deze drie methoden; niet vul ze nog.
------
Binnen gameScreen maken we een paar toevoegingen van de vogel-functionaliteit toevoegen aan het spel. Eerst maakt u een exemplaar van vogels in de buurt van de bovenkant van de methode, maken twee variabelen voor het bijhouden van de vogel X (onveranderlijke) en Y-coördinaten. Het is de moeite waard om op te merken, y = 0 is de bovenkant van het scherm, zodat wanneer de vogel springt, u eigenlijk y-coördinaat van de vogel decrement.
Binnen de game lus ziet u verschillende voorwaardelijke instructie toevoegingen die relevant zijn voor de vogel; elk zal controleren als we eigenlijk op het spel en niet het splash-scherm met! isSplash. In de eerste voorwaardelijke testen we of de vogel heeft verteld om te bewegen (gebruiker op de spatiebalk, waardoor birdFired gewijzigd in true heeft geklikt). Zo ja, updaten we de globale vogel Y coördinaat variabele om de lokaal sleutel Y-coördinaat en birdFired te wijzigen naar false.
Vervolgens testen we als de vogel nog heeft stuwkracht, wat betekent dat we kijken of de vogel nog steeds uit de vorige druk op de SPATIEBALK. Als dat zo is, controleren we of de vogel aan springen te vertellen opnieuw uit de bovenkant van het scherm dwingen zal. Als dat niet het geval is, mogen een volledige sprong. Anders y-coördinaat van de vogel ingesteld op nul (bovenkant van het scherm), bijwerken van de globale variabele, en wijzigen van birdThrust naar false, zoals de vogel kan niet meer springen.
De volgende instructie else geeft aan dat van de vogel sprong bewerking heeft voltooid, dus bijwerken van de globale variabele en birdThrust te wijzigen naar false zodat de vogel niet verticaal meer kan verplaatsen.
De volgende anders als verklaring de vogel vertelt te vallen wanneer er geen geplaatste opdracht is.
Nadat de BottomPipe en TopPipe X en Y-coördinaten zijn ingesteld, hebben we een snelle als instructie te volgen voor het bijwerken van de vogel van het object X en Y coördinaten, evenals de vogel-instellingenobject in pgs.
In de laatste voorwaardelijke noemen we de updateScore-methode in TopClass om te testen of de score moet worden bijgewerkt. Houd er rekening mee, in de verklaring van de voorwaarde voor deze IF-statement, controleren we of de breedte van het object van de vogel niet nul is. Dit is om te voorkomen dat een oneven resource-laden probleem dat zich tijdens de botsingdetectie in de volgende stap voordoet. Weet alleen moet u die code hier.
------
Verhuizen naar de updateScore-methode, alles wat we doen hier test is of de vogel-object een van de BottomPipe objecten heeft doorgegeven. Wij doen dit door te controleren als het tussen de buitenrand en niet verder dan de buitenrand plus X_MOVEMENT_DIFFERENCE. Als er binnen dit bereik, roept u de methode van de incrementJump binnen de PlayGameScreen aan de game score bijwerken.
------
Wij wijzen drie toetsen in gebruik in dit programma: de spatiebalk springt de vogel, de 'b' toets zal een nieuwe ronde starten nadat we verliezen, en het programma volledig door de escape-toets afsluiten zal. In de methode keyPressed controleert de eerste instructie als dat als de SPATIEBALK wordt gedrukt, het spel wordt gespeeld (gamePlay == true), en de spatiebalk is vrijgegeven, voert u de volgende:
* Eerst als birdThrust geldt, die wordt gecontroleerd als de vogel nog steeds van eerder op de spatiebalk te drukken. Zo ja, wijzigen birdFired trouw aan register de nieuwe knop drukken.
* Volgende Verander de birdThrust variabele naar trouw start de vogel verhuizen als het niet reeds
* Tenslotte aangeven dat de spatiebalk is geregistreerd en alle daarmee samenhangende activiteiten voltooid zijn door het veranderen van de vrijgegeven op false.
Vervolgens controleren we als het spel niet langer wordt afgespeeld (dat wil zeggen een botsing is waargenomen), en als de 'b' knop werd ingedrukt, dan wij opnieuw de vogel de hoogte starten, Verwijder eventuele strekking kan hebben was toen de botsing is waargenomen, en simuleren van de druk op de knop de startGame (opnieuw starten van het spel is niet anders dan dringende startGame).
Ten slotte als de escape-knop is ingedrukt, willen wij volledig afsluiten van de wedstrijd, die wordt bereikt met behulp van System.exit(0).
Binnen keyReleased registreren we gewoon de SPATIEBALK wordt gedrukt door het veranderen van de vrijgegeven op true (binnen de IF statement).
import java.awt.Dimension; import java.awt.Font; import java.awt.Image; import java.awt.Color; import java.awt.LayoutManager; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import javax.swing.*; public class TopClass implements ActionListener, KeyListener { //global constant variables private static final int SCREEN_WIDTH = (int) Toolkit.getDefaultToolkit().getScreenSize().getWidth(); private static final int SCREEN_HEIGHT = (int) Toolkit.getDefaultToolkit().getScreenSize().getHeight(); private static final int PIPE_GAP = SCREEN_HEIGHT/5; //distance in pixels between pipes private static final int PIPE_WIDTH = SCREEN_WIDTH/8, PIPE_HEIGHT = 4*PIPE_WIDTH; private static final int BIRD_WIDTH = 120, BIRD_HEIGHT = 75; private static final int UPDATE_DIFFERENCE = 25; //time in ms between updates private static final int X_MOVEMENT_DIFFERENCE = 5; //distance the pipes move every update private static final int SCREEN_DELAY = 300; //needed because of long load times forcing pipes to pop up mid-screen private static final int BIRD_X_LOCATION = SCREEN_WIDTH/7; private static final int BIRD_JUMP_DIFF = 10, BIRD_FALL_DIFF = BIRD_JUMP_DIFF/2, BIRD_JUMP_HEIGHT = PIPE_GAP - BIRD_HEIGHT - BIRD_JUMP_DIFF*2; //global variables private boolean loopVar = true; //false -> don't run loop; true -> run loop for pipes private boolean gamePlay = false; //false -> game not being played private boolean birdThrust = false; //false -> key has not been pressed to move the bird vertically private boolean birdFired = false; //true -> button pressed before jump completes private boolean released = true; //space bar released; starts as true so first press registers private int birdYTracker = SCREEN_HEIGHT/2 - BIRD_HEIGHT; private Object buildComplete = new Object(); //global swing objects private JFrame f = new JFrame("Flappy Bird Redux"); private JButton startGame; private JPanel topPanel; //declared globally to accommodate the repaint operation and allow for removeAll(), etc. //other global objects private static TopClass tc = new TopClass(); private static PlayGameScreen pgs; //panel that has the moving background at the start of the game /** * Default constructor */ public TopClass() { } /** * Main executable method invoked when running .jar file * args */ public static void main(String[] args) { //build the GUI on a new thread javax.swing.SwingUtilities.invokeLater(new Runnable() { public void run() { tc.buildFrame(); //create a new thread to keep the GUI responsive while the game runs Thread t = new Thread() { public void run() { tc.gameScreen(true); } }; t.start(); } }); } /** * Method to construct the JFrame and add the program content */ private void buildFrame() { Image icon = Toolkit.getDefaultToolkit().getImage(this.getClass().getResource("resources/blue_bird.png")); f.setContentPane(createContentPane()); f.setResizable(true); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setAlwaysOnTop(false); f.setVisible(true); f.setMinimumSize(new Dimension(SCREEN_WIDTH*1/4, SCREEN_HEIGHT*1/4)); f.setExtendedState(JFrame.MAXIMIZED_BOTH); f.setIconImage(icon); f.addKeyListener(this); } private JPanel createContentPane() { topPanel = new JPanel(); //top-most JPanel in layout hierarchy topPanel.setBackground(Color.BLACK); //allow us to layer the panels LayoutManager overlay = new OverlayLayout(topPanel); topPanel.setLayout(overlay); //Start Game JButton startGame = new JButton("Start Playing!"); startGame.setBackground(Color.BLUE); startGame.setForeground(Color.WHITE); startGame.setFocusable(false); //rather than just setFocusabled(false) startGame.setFont(new Font("Calibri", Font.BOLD, 42)); startGame.setAlignmentX(0.5f); //center horizontally on-screen startGame.setAlignmentY(0.5f); //center vertically on-screen startGame.addActionListener(this); topPanel.add(startGame); //must add last to ensure button's visibility pgs = new PlayGameScreen(SCREEN_WIDTH, SCREEN_HEIGHT, true); //true --> we want pgs to be the splash screen topPanel.add(pgs); return topPanel; } /** * Implementation for action events */ public void actionPerformed(ActionEvent e) { if(e.getSource() == startGame) { //stop the splash screen loopVar = false; fadeOperation(); } else if(e.getSource() == buildComplete) { Thread t = new Thread() { public void run() { loopVar = true; gamePlay = true; tc.gameScreen(false); } }; t.start(); } } public void keyPressed(KeyEvent e) { if(e.getKeyCode() == KeyEvent.VK_SPACE && gamePlay == true && released == true){ //update a boolean that's tested in game loop to move the bird if(birdThrust) { //need this to register the button press and reset the birdYTracker before the jump operation completes birdFired = true; } birdThrust = true; released = false; } else if(e.getKeyCode() == KeyEvent.VK_B && gamePlay == false) { birdYTracker = SCREEN_HEIGHT/2 - BIRD_HEIGHT; //need to reset the bird's starting height birdThrust = false; //if user presses SPACE before collision and a collision occurs before reaching max height, you get residual jump, so this is preventative actionPerformed(new ActionEvent(startGame, -1, "")); } if(e.getKeyCode() == KeyEvent.VK_ESCAPE) { System.exit(0); } } public void keyReleased(KeyEvent e) { if(e.getKeyCode() == KeyEvent.VK_SPACE) { released = true; } } public void keyTyped(KeyEvent e) { } /** * Perform the fade operation that take place before the start of rounds */ private void fadeOperation() { Thread t = new Thread() { public void run() { topPanel.remove(startGame); topPanel.remove(pgs); topPanel.revalidate(); topPanel.repaint(); //panel to fade JPanel temp = new JPanel(); int alpha = 0; //alpha channel variable temp.setBackground(new Color(0, 0, 0, alpha)); //transparent, black JPanel topPanel.add(temp); topPanel.add(pgs); topPanel.revalidate(); topPanel.repaint(); long currentTime = System.currentTimeMillis(); while(temp.getBackground().getAlpha() != 255) { if((System.currentTimeMillis() - currentTime) > UPDATE_DIFFERENCE/2) { if(alpha < 255 - 10) { alpha += 10; } else { alpha = 255; } temp.setBackground(new Color(0, 0, 0, alpha)); topPanel.revalidate(); topPanel.repaint(); currentTime = System.currentTimeMillis(); } } topPanel.removeAll(); topPanel.add(temp); pgs = new PlayGameScreen(SCREEN_WIDTH, SCREEN_HEIGHT, false); pgs.sendText(""); //remove title text topPanel.add(pgs); while(temp.getBackground().getAlpha() != 0) { if((System.currentTimeMillis() - currentTime) > UPDATE_DIFFERENCE/2) { if(alpha > 10) { alpha -= 10; } else { alpha = 0; } temp.setBackground(new Color(0, 0, 0, alpha)); topPanel.revalidate(); topPanel.repaint(); currentTime = System.currentTimeMillis(); } } actionPerformed(new ActionEvent(buildComplete, -1, "Build Finished")); } }; t.start(); } /** * Method that performs the splash screen graphics movements */ private void gameScreen(boolean isSplash) { BottomPipe bp1 = new BottomPipe(PIPE_WIDTH, PIPE_HEIGHT); BottomPipe bp2 = new BottomPipe(PIPE_WIDTH, PIPE_HEIGHT); TopPipe tp1 = new TopPipe(PIPE_WIDTH, PIPE_HEIGHT); TopPipe tp2 = new TopPipe(PIPE_WIDTH, PIPE_HEIGHT); Bird bird = new Bird(BIRD_WIDTH, BIRD_HEIGHT); //variables to track x and y image locations for the bottom pipe int xLoc1 = SCREEN_WIDTH+SCREEN_DELAY, xLoc2 = (int) ((double) 3.0/2.0*SCREEN_WIDTH+PIPE_WIDTH/2.0)+SCREEN_DELAY; int yLoc1 = bottomPipeLoc(), yLoc2 = bottomPipeLoc(); int birdX = BIRD_X_LOCATION, birdY = birdYTracker; //variable to hold the loop start time long startTime = System.currentTimeMillis(); while(loopVar) { if((System.currentTimeMillis() - startTime) > UPDATE_DIFFERENCE) { //check if a set of pipes has left the screen //if so, reset the pipe's X location and assign a new Y location if(xLoc1 < (0-PIPE_WIDTH)) { xLoc1 = SCREEN_WIDTH; yLoc1 = bottomPipeLoc(); } else if(xLoc2 < (0-PIPE_WIDTH)) { xLoc2 = SCREEN_WIDTH; yLoc2 = bottomPipeLoc(); } //decrement the pipe locations by the predetermined amount xLoc1 -= X_MOVEMENT_DIFFERENCE; xLoc2 -= X_MOVEMENT_DIFFERENCE; if(birdFired && !isSplash) { birdYTracker = birdY; birdFired = false; } if(birdThrust && !isSplash) { //move bird vertically if(birdYTracker - birdY - BIRD_JUMP_DIFF < BIRD_JUMP_HEIGHT) { if(birdY - BIRD_JUMP_DIFF > 0) { birdY -= BIRD_JUMP_DIFF; //coordinates different } else { birdY = 0; birdYTracker = birdY; birdThrust = false; } } else { birdYTracker = birdY; birdThrust = false; } } else if(!isSplash) { birdY += BIRD_FALL_DIFF; birdYTracker = birdY; } //update the BottomPipe and TopPipe locations bp1.setX(xLoc1); bp1.setY(yLoc1); bp2.setX(xLoc2); bp2.setY(yLoc2); tp1.setX(xLoc1); tp1.setY(yLoc1-PIPE_GAP-PIPE_HEIGHT); //ensure tp1 placed in proper location tp2.setX(xLoc2); tp2.setY(yLoc2-PIPE_GAP-PIPE_HEIGHT); //ensure tp2 placed in proper location if(!isSplash) { bird.setX(birdX); bird.setY(birdY); pgs.setBird(bird); } //set the BottomPipe and TopPipe local variables in PlayGameScreen by parsing the local variables pgs.setBottomPipe(bp1, bp2); pgs.setTopPipe(tp1, tp2); if(!isSplash && bird.getWidth() != -1) { //need the second part because if bird not on-screen, cannot get image width and have cascading error in collision updateScore(bp1, bp2, bird); } //update pgs's JPanel topPanel.revalidate(); topPanel.repaint(); //update the time-tracking variable after all operations completed startTime = System.currentTimeMillis(); } } } /** * Calculates a random int for the bottom pipe's placement * int */ private int bottomPipeLoc() { int temp = 0; //iterate until temp is a value that allows both pipes to be onscreen while(temp <= PIPE_GAP+50 || temp >= SCREEN_HEIGHT-PIPE_GAP) { temp = (int) ((double) Math.random()*((double)SCREEN_HEIGHT)); } return temp; } /** * Method that checks whether the score needs to be updated * bp1 First BottomPipe object * bp2 Second BottomPipe object * bird Bird object */ private void updateScore(BottomPipe bp1, BottomPipe bp2, Bird bird) { if(bp1.getX() + PIPE_WIDTH < bird.getX() && bp1.getX() + PIPE_WIDTH > bird.getX() - X_MOVEMENT_DIFFERENCE) { pgs.incrementJump(); } else if(bp2.getX() + PIPE_WIDTH < bird.getX() && bp2.getX() + PIPE_WIDTH > bird.getX() - X_MOVEMENT_DIFFERENCE) { pgs.incrementJump(); } } }
We eindigen ten slotte in deze stap de code voor de klasse PlayGameScreen. Om wrap dit up, we een globale vogel-object maken, toevoegen van een globale variabele die de score bijhoudt, maak een globale variabele voor de breedte van de tekst van de score, voeg een paar regels code in de Paint-methode, en maak drie eenvoudige methoden.
In Paint voegen we de voorwaardelijke instructie om te controleren als we niet langer op het scherm van de plons en zorgen dat de vogel is niet nul. Indien voldaan, tekenen wij de vogel-object. In de poging-vangst blok, toewijzen op basis van de huidige score met FontMetrics scoreWidth. Tot slot, als we niet op het scherm van de plons, tekent het aantal succesvolle sprongen op het scherm.
Nu maken we drie eenvoudige methoden. Eerst setBird stelt de vogel-object binnen de PlayGameScreen, incrementJump verhoogt de sprong van de globale variabele, en getScore retourneert het aantal succesvolle sprongen (geen functionaliteit in dit spel).
import javax.swing.*; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Color; public class PlayGameScreen extends JPanel { //default reference ID private static final long serialVersionUID = 1L; //global variables private int screenWidth, screenHeight; private boolean isSplash = true; private int successfulJumps = 0; private String message = "Flappy Bird"; private Font primaryFont = new Font("Goudy Stout", Font.BOLD, 56), failFont = new Font("Calibri", Font.BOLD, 56); private int messageWidth = 0, scoreWidth = 0; private BottomPipe bp1, bp2; private TopPipe tp1, tp2; private Bird bird;</p><p> /** * Default constructor for the PlayGameScreen class */ public PlayGameScreen(int screenWidth, int screenHeight, boolean isSplash) { this.screenWidth = screenWidth; this.screenHeight = screenHeight; this.isSplash = isSplash; } /** * Manually control what's drawn on this JPanel by calling the paintComponent method * with a graphics object and painting using that object */ public void paintComponent(Graphics g) { super.paintComponent(g); g.setColor(new Color(89, 81, 247)); //color for the blue sky g.fillRect(0, 0, screenWidth, screenHeight*7/8); //create the sky rectangle g.setColor(new Color(147, 136, 9)); //brown color for ground g.fillRect(0, screenHeight*7/8, screenWidth, screenHeight/8); //create the ground rectangle g.setColor(Color.BLACK); //dividing line color g.drawLine(0, screenHeight*7/8, screenWidth, screenHeight*7/8); //draw the dividing line //objects must be instantiated before they're drawn! if(bp1 != null && bp2 != null && tp1 != null && tp2 != null) { g.drawImage(bp1.getPipe(), bp1.getX(), bp1.getY(), null); g.drawImage(bp2.getPipe(), bp2.getX(), bp2.getY(), null); g.drawImage(tp1.getPipe(), tp1.getX(), tp1.getY(), null); g.drawImage(tp2.getPipe(), tp2.getX(), tp2.getY(), null); } if(!isSplash && bird != null) { g.drawImage(bird.getBird(), bird.getX(), bird.getY(), null); } //needed in case the primary font does not exist try { g.setFont(primaryFont); FontMetrics metric = g.getFontMetrics(primaryFont); messageWidth = metric.stringWidth(message); scoreWidth = metric.stringWidth(String.format("%d", successfulJumps)); } catch(Exception e) { g.setFont(failFont); FontMetrics metric = g.getFontMetrics(failFont); messageWidth = metric.stringWidth(message); scoreWidth = metric.stringWidth(String.format("%d", successfulJumps)); } g.drawString(message, screenWidth/2-messageWidth/2, screenHeight/4); if(!isSplash) { g.drawString(String.format("%d", successfulJumps), screenWidth/2-scoreWidth/2, 50); } } /** * Parsing method for PlayGameScreen's global BottomPipe variables * bp1 The first BottomPipe * bp2 The second BottomPipe */ public void setBottomPipe(BottomPipe bp1, BottomPipe bp2) { this.bp1 = bp1; this.bp2 = bp2; } /** * Parsing method for PlayGameScreen's global TopPipe variables * tp1 The first TopPipe * tp2 The second TopPipe */ public void setTopPipe(TopPipe tp1, TopPipe tp2) { this.tp1 = tp1; this.tp2 = tp2; } /** * Parsing method for PlayGameScreen's global Bird variable * bird The Bird object */ public void setBird(Bird bird) { this.bird = bird; } /** * Method called to invoke an increase in the variable tracking the current * jump score */ public void incrementJump() { successfulJumps++; } /** * Method called to return the current jump score * */ public int getScore() { return successfulJumps; } /** * Method called to parse a message onto the screen * message The message to parse */ public void sendText(String message) { this.message = message; } }