Entwicklung eines 2D Computerspiels in Java Teil 2 Motivation • Arbeiten mit grösseren Programmen! • Lesen und Manipulieren von Programmen anderer. Alle Beispielprogramme sind aus dem Buch: "Developing Games in Java." von David Brackeen http://www.brackeen.com/javagamebook Das Buch wird für die Vorlesung und die Übungen NICHT benötigt, der Programmcode für Kapitel 2 und 3 sollte übers Web geladen werden. Page 1 Spielentwicklung: was haben wir bis jetzt! SreeenManager.java Hier werden alle für den Bildschirm relevanten Einstellungen gesetzt. Vollbildschirm, Pixelauflösung, Bufferstrategy. Animation.java Abfolge von Bildern ergibt eine Animation eines Objekts (Person). AnimationTest.java ( mit main ) Zeigt Animation vor Hintergrund. Animation • Dynamisches verändern des Bildes! Am einfachsten ist eine Abfolge von fertigen Teilbildern. Bild A Zeit ms 0ms Bild B 200ms Bild C 275ms Page 2 Bild B 300ms end Animation Abfolge von einzelnen Teilbildern wobei jedes Bild für eine bestimmte Zeit präsentiert wird. Teilbilder : class AnimFrame { Image image; long endTime; public AnimFrame( Image image, long endTime ) { this.image = image; this.endTime = endTime; } } Animation.java public synchronized void update(long elapsedTime) { if (frames.size() > 1) { animTime += elapsedTime; public class Animation { private private private private ArrayList frames; int currFrameIndex; long animTime; long totalDuration; /** Creates a new, empty Animation. public Animation() { ……… } if (animTime >= totalDuration) { animTime = animTime % totalDuration; currFrameIndex = 0; } */ while (animTime > getFrame(currFrameIndex).endTime) { currFrameIndex++; } public synchronized void addFrame(Image image, }long duration) {……. } /** Adds an image to the animation with the specified duration (time to display the image). */ /** Starts this animation over from the beginning. /** Updates this animation's current image (frame), if neccesary. */ /** Gets this Animation's current image. Returns null if this animation has no images. */ public synchronized void start() { ………..} */ public synchronized void update(long elapsedTime) {…………. } public synchronized Image getImage() {…….. } private AnimFrame getFrame(int i) { } } private class AnimFrame { } Page 3 BufferStrategy Klasse API ---> gibt guten Überblick zu den verschieden Möglichkeiten. Canvas, Window besitzen eine BufferStrategy: Für das Jframe möchten wir mindestens 2 Buffer: // Create a general double-buffering strategy frame.createBufferStrategy(2); BufferStrategy strategy = frame.getBufferStrategy(); // Render loop while (!done) { Graphics g = strategy.getDrawGraphics(); // Draw to graphics draw(g); g.dispose(); strategy.show(); // switch to next buffer } Verbesserter ScreenManager 'Double Buffering' + 'Active Rendering' sollen von ScreenManager übernommenen werden. Weitere Verbesserungen: Screenmanager vergleicht automatisch mögliche Displaymodi mit einer vorgegebenen internen Liste. Da 'Active Rendering' verwendet wird, wird die automatische paint() Methode abgeschaltet. frame.setIgnoreRepaint(true) ScreenManager.java soll hier nicht im Detail besprochen werden, relativ viel Code zur Anpassung an die vielen verschieden Plattformen! Page 4 AnimationTest2.java public class AnimationTest2 { public static void main(String args[]) { ..changed.. } private static final DisplayMode POSSIBLE_MODES[] = { new DisplayMode(1024, 768, 32, 0), new DisplayMode( 800, 600, 32, 0), ………. }; private private private private static final long DEMO_TIME = 10000; SimpleScreenManager screen; Image bgImage; Animation anim; public void loadImages() { ..as before } private Image loadImage(String fileName) {..as before } public void run(DisplayMode displayMode) { ..changed.. } public void animationLoop() {..changed.. } } public void draw(Graphics g) {..as before } AnimationTest1.java public void animationLoop() { long startTime = System.currentTimeMillis(); long currTime = startTime; AnimationTest2.java public void animationLoop() { long startTime = System.currentTimeMillis(); long currTime = startTime; while (currTime - startTime < DEMO_TIME) { long elapsedTime = System.currentTimeMillis() - currTime; currTime += elapsedTime; while (currTime - startTime < DEMO_TIME) { long elapsedTime = System.currentTimeMillis() - currTime; currTime += elapsedTime; // update animation anim.update(elapsedTime); // update animation anim.update(elapsedTime); // draw and update screen Graphics2D g = screen.getGraphics(); draw(g); g.dispose(); screen.update(); // basically strategy.show(); // draw to screen Graphics g = screen.getFullScreenWindow().getGraphics(); draw(g); g.dispose(); // take a nap try { Thread.sleep(20); } catch (InterruptedException ex) { } // take a nap try { Thread.sleep(20); } catch (InterruptedException ex) { } } } } } Page 5 AnimationTest1.java AnimationTest2.java public static void main(String args[]) { DisplayMode displayMode; if (args.length == 3) {displayMode = new DisplayMode( Integer.parseInt(args[0]), Integer.parseInt(args[1]), Integer.parseInt(args[2]), DisplayMode.REFRESH_RATE_UNKNOWN ); } else { displayMode = new DisplayMode(800, 600, 16, DisplayMode.REFRESH_RATE_UNKNOWN); } AnimationTest1 test = new AnimationTest1(); test.run(displayMode); } public void run(DisplayMode displayMode) { screen = new SimpleScreenManager(); try { screen.setFullScreen(displayMode, new JFrame()); loadImages(); animationLoop(); } finally { screen.restoreScreen(); } } public static void main(String args[]) { AnimationTest2 test = new AnimationTest2(); test.run(); } public void run() { screen = new ScreenManager(); try { DisplayMode displayMode = screen.findFirstCompatibleMode(POSSIBLE_MODES); screen.setFullScreen(displayMode); loadImages(); animationLoop(); } finally { screen.restoreScreen(); } } AnimationTest 2 .draw() public void draw(Graphics g) { // draw background g.drawImage(bgImage, 0, 0, null); // draw image g.drawImage(anim.getImage(), 0, 0, null); } Page 6 Schnelle Animation mit „Sprites“ „Sprites“ sind Rasterbilder die einem Hintergrund überlagert werden. In unserem Fall sind Sprites Animationen (Bildfolgen) die als Ganzes über den Bildschirm bewegt werden. Sprite.java public class Sprite { private Animation anim; // position (pixels) private float x; private float y; // velocity (pixels per millisecond) private float dx; private float dy; /** Constructor */ public Sprite (Animation anim) { this.anim = anim; } /** Updates this Sprite's Animation and its position */ public void update (long elapsedTime) { x += dx * elapsedTime; y += dy * elapsedTime; anim.update(elapsedTime); } ........ + many set and get Methods for sprite parameter Page 7 Sprite.java (2) ...... /** Sets and gets this Sprite's current x position. public float getX() { return x; } public float getY() { return y; } public void setX(float x) { this.x = x; } public void setY(float y) { this.y = y; } /** */ Gets this Sprite's width, based on the size of the current image. */ public int getWidth() { return anim.getImage().getWidth(null); } public int getHeight() { return anim.getImage().getHeight(null); } /** Gets and sets the horizontal velocity of this Sprite in pixels per millisecond. */ public float getVelocityX() { return dx; public float getVelocityY() { return dy; } } public void setVelocityX(float dx) { this.dx = dx; } public void setVelocityY(float dy) { this.dy = dy; } /** Gets this Sprite's current image. */ public Image getImage() { return anim.getImage(); } } .......... SpriteTest2.java public void run() { as usual } public class SpriteTest2 { public void animationLoop() { also includes drawFade } public static void main(String args[]) { public void drawFade(Graphics2D g, long currTime) { } SpriteTest2 test = new SpriteTest2(); test.run(); public void update(long elapsedTime) { .... } ..... updates all Sprites and each Sprite updates ts animation .... } private static final DisplayMode POSSIBLE_MODES[] = { public void draw(Graphics2D g) { ... draws and new DisplayMode(800, 600, 32, 0), transforms the sprites .................}; } private static final long DEMO_TIME = 10000; } private static final long FADE_TIME = 1000; private static final int NUM_SPRITES = 3; private ScreenManager screen; private Image bgImage; private Sprite sprites[]; public void loadImages() { } Page 8 Zum vollständigen Spiel Wir haben: SreeenManager.java Animation.java Sprite.java Es fehlt noch: InputManager.java A) Interaktion mit Tastatur und Maus B) Unterschiedliche Verhalten- / Bewegungsmuster der Sprites C) Verküpfung von A) und B) in GameAction.java extend GameCore.java Die Integration von allem ergibt das Spiel! abstract GameCore.java /** Player.java ( ch03src ) Simple abstract class used for testing. Subclasses should implement the draw() method. */ public abstract class GameCore { /** protected static final int FONT_SIZE = 24; private static final DisplayMode POSSIBLE_MODES[] = { Sets full screen mode and initiates and objects. */ public void init() { } new DisplayMode(800, 600, 32, 0), ......... /** }; Runs through the game loop until stop() is called. */ public void gameLoop() { } private boolean isRunning; /** protected ScreenManager screen; /** Signals the game loop that it's time to quit public void update(long elapsedTime) { */ // do nothing public void stop() { } isRunning = false; } /** Calls init() and gameLoop() Updates the state of the game/animation based on the amount of elapsed time that has passed. */ /** */ public void run() { } Draws to the screen. Subclasses must override this method. */ public abstract void draw(Graphics2D g); } Page 9 abstract GameCore.java /** ( ch03src ) /** Calls init() and gameLoop() */ Sets full screen mode and initiates and objects. */ public void run() { public void init() { try { screen = new ScreenManager(); init(); DisplayMode displayMode = screen.findFirstCompatibleMode(POSSIBLE_MODES); gameLoop(); screen.setFullScreen(displayMode); } finally { Window window = screen.getFullScreenWindow(); screen.restoreScreen(); window.setFont(new Font("Dialog", Font.PLAIN, FONT_SIZE) ); } } window.setBackground(Color.blue); window.setForeground(Color.white); isRunning = true; } abstract GameCore.java ( ch03src ) public void gameLoop() { long startTime = System.currentTimeMillis(); /** long currTime = startTime; Calls init() and gameLoop() */ while (isRunning) { public void run() { long elapsedTime = try { System.currentTimeMillis() - currTime; currTime += elapsedTime; init(); gameLoop(); // update } update(elapsedTime); finally { screen.restoreScreen(); // draw the screen Graphics2D g = screen.getGraphics(); } draw(g); } g.dispose(); screen.update(); // take a nap try { Thread.sleep(20); } catch (InterruptedException ex) { } } } Page 10 Player extends Sprite Ein Spriteobjekt hat eine - Animation Geschwindigkeit Position Aus der Geschwindigkeit, der alten Position und der Zeit seit dem letzten Update ergibt sich die neue Position. Ein "Player" soll nun einen Sprung durchführen können. Ein zweiter Sprung darf erst nach der Landung wieder ausgelöst werden. Wie soll ein Sprung simuliert werden? Welche Parameter werden hierzu benötigt? Wie in der realen Welt? • • • Wir heben mit einer konstanten Geschwindigkeit nach oben ab! Wir werden durch die Erdanziehung gebremst und fallen wieder herunter! Wir kommen auf dem Boden wieder zur Ruhe! Player extends Sprite Ein Spriteobjekt hat eine - Animation - Geschwindigkeit - Position Ein "Player" benötigt zusätzlich: - Startgeschindigkeit - Bremsbescheunigung - Position des Bodens - Zustandsvariable : IM-SPRUNG Sprungsimulation: Vy = Vsprung; while (PositionY > PositionBoden ) { Vy = Vsprung; + g* Zeit_seit _Absprung; } Page 11 Player extends Sprite public void jump() { setVelocityY(-1); public class Player extends Sprite { state = STATE_JUMPING; } public static final int STATE_NORMAL = 0; public void update (long elapsedTime) { public static final int STATE_JUMPING = 1; // set vertical velocity (gravity effect) public static final float SPEED = .3f; if (getState() == STATE_JUMPING) { public static final float GRAVITY = .002f; setVelocityY(getVelocityY() private int floorY; + GRAVITY * elapsedTime); private int state; } // move player public Player(Animation anim) { super.update(elapsedTime); super(anim); state = STATE_NORMAL; // check if player landed on floor } if (getState() == public int getState() { return state; } STATE_JUMPING && getY() >= floorY) { public void setState(int state) { .....} setVelocityY(0); public void setFloorY(int floorY) {.....} setY(floorY); public void jump() {..... } setState(STATE_NORMAL); public void update(long elapsedTime) {..... } } } } InputManagerTest extends GameCore InputManagerTest.java init() Setzen von : ScreenManager() 1. Bildschirmeinstellungen 2. Spielzustände mit Maus und Tastatur verknüpfen 3. GameAction() • Beenden • Pause • Bewegungsmuster des Spielers {LINKS, RECHTS, SPRINGEN ... } Sprites erstellen gameLoop() Animationszyklus : 1. Alle Sprites bzgl. Tastatur und Zeit aktualisieren. 2. Alle entsprechenden Bilder anzeigen. 3. Eventuell warten 4. Wieder zu Schritt eins. Page 12 InputManagerTest.java public class InputManagerTest extends GameCore { public static void main(String[] args) { new InputManagerTest().run(); } ............... protected GameAction jump; protected GameAction exit; protected GameAction moveLeft; public void init() {......... } protected GameAction moveRight; public boolean isPaused() {......... } protected GameAction pause; public void setPaused(boolean p) {......... } public void update(long elapsedTime) {........} protected InputManager inputManager; public void checkSystemInput() {......... private Player player; public void checkGameInput() {......... } // extends Sprite } private Image bgImage; public void draw(Graphics2D g) {......... } private boolean paused; public void createGameActions() {......... } private void createSprite() {......... } } .............. InputManagerTest.java (2) /** Calls init() and gameLoop() */ public void run() { public void init() { try { init(); gameLoop(); super.init(); // from // GameCore inputManager = new InputManager( screen.getFullScreenWindow()); } finally { screen.restoreScreen(); createGameActions(); } createSprite(); } paused = false; } Page 13 InputManagerTest.java (3) public void gameLoop() { public void update(long elapsedTime) { long startTime = System.currentTimeMillis(); // check input that can happen whether paused or not long currTime = startTime; checkSystemInput(); while (isRunning) { if (!isPaused()) { long elapsedTime = // check game input System.currentTimeMillis() - currTime; checkGameInput(); currTime += elapsedTime; // update sprite // update player.update(elapsedTime); update(elapsedTime); } } // draw the screen Graphics2D g = screen.getGraphics(); public void draw(Graphics2D g) { draw(g); g.dispose(); // draw background screen.update(); g.drawImage(bgImage, 0, 0, null); // draw sprite g.drawImage(player.getImage(), // take a nap Thread.sleep(20); } Math.round(player.getX()), catch (InterruptedException ex) { } Math.round(player.getY()), try { null); } } } InputManagerTest.java (4) public void update(long elapsedTime) { public void checkSystemInput() { // check input that can happen if (pause.isPressed()) { // whether paused or not setPaused(!isPaused()); checkSystemInput(); } if (exit.isPressed()) { if (!isPaused()) { stop(); // check game input checkGameInput(); // update sprite } } public void checkGameInput() { float velocityX = 0; player.update(elapsedTime); if (moveLeft.isPressed()) } { velocityX -=Player.SPEED; } if (moveRight.isPressed()) { velocityX +=Player.SPEED;} } player.setVelocityX(velocityX); if (jump.isPressed() && player.getState() != Player.STATE_JUMPING) { player.jump(); } } Page 14 InputManagerTest.java (5) public void init() { public void createGameActions() { jump = new GameAction("jump", GameAction.DETECT_INITAL_PRESS_ONLY); super.init(); exit = new GameAction("exit", GameAction.DETECT_INITAL_PRESS_ONLY); moveLeft = new GameAction("moveLeft"); moveRight = new GameAction("moveRight"); inputManager = pause = new GameAction("pause", GameAction.DETECT_INITAL_PRESS_ONLY); new InputManager( screen.getFullScreenWindow()); inputManager.mapToKey(exit, KeyEvent.VK_ESCAPE); createGameActions(); inputManager.mapToKey(pause, KeyEvent.VK_P); createSprite(); paused = false; // jump with spacebar or mouse button } inputManager.mapToKey(jump, KeyEvent.VK_SPACE); inputManager.mapToMouse(jump, InputManager.MOUSE_BUTTON_1); // move with the arrow keys... inputManager.mapToKey(moveLeft, KeyEvent.VK_LEFT); inputManager.mapToKey(moveRight, KeyEvent.VK_RIGHT); // ... or with A and D. inputManager.mapToKey(moveLeft, KeyEvent.VK_A); inputManager.mapToKey(moveRight, KeyEvent.VK_D); } GameAction.java Die Klasse GameAction ordnet jedem Zustand oder jeder Zustandsänderung im Spiel (Pause, nachLinks, nachRechts, Springen) einem Zustand einer Taste oder Maus zu (pressed, released, .....). Damit entsteht eine Trennung vom eigentlichen Abfragen der Events und den eigentlichen Aktionen im Spiel. Ein GameAction-Objekt weiss nicht welcher Tasten oder Mausfunktion sie zugeordnet ist. Es kennt nur die Zustände in der die entsprechende Taste oder Maus ist. InputManager fängt alle Maus und Tastatur Events ab und kennt welche Taste welche GameAction verändert. Der Page 15 GameAction.java public class GameAction { public static final int NORMAL = 0; public static final int DETECT_INITAL_PRESS_ONLY = 1; private static final int STATE_RELEASED = 0; private static final int STATE_PRESSED = 1; private static final int STATE_WAITING_FOR_RELEASE = 2; private private private private String name; int behavior; int amount; int state; public GameAction(String name) {.... } public GameAction(String name, int behavior) {.... } /** Create a new GameAction with the specified behavior. */ public GameAction(String name, int behavior) { this.name = name; this.behavior = behavior; reset(); } /** Signals that the key was pressed. public synchronized void press() { press(1); } */ /** Signals that the key was pressed a specified number of times, or that the mouse move a spcified distance. public String getName() {.... } */ public void reset() {.... } public synchronized void tap() {.... public synchronized void press(int amount) { if (state != STATE_WAITING_FOR_RELEASE) { this.amount+=amount; state = STATE_PRESSED; } } public synchronized void press() {.... } public synchronized void press(int amount) {.... } public synchronized void release() {.... } public synchronized boolean isPressed() {.... } } public synchronized int getAmount() {.... } } GameAction.java public class GameAction { /** public static final int NORMAL = 0; public static final int DETECT_INITAL_PRESS_ONLY = 1; private static final int STATE_RELEASED = 0; private static final int STATE_PRESSED = 1; private static final int STATE_WAITING_FOR_RELEASE = 2; private private private private String name; int behavior; int amount; int state; Returns whether the key was pressed or not since last checked. */ public synchronized boolean isPressed() { return (getAmount() != 0); } /** For keys, this is the number of times the key was pressed since it was last checked. public GameAction(String name) {.... } public GameAction(String name, int behavior) {.... } For mouse movement, this is the distance moved. */ public synchronized int getAmount() { int retVal = amount; if (retVal != 0) { public String getName() {.... } if (state == STATE_RELEASED) { amount = 0; public void reset() {.... } public synchronized void tap() {.... } else if (behavior == DETECT_INITAL_PRESS_ONLY) { } public synchronized void press() {.... } public synchronized void press(int amount) {.... state = STATE_WAITING_FOR_RELEASE; amount = 0; } } public synchronized void release() {.... } public synchronized boolean isPressed() {.... } public synchronized int getAmount() {.... } } } return retVal; } } Page 16 InputManager.java InputManager implements KeyListener, MouseListener, MouseMotionListener, MouseWheelListener Folgende Methoden müssen implementiert werden void keyPressed(KeyEvent e) void keyReleased(KeyEvent e) void keyTyped(KeyEvent e) Der InputManager fängt alle Maus und Tastatur Events ab und kennt welche Taste welche GameAction verändert. InputManager.java ................ public class InputManager implements KeyListener // ALL MOUSE RELATED FUNCTIONALITY REMOVED /** Clears all mapped keys and mouse actions to this GameAction. */ { public void clearMap(GameAction gameAction) {..... } // key codes are defined in java.awt.KeyEvent. // most of the codes (except for some rare ones like // "alt graph") are less than 600. private static final int NUM_KEY_CODES = 600; /** Gets a List of names of the keys and mouse actions mapped to this GameAction. Each entry in the List is a string. */ public List getMaps(GameAction gameCode) {..... } private GameAction[] keyActions = new GameAction[NUM_KEY_CODES]; /** Resets all GameActions so they appear like they haven't been pressed. */ public void resetAllGameActions() {..... } private Component comp; public InputManager(Component comp) { this.comp = comp; /** Gets the name of a key code. */ public static String getKeyName(int keyCode) { return KeyEvent.getKeyText(keyCode); } // register key listeners comp.addKeyListener(this); // allow input of the TAB key and other keys // normally used for focus traversal comp.setFocusTraversalKeysEnabled(false); private GameAction getKeyAction(KeyEvent e) {..... // from the KeyListener interface public void keyPressed(KeyEvent e) {..... } public void keyReleased(KeyEvent e) {..... } public void keyTyped(KeyEvent e) {..... } } public void mapToKey(GameAction gameAction, int keyCode) { keyActions[keyCode] = gameAction; } ................ } Page 17 } InputManager.java (2) // from the KeyListener interface public void keyPressed(KeyEvent e) { GameAction gameAction = getKeyAction(e); if (gameAction != null) { gameAction.press(); } // the key isn't processed for anything else e.consume(); } private GameAction getKeyAction(KeyEvent e) { int keyCode = e.getKeyCode(); if (keyCode < keyActions.length) { return keyActions[keyCode]; } else { return null; } } // from the KeyListener interface public void keyReleased(KeyEvent e) { GameAction gameAction = getKeyAction(e); if (gameAction != null) { gameAction.release(); } e.consume(); } // from the KeyListener interface public void keyTyped(KeyEvent e) { e.consume(); } Wiederholung InputManagerTest.java Page 18