Saturday, September 22, 2012

Sword Sword Revolution - Part 2

Continuing from yesterday, I have added a few new features to my Sword Sword Revolution game. For today, I decided to tackle user input. Specifically, my goal was to handle listening for the user to press an arrow key, and then change the animated GIF accordingly. After a short delay, I would return to the original waiting GIF.

The first thing I did was to add a KeyListener to my JFrame. Java's KeyListener requires three methods: keyPressed, keyReleased, and keyTyped. For my purposes, I really only care about keyPressed, but all three have to be defined. Here is my code for adding the KeyListener.

 public static void main(String[] args)
 {
  GameWindow g = new GameWindow();
  frame = new JFrame("Sword Sword Revolution");
  frame.add(g);
  frame.addKeyListener(new KeyListener(){
   public void keyPressed(KeyEvent e) { processKey(e.getKeyCode()); }

      public void keyReleased(KeyEvent e) {  }

      public void keyTyped(KeyEvent e) {  }
  });
  frame.setSize(600,600);
  frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  frame.setVisible(true);
 }


I chose to just add a new KeyListener() call directly in the addKeyListener call, but you can also create a KeyListener object separately by writing a custom class that implements KeyListener.

I just ignore keyReleased and keyTyped by not putting anything in their brackets. However, when the user first presses a key down, I want to register what key they pressed and then do some action. Therefore, in keyPressed, I make a call to a function processKey, and pass it an integer representation of the key that was pressed.

public static void processKey(int key)
{
 switch(key)
 {
 case 37:
  mainWindow.setIcon(left);
  break;
 case 38:
  mainWindow.setIcon(forward);
  break;
 case 39:
  mainWindow.setIcon(right);
  break;
 case 40:
  mainWindow.setIcon(backward);
  break;
 }
        frame.repaint();
}


My processKey function takes an integer, and then uses a switch statement to decide what to do. The arrow keys represent numbers 37-40, so I have specific cases for each of those. I do not care if the user presses any other button, so I do not need a default case. This is fairly simple, I just want to change the animated GIF being displayed. I also have to make sure to repaint the frame so the change appears on screen. There is just one problem now. When I press a button, the picture changes, but it stays changed. What I want is for the new animation to be run once, and then return to the waiting animation.

In order to handle this, Java has something called a ScheduledExecutorService. This is a class that allows you to schedule a future event, and it requires two objects: a ScheduledExecutorService object and a ScheduledFuture object. I create both of these objects at the top of my class, and then instantiate them after my switch statement.

scheduledExecutorService = Executors.newScheduledThreadPool(1);

scheduledFuture = scheduledExecutorService.schedule(new Runnable() {
 public void run() {
  mainWindow.setIcon(ready);
 }
},
1,
TimeUnit.SECONDS);

scheduledExecutorService.shutdown();


Basically, what this code does is creates a thread, waits 1 second, and then runs the run function of my Runnable object. Once that completes I am free to shutdown the thread.

There is still an issue, however. If I press more than one arrow key during that 1 second, the animation is off. The second animation interrupts the first one, but the scheduler is still running in the background. So 1 second after the first button gets pressed, the last button pressed gets interrupted. I want to cancel the scheduled run if it exists and a new one is being created, so at the top of my processKey function I add:
try { 
   scheduledFuture.cancel(true); 
  } catch(Exception ex){}


The reason for the try...catch statement is because when you first press a button, there is nothing scheduled. This results in cancel() throwing a NullPointerException. We want to safely catch that Exception and just move on if there is nothing to cancel. Voila! I can now change images at the press of a button!

Photobucket

Another thing to note, in order to call functions and reference variables from within a static method, those methods and variables also have to be static. Here is the full code of the game so far:

import java.awt.BorderLayout;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class GameWindow extends JPanel{
 
 private static JLabel mainWindow;
 private static ImageIcon forward;
 private static ImageIcon backward;
 private static ImageIcon left;
 private static ImageIcon right;
 private static ImageIcon ready;
 private static ImageIcon miss;
 protected static JFrame frame;
 protected static ScheduledFuture scheduledFuture;
 protected static ScheduledExecutorService scheduledExecutorService;
 
 public GameWindow(){
  forward = new ImageIcon("src/images/forward.gif");
  backward = new ImageIcon("src/images/backward.gif");
  left = new ImageIcon("src/images/left.gif");
  right = new ImageIcon("src/images/right.gif");
  ready = new ImageIcon("src/images/ready.gif");
  miss = new ImageIcon("src/images/miss.gif");
  mainWindow = new JLabel(ready);
  this.setSize(600,600);
  this.setLayout(new BorderLayout());
  this.add(mainWindow, BorderLayout.CENTER);
 }
 
 public static void main(String[] args)
 {
  GameWindow g = new GameWindow();
  frame = new JFrame("Sword Sword Revolution");
  frame.add(g);
  frame.addKeyListener(new KeyListener(){
   public void keyPressed(KeyEvent e) { processKey(e.getKeyCode()); }

      public void keyReleased(KeyEvent e) {  }

      public void keyTyped(KeyEvent e) {  }
  });
  frame.setSize(600,600);
  frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  frame.setVisible(true);
 }
 
 public static void processKey(int key)
 {
  try { 
   scheduledFuture.cancel(true); 
  } catch(Exception ex){}
  switch(key)
  {
  case 37:
   mainWindow.setIcon(left);
   break;
  case 38:
   mainWindow.setIcon(forward);
   break;
  case 39:
   mainWindow.setIcon(right);
   break;
  case 40:
   mainWindow.setIcon(backward);
   break;
  }
  frame.repaint();
  scheduledExecutorService =
          Executors.newScheduledThreadPool(1);

  scheduledFuture =
      scheduledExecutorService.schedule(new Runnable() {
          public void run() {
              mainWindow.setIcon(ready);
          }
      },
      1,
      TimeUnit.SECONDS);

  scheduledExecutorService.shutdown();
 }

}

No comments:

Post a Comment