WormPanel es como el GamePanel que hicimos anteriormente, primero vamos a declarar las variables globales
public class WormPanel extends JPanel implements Runnable { private static final int PWIDTH = 500; // tamaño del panel private static final int PHEIGHT = 400; private static long MAX_STATS_INTERVAL = 1000L; // guardar estadisticas cada 1 segundo private static final int NO_DELAYS_PER_YIELD = 16; /* Numero de frames con un retraso de 0ms antes d que el hilo de animacion * alacnce a otros hilos en ejecucion */ private static int MAX_FRAME_SKIPS = 5; // era 2; // Numero de frames que pueden ser salteados en un bucle de animacion // El estado del juego se actualiza pero no se renderiza private static int NUM_FPS = 10; // numero de FPS guardados en promedio // variables para las estadisticas private long statsInterval = 0L; // en ms private long prevStatsTime; private long totalElapsedTime = 0L; private long gameStartTime; private int timeSpentInGame = 0; // en segundos private long frameCount = 0; private double fpsStore[]; private long statsCount = 0; private double averageFPS = 0.0; private long framesSkipped = 0L; private long totalFramesSkipped = 0L; private double upsStore[]; private double averageUPS = 0.0; private DecimalFormat df = new DecimalFormat("0.##"); // 2 decimales private DecimalFormat timedf = new DecimalFormat("0.####"); // 4 decimales private Thread animator; // hilo q se encarga de la animacion private volatile boolean running = false; // usado para parar el hilo de la animacion private volatile boolean isPaused = false; private int period; // periodo entre dibujos, en ms private WormChase wcTop; private Worm fred; // El gusano private Obstacles obs; // Los obstaculos // usado en el fin del juego private volatile boolean gameOver = false; private int score = 0; private Font font; private FontMetrics metrics; // renderizado fuera de la pantalla private Graphics dbg; private Image dbImage = null;
Si leyeron lo de Space Invaders se darán cuenta que no es muy diferente, muchas de las variables que aparecen acá son para elaborar las estadísticas
Ahora el constructor:
public WormPanel(WormChase wc, int period)//recibe el objeto WormChase y un periodo { wcTop = wc; this.period = period; setBackground(Color.white); setPreferredSize( new Dimension(PWIDTH, PHEIGHT)); setFocusable(true); requestFocus(); // Jpanel es enfocado, por lo tanto esta atento al teclado readyForTermination(); // crea los componentes de juego obs = new Obstacles(wcTop); fred = new Worm(PWIDTH, PHEIGHT, obs); addMouseListener( new MouseAdapter() { public void mousePressed(MouseEvent e) { testPress(e.getX(), e.getY()); } }); // setea la fuente para el mensaje font = new Font("SansSerif", Font.BOLD, 24); metrics = this.getFontMetrics(font); // inicializa los elementos para controlar el tiempo fpsStore = new double[NUM_FPS]; upsStore = new double[NUM_FPS]; for (int i=0; i < NUM_FPS; i++) { fpsStore[i] = 0.0; upsStore[i] = 0.0; } } // Fin de WormPanel()
Hasta acá solo hemos creado los elementos del juego, el mensaje y su fuente se muestra cuando el juego termina, fpsStore[] y upsStore[] contiene los últimos 10 FPS y UPS calculados en las estadísticas
3.1. User Input
Bueno ahora viene la parte del usuario, la función que se invoca cada vez que haces un clic con el mouse es testpress.
private void testPress(int x, int y) // (x,y) estan cerca de la cabeza o deberiamos agregar un obstaculo? { if (!isPaused && !gameOver) { if (fred.nearHead(x,y)) { // Hicieron click cerca de la cabeza? gameOver = true; score = (40 - timeSpentInGame) + (40 - obs.getNumObstacles()); // Aumenta el score } else { // adhiere un obstaculo, si se peude if (!fred.touchedAt(x,y)) // no se toco el cuerpo del gusano? obs.add(x,y); } } }// fin de testPress()
Las variables isPaused y gameOver son manipuladas por los window listener de la aplicación y estas a su vez utilizan estas funciones:
// ------------- metodos para el ciclo de vida del juego ------------ // llamados por los window listener methods public void resumeGame() // llamado cuando el JFrame esta activado { isPaused = false; } public void pauseGame() // llamado cuando el JFrame esta desactivado { isPaused = true; } public void stopGame() // llamado cuando el JFrame esta cerrando { running = false; } // ----------------------------------------------
3.2. The Animation Loop
Quizas ahora viene la función principal de un juego, el ciclo de animación
public void run () { /*Los frames de la animacion son dibujados dentro de while(running){}*/ long beforeTime, afterTime, timeDiff, sleepTime; int overSleepTime = 0; int noDelays = 0; int excess = 0; Graphics g; gameStartTime = System.currentTimeMillis(); prevStatsTime = gameStartTime; beforeTime = gameStartTime; running = true; while(running) { gameUpdate(); gameRender(); // renderiza el juego en un buffer paintScreen(); // Dibuja el buffer en pantalla afterTime = System.currentTimeMillis(); timeDiff = afterTime - beforeTime; sleepTime = (period - timeDiff) - overSleepTime; if (sleepTime > 0) { // sobro un poco de tiempo try { Thread.sleep(sleepTime); // En ms } catch(InterruptedException ex){} overSleepTime = (int)((System.currentTimeMillis() - afterTime) - sleepTime); } else { // sleepTime <= 0; El frame tomo mas de lo definicio en period excess -= sleepTime; // graba el exceso overSleepTime = 0; if (++noDelays >= NO_DELAYS_PER_YIELD) { Thread.yield(); // le da chance a otros hilos a funcionar noDelays = 0; } } beforeTime = System.currentTimeMillis(); /* Si los frames de animacion estan tomando demasiado tiempo * actualiza el juego sin renderizar hasta conseguir * actualizaciones/seg cerca los FPS requeridos */ int skips = 0; while((excess > period) && (skips < MAX_FRAME_SKIPS)) { excess -= period; gameUpdate(); // actualiza sin renderizar skips++; } framesSkipped += skips; storeStats(); } printStats(); System.exit(0); //cerramos }//Fin de run()
gameStartTime and prevStatsTime son utilizados en para el calculo de las estadísticas, la verdad es que la función de las estadísticas como bien se habrán dado cuenta es opcional, por ahora lo voy a dejar, pero si un juego no da problemas en el SO que lo necesitan no deberían utilizarlo.
3.3. Statistics Gathering
Bueno no voy a explicar como se calculan las estadísticas, dejare el código pero si quieren entenderlo chequen en el libro
private void storeStats() { frameCount++; statsInterval += period; if (statsInterval >= MAX_STATS_INTERVAL) { long timeNow = System.currentTimeMillis(); timeSpentInGame = (int) ((timeNow - gameStartTime)/1000L); // ms --> secs wcTop.setTimeSpent( timeSpentInGame ); long realElapsedTime = timeNow - prevStatsTime; totalElapsedTime += realElapsedTime; double timingError = ((double)(realElapsedTime - statsInterval) / statsInterval) * 100.0; totalFramesSkipped += framesSkipped; double actualFPS = 0; double actualUPS = 0; if (totalElapsedTime > 0) { actualFPS = (((double)frameCount / totalElapsedTime) * 1000L); actualUPS = (((double)(frameCount + totalFramesSkipped) / totalElapsedTime) * 1000L); } fpsStore[ (int)statsCount%NUM_FPS ] = actualFPS; upsStore[ (int)statsCount%NUM_FPS ] = actualUPS; statsCount = statsCount+1; double totalFPS = 0.0; double totalUPS = 0.0; for (int i=0; i < NUM_FPS; i++) { totalFPS += fpsStore[i]; totalUPS += upsStore[i]; } if (statsCount < NUM_FPS) { averageFPS = totalFPS/statsCount; averageUPS = totalUPS/statsCount; } else { averageFPS = totalFPS/NUM_FPS; averageUPS = totalUPS/NUM_FPS; } framesSkipped = 0; prevStatsTime = timeNow; statsInterval = 0L; } } private void printStats() { System.out.println("Frame Count/Loss: " + frameCount + " / " + totalFramesSkipped); System.out.println("Average FPS: " + df.format(averageFPS)); System.out.println("Average UPS: " + df.format(averageUPS)); System.out.println("Time Spent: " + timeSpentInGame + " secs"); System.out.println("Boxes used: " + obs.getNumObstacles()); }
3.4. Game-Specific Behaviour
El comportamiento del juego se origina en 2 funciones:
while(running) { gameUpdate(); gameRender(); // renderiza el juego en un buffer
Recuerden que inicializamos un objeto gusano como Fred, y después lo actualizamos en la función
private Worm fred; - - - private void gameUpdate() { if (!isPaused && !gameOver) fred.move(); } // fin de gameUpdate()
gameRender() dibuja el gusano y los obstáculos en un buffer
private void gameRender() { if (dbImage == null){ dbImage = createImage(PWIDTH, PHEIGHT); if (dbImage == null) { System.out.println("dbImage is null"); return; } else dbg = dbImage.getGraphics(); } // limpia el background dbg.setColor(Color.white); dbg.fillRect (0, 0, PWIDTH, PHEIGHT); dbg.setColor(Color.blue); dbg.setFont(font); //En la esquina izquierda se hara un conteo y promedio de FPS y UPS dbg.drawString("Average FPS/UPS: " + df.format(averageFPS) + ", " + df.format(averageUPS), 20, 25); // was (10,55) dbg.setColor(Color.black); // dibuja los elementos del juego obs.draw(dbg); fred.draw(dbg); if (gameOver) gameOverMessage(dbg); } // fin de gameRender()
bueno creo que esta bastante entendible, las funciones que dibujan son : obs.draw(dbg) y fred.draw(dbg), con esto aseguramos que el dibujado se haga en el componente del juego liberando de esa tarea a esta clase
gameOverMessage() se ubicara en el centro de la pantalla
private void gameOverMessage(Graphics g) // Centra el mensaje de game-over en el panel { String msg = "Game Over. Your Score: " + score; int x = (PWIDTH - metrics.stringWidth(msg))/2; int y = (PHEIGHT - metrics.getHeight())/2; g.setColor(Color.red); g.setFont(font); g.drawString(msg, x, y); } // fin de gameOverMessage()
quedaría ahora la función paintScreen(), que es igual al del capitulo anterior
private void paintScreen() // constantemente renderiza la imagen del buffer en la pantalla { Graphics g; try { g = this.getGraphics(); // trae el contexto grafico del panel if ((g != null) && (dbImage != null)) g.drawImage(dbImage, 0, 0, null); g.dispose(); } catch (Exception e) { System.out.println("Error del contexto grafico: " + e); } } // fin de paintScreen()
El resto de funciones son iguales al de game panel del capitulo anterior:
private void readyForTermination() { addKeyListener( new KeyAdapter() { // Escucha a esc, q, fin, ctrl + c public void keyPressed(KeyEvent e) { int keyCode = e.getKeyCode(); if ((keyCode == KeyEvent.VK_ESCAPE) || (keyCode == KeyEvent.VK_Q) || (keyCode == KeyEvent.VK_END) || ((keyCode == KeyEvent.VK_C) && e.isControlDown()) ) { running = false; }}}); } // fin de readyForTermination() public void addNotify () { super.addNotify(); // Crea el lugar startGame(); // Inicia el hilo } private void startGame() // Inicializa y comienza el hilo { if (animator == null || !running) { animator = new Thread(this); animator.start(); } } // fin de startGame()