Mobiltelefonapplikationer med Java ME: Lektion 11

Idag: Spel, rörlig grafik och klassen GameCanvas

Klicka på startknappen i den lilla mediespelaren ovan för att lyssna på lektionen. (Man kan behöva vänta en stund på att ljudfilen laddas ner.) Om mediespelaren inte syns, eller om det inte fungerar av något annat skäl, kan man klicka här för att ladda ner mp3-filen (ca 29 minuter, ca 13 megabyte). Beroende på hur webbläsaren är konfigurerad kan det kräva ett separat mp3-spelarprogram av något slag.

Bild 1: En studsboll

I lektionen om hur man ritar saker såg vi hur man kan rita upp saker på en Canvas. Man anropar metoder som drawLine, drawRect och fillRect. Vi såg också hur man kunde få grafiken att ändras på kommando från användaren.

Rörlig grafik innebär att grafiken ändras av sig själv, utan kommandon från användaren. Programmet ska självt göra ändringar, helst många gånger per sekund. Vi ska göra en studsboll som åker runt på skärmen och studsar på kanterna:

En boll som studsar runt på skärmen

"Bild" 2: Data om studsbollen

  • Utseende: radie, färg
  • Var är bollen nu: x och y
  • Bollens riktning och hastighet: delta-x och delta-y

"Bild" 3: Klassen Studsboll1

Klassen Studsboll1. Utdrag ur filen Studsboll1.java:

    1   public class Studsboll1 extends MIDlet {
    2       private Display displayen;
    3       private Studscanvas canvasen;
    4
    5       public void startApp() {
    6           displayen = Display.getDisplay(this);
    7           if (canvasen == null) {
    8               canvasen = new Studscanvas();
    9               displayen = Display.getDisplay(this);
   10               displayen.setCurrent(canvasen);
   11           }
   12       }
   13
   14       public void pauseApp() { }
   15
   16       public void destroyApp(boolean unconditional) { }
   17   } // class Studsboll1

"Bild" 4: Klassen Studscanvas - ett misslyckat försök

Klassen Studscanvas, ur programmet Studsboll1. Filen Studscanvas.java:

    1	// Studscanvas.java
    2	
    3	import javax.microedition.lcdui.*;
    4	
    5	public class Studscanvas extends Canvas {
    6	    int ball_radius;
    7	    double ball_x, ball_y, ball_dx, ball_dy;
    8	
    9	    public Studscanvas() {
   10	        int screen_width = getWidth();
   11	        int screen_height = getHeight();
   12	
   13	        ball_radius = 5;
   14	        ball_x = screen_width / 2;
   15	        ball_y = screen_height / 2;
   16	        ball_dx = 1;
   17	        ball_dy = -1;
   18	
   19	        // This doesn't work
   20	        while (true) {
   21	            update_game_state();
   22	            repaint();
   23	        }
   24	    }
   25	
   26	    private void update_game_state() {
   27	        int screen_width = getWidth();
   28	        int screen_height = getHeight();
   29	        ball_x += ball_dx; // This means: ball_x = ball_x + ball_dx;
   30	        ball_y += ball_dy;
   31	        if (ball_x + ball_radius >= screen_width) {
   32	            ball_dx *= -1; // Same as: ball_dx = -ball_dx;
   33	        }
   34	        else if (ball_x - ball_radius <= 0) {
   35	            ball_dx *= -1;
   36	        }
   37	        if (ball_y - ball_radius <= 0) {
   38	            ball_dy *= -1;
   39	        }
   40	        else if (ball_y + ball_radius >= screen_height) {
   41	            ball_dy *= -1;
   42	        }
   43	
   44	        System.out.println("ball_x = " + ball_x + ", ball_y = " + ball_y +
   45	                           ", ball_dx = " + ball_dx + " + ball_dy = " + ball_dy);
   46	    }
   47	
   48	    public void paint(Graphics g) {
   49	        g.setColor(0xffffff);
   50	        g.fillRect(0, 0, getWidth(), getHeight());
   51	        g.setColor(0x000000);
   52	        g.fillArc((int)(ball_x - ball_radius), (int)(ball_y - ball_radius),
   53	                  ball_radius * 2, ball_radius * 2, 0, 360);
   54	    }
   55	
   56	} // class Studscanvas

"Bild" 5: En bättre Studscanvas

Använd en egen tråd eller en timer i stället för en loop. En timer är enklast.

Klassen Studscanvas, ur programmet Studsboll2. Filen Studscanvas.java:

    1   // Studscanvas.java
    2   
    3   import java.util.*;
    4   import javax.microedition.lcdui.*;
    5   
    6   public class Studscanvas extends Canvas {
    7       int ball_radius;
    8       double ball_x, ball_y, ball_dx, ball_dy;
    9       Timer the_timer;
   10   
   11       public Studscanvas() {
   12           int screen_width = getWidth();
   13           int screen_height = getHeight();
   14   
   15           ball_radius = 5;
   16           ball_x = screen_width / 2;
   17           ball_y = screen_height / 2;
   18           ball_dx = 1;
   19           ball_dy = -1;
   20       }
   21   
   22       public void showNotify() {
   23           the_timer = new Timer();
   24           TimerTask task = new TimerTask() {
   25                   public void run() {
   26                       update_game_state();
   27                       repaint();
   28                   }
   29               };
   30           the_timer.schedule(task, 0, 20);
   31       }
   32   
   33       public void hideNotify() {
   34           if (the_timer != null) {
   35               the_timer.cancel();
   36               the_timer = null;
   37           }
   38       }
   39   
   40       private void update_game_state() {
   41           int screen_width = getWidth();
   42           int screen_height = getHeight();
   43           ball_x += ball_dx;
   44           ball_y += ball_dy;
   45           if (ball_x + ball_radius >= screen_width) {
   46               ball_dx *= -1;
   47           }
   48           else if (ball_x - ball_radius <= 0) {
   49               ball_dx *= -1;
   50           }
   51           if (ball_y - ball_radius <= 0) {
   52               ball_dy *= -1;
   53           }
   54           else if (ball_y + ball_radius >= screen_height) {
   55               ball_dy *= -1;
   56           }
   57   
   58           System.out.println("ball_x = " + ball_x + ", ball_y = " + ball_y +
   59                              ", ball_dx = " + ball_dx + " + ball_dy = " + ball_dy);
   60       }
   61   
   62       public void paint(Graphics g) {
   63           g.setColor(0xffffff);
   64           g.fillRect(0, 0, getWidth(), getHeight());
   65           g.setColor(0x000000);
   66           g.fillArc((int)(ball_x - ball_radius), (int)(ball_y - ball_radius),
   67                     ball_radius * 2, ball_radius * 2, 0, 360);
   68       }
   69   
   70   } // class Studscanvas

"Bild" 6: En ännu bättre Studscanvas

Klassen Studscanvas, ur programmet Studsboll3. Filen Studscanvas.java:

    1   // Studscanvas.java
    2   
    3   import java.util.*;
    4   import javax.microedition.lcdui.*;
    5   import javax.microedition.lcdui.game.*;
    6   
    7   public class Studscanvas extends GameCanvas {
    8       int ball_radius;
    9       double ball_x, ball_y, ball_dx, ball_dy;
   10       Timer the_timer;
   11       Graphics g;
   12   
   13       public Studscanvas() {
   14           super(false);
   15           int screen_width = getWidth();
   16           int screen_height = getHeight();
   17   
   18           ball_radius = 5;
   19           ball_x = screen_width / 2;
   20           ball_y = screen_height / 2;
   21           ball_dx = 1;
   22           ball_dy = -1;
   23       }
   24   
   25       public void showNotify() {
   26           the_timer = new Timer();
   27           g = getGraphics();
   28           TimerTask task = new TimerTask() {
   29                   public void run() {
   30                       update_game_state();
   31                       render();
   32                       flushGraphics();
   33                   }
   34               };
   35           the_timer.schedule(task, 0, 20);
   36       }
   37   
   38       public void hideNotify() {
   39           if (the_timer != null) {
   40               the_timer.cancel();
   41               the_timer = null;
   42           }
   43       }
   44   
   45       private void update_game_state() {
   46           int screen_width = getWidth();
   47           int screen_height = getHeight();
   48           ball_x += ball_dx;
   49           ball_y += ball_dy;
   50           if (ball_x + ball_radius >= screen_width) {
   51               ball_dx *= -1;
   52           }
   53           else if (ball_x - ball_radius <= 0) {
   54               ball_dx *= -1;
   55           }
   56           if (ball_y - ball_radius <= 0) {
   57               ball_dy *= -1;
   58           }
   59           else if (ball_y + ball_radius >= screen_height) {
   60               ball_dy *= -1;
   61           }
   62   
   63           System.out.println("ball_x = " + ball_x + ", ball_y = " + ball_y +
   64                              ", ball_dx = " + ball_dx + " + ball_dy = " + ball_dy);
   65       }
   66   
   67       public void render() {
   68           g.setColor(0xffffff);
   69           g.fillRect(0, 0, getWidth(), getHeight());
   70           g.setColor(0x000000);
   71           g.fillArc((int)(ball_x - ball_radius), (int)(ball_y - ball_radius),
   72                     ball_radius * 2, ball_radius * 2, 0, 360);
   73       }
   74   
   75   } // class Studscanvas

"Bild" 7: Knapptryckningar

Man kan använda metoderna keyPressed, keyReleased och keyRepeated,
eller hantera knapptryckningarna helt själv med getKeyStates.

Ur klassen Studscanvas, från programmet Studsboll4. Filen Studscanvas.java:

    1       private void process_keys() {
    2           int keys = getKeyStates();
    3           // System.out.println("keys = " + keys);
    4           if ((keys & LEFT_PRESSED) != 0) {
    5               System.out.println("Pressed left!");
    6               ball_dx -= 1;
    7           }
    8           if ((keys & RIGHT_PRESSED) != 0) {
    9               System.out.println("Pressed right!");
   10               ball_dx += 1;
   11           }
   12           if ((keys & UP_PRESSED) != 0) {
   13               System.out.println("Pressed up!");
   14               ball_dy -= 1;
   15           }
   16           if ((keys & DOWN_PRESSED) != 0) {
   17               System.out.println("Pressed down!");
   18               ball_dy += 1;
   19           }
   20           if ((keys & FIRE_PRESSED) != 0)  {
   21               System.out.println("Pressed FIRE!");
   22               ball_dx = 0;
   23               ball_dy = 0;
   24           }
   25       }

"Bild" 8: Lägg till process_keys i "spel-loopen"

Ur klassen Studscanvas, från programmet Studsboll4. Filen Studscanvas.java:

    1       public void showNotify() {
    2           the_timer = new Timer();
    3           g = getGraphics();
    4           TimerTask task = new TimerTask() {
    5                   public void run() {
    6                       process_keys();
    7                       update_game_state();
    8                       render();
    9                       flushGraphics();
   10                   }
   11               };
   12           the_timer.schedule(task, 0, 20);
   13       }

Bild 9: Ett problem med knapptryckningar

"Kontakstudsar" på telefonen, och ännu värre i emulatorn.

En boll som studsar runt på skärmen

"Bild" 10: Tangent-repeat

Man kan hålla reda på tangentrepeaten själv:

  • Håller användaren knappen nere nu?
  • Om hon håller knappen nere: var det mer än (säg) en kvarts sekund (dvs 250 millisekunder) sen knappen "räknades" senast?
  • Om det var mer än en kvarts sekund: räkna knapptryckningen, och börja om att räkna tiden.
  • Om användaren inte håller knappen nere, eller om det var mindre än en kvarts sekund sen den "räknades" senast: gör ingenting.

Se även: http://devlinslab.blogspot.com/2007/10/input-handling-keypress-with-repeat.html

"Bild" 11: Tangent-repeat som kod

Klassen Studscanvas, från programmet Studsboll5. Ur filen Studscanvas.java:

    1	// Studscanvas.java
    2	
    3	import java.util.*;
    4	import javax.microedition.lcdui.*;
    5	import javax.microedition.lcdui.game.*;
    6	
    7	public class Studscanvas extends GameCanvas {
    8	    int ball_radius;
    9	    double ball_x, ball_y, ball_dx, ball_dy;
   10	    Timer the_timer;
   11	    Graphics g;
   12	
   13	    // Key repeat rate in milliseconds
   14	    public static final int KEY_REPEAT_RATE = 250;
   15	
   16	    // Key constants
   17	    public static final int MY_UP_KEY = 0;
   18	    public static final int MY_LEFT_KEY = 1;
   19	    public static final int MY_DOWN_KEY = 2;
   20	    public static final int MY_RIGHT_KEY = 3;
   21	    public static final int MY_FIRE_KEY = 4;
   22	
   23	    // Key states for up, left, down, right, and fire key
   24	    private boolean[] key_is_down = {
   25	        false, false, false, false, false
   26	    };
   27	
   28	    // The last time each key changed state
   29	    private long[] last_key_time = {
   30	        0, 0, 0, 0, 0
   31	    };
   32	
   33	    // Lookup table for key constants
   34	    private int[] key_value = {
   35	        GameCanvas.UP_PRESSED,
   36	        GameCanvas.LEFT_PRESSED,
   37	        GameCanvas.DOWN_PRESSED,
   38	        GameCanvas.RIGHT_PRESSED,
   39	        GameCanvas.FIRE_PRESSED
   40	    };
...
   75	    private void process_keys() {
   76	        int keys = getKeyStates();
   77	        long current_time = System.currentTimeMillis();
   78	
   79	        // Loop through the keys
   80	        for (int i = 0; i < 5; i++){
   81	
   82	            // By default, the key is not pressed
   83	            key_is_down[i] = false;
   84	
   85	            // Is the user pressing this key?
   86	            if ((keys & key_value[i]) != 0) {
   87	                long elapsed_time = current_time - last_key_time[i];
   88	
   89	                // Is it time to toggle the key state?
   90	                if (elapsed_time >= KEY_REPEAT_RATE) {
   91	
   92	                    // Save the current time
   93	                    last_key_time[i] = current_time; 
   94	
   95	                    // Toggle the state to "down"
   96	                    key_is_down[i] = true;
   97	                }
   98	            }
   99	        }
  100	
  101	        if (key_is_down[MY_LEFT_KEY]) {
  102	            System.out.println("Pressed left!");
  103	            ball_dx -= 1;
  104	        }
  105	        if (key_is_down[MY_RIGHT_KEY]) {
  106	            System.out.println("Pressed right!");
  107	            ball_dx += 1;
  108	        }
  109	        if (key_is_down[MY_UP_KEY]) {
  110	            System.out.println("Pressed up!");
  111	            ball_dy -= 1;
  112	        }
  113	        if (key_is_down[MY_DOWN_KEY]) {
  114	            System.out.println("Pressed down!");
  115	            ball_dy += 1;
  116	        }
  117	        if (key_is_down[MY_FIRE_KEY]) {
  118	            System.out.println("Pressed FIRE!");
  119	            ball_dx = 0;
  120	            ball_dy = 0;
  121	        }
  122	
  123	    }
...

"Bild" 12: Ett mycket enklare sätt

Som vi sagt ovan: Man kan använda metoderna keyPressed, keyReleased och keyRepeated,
eller hantera knapptryckningarna helt själv med getKeyStates.

Nu provar vi med keyPressed med flera.

Klassen Studscanvas, från programmet Studsboll6. Ur filen Studscanvas.java:

    1       protected void keyPressed(int keyCode) {
    2           System.out.println("keyPressed, keyCode = " + keyCode);
    3           int game_action = getGameAction(keyCode);
    4           System.out.println("keyPressed, game_action = " + game_action);
    5
    6           switch (game_action) {
    7           case UP: ball_dy -= 1; break;
    8           case DOWN: ball_dy += 1; break;
    9           case LEFT: ball_dx -= 1; break;
   10           case RIGHT: ball_dx += 1; break;
   11           case FIRE: ball_dx = 0; ball_dy = 0; break;
   12           }
   13       }
   14
   15       protected void keyRepeated(int keyCode) {
   16           System.out.println("keyRepeated, keyCode = " + keyCode);
   17           keyPressed(keyCode);
   18       }
   19
   20       protected void keyReleased(int keyCode) {
   21           System.out.println("keyReleased, keyCode = " + keyCode);
   22       }

Läsanvisningar

Man kan läsa mer om klassen GameCanvas i kapitel 11, Using the Game API, i den nya kursboken Kicking Butt with MIDP and MSA.

I den gamla kursboken Beginning J2ME kan man läsa kapitel 14, The Game API.

Programmeringsövningar

  1. Det finns inget kommando för att avsluta studsbollsprogrammet. Lägg till ett Avsluta-kommando till canvasen.
  2. Lägg till en racket, som visas längst ner på canvasen. Den ritas som en fyrkant med fillRect. Håll reda på racketens bredd, x-koordinat och y-koordinat i tre variabler. Ändra studsbollsprogrammet så man inte längre styr bollens hastighet med knapparna, utan låt vänster- och högerknapparna på telefonen ändra racketens position i x-led.
  3. Ändra studsen mot den nedersta kanten av canvasen till att studsa mot racketen, men bara om bollen träffar racketen. Om bollen missar, ska spelet avslutas.
  4. Ladda ner exempel-midleten till kursbokens kapitel 11 från http://kickbutt.jonathanknudsen.com/. Får man problem med att en knapptryckning registreras som flera? I så fall, laga. Fungerar det olika i emulatorn, och (om du har en) på din riktiga telefon?

Föregående lektion | Lektionslista | Nästa lektion


Thomas Padron-McCarthy (Thomas.Padron-McCarthy@oru.se), 13 juni 2008