Plansza aksonometryczna
Założenia
Chcemy stworzyć planszę aksonometryczną z postacią poruszaną z klawiatury przy użyciu strzałek
Na wideo kolejno używam kolejno strzałki w lewo, w górę, w prawo i w dół.
Rysunki do animacji wykonała Martyna Sobienikow. Postać na cześć M.S. nazwałem
Martynką.
Przygotowanie
Animacja ruchu składa się z czterech obrazków z postacią ustawioną w prawo (Rys. 136)
oraz z czterech obrazków z postacią ustawiona w lewo (Rys. 137)
Obrazki mają wielkość 42 x 85 pikseli dopasowana do wielkości planszy.
Struktura projektu
Mój projekt ma strukturę (Rys. 138)
Kody
Plik module-info
module planszaakson { requires java.desktop; }
Klasa AnimFrame
package planszaakson; import java.awt.*; import java.awt.image.*; import javax.swing.*; class AnimFrame extends JComponent { private static final long serialVersionUID = -2960156825708485829L; private final BufferedImage bimage; public AnimFrame(String obrazek, boolean flip) { if (flip) { bimage = U.flipHorizontally(U.fileToBimage(obrazek)); } else { bimage = U.fileToBimage(obrazek); } setLayout(null); setSize(U.ANIMFRAME_WIDTH, U.ANIMFRAME_HEIGHT); setBackground(Color.WHITE); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; g2.drawImage(bimage, 0, 0, U.ANIMFRAME_WIDTH, U.ANIMFRAME_HEIGHT, this); } }
Klasa Sprite
package planszaakson; import java.awt.*; import java.awt.event.*; import java.io.*; import javax.swing.*; /** * animacja z wykorzystaniem klasy Timer i CardLayout */ class Sprite extends JComponent implements ActionListener { private static final long serialVersionUID = -1708095494186169452L; private final CardLayout cl; private final File[] files; private int frames; private final Timer timer; private boolean going; private boolean loop; public Sprite(String dir, String ext, boolean flip) { cl = new CardLayout(); setPreferredSize(new Dimension(182, 335)); setBounds(0, 0, 182, 335); setBackground(Color.WHITE); setLayout(cl); files = U.listFiles("planszaakson/assets/" + dir, ext); frames = files.length; for (File file : files) { AnimFrame card = new AnimFrame("" + file, flip); add(card, "" + file); } going = false; loop = false; timer = new Timer(0, this); timer.setInitialDelay(0); timer.setDelay(60); timer.start(); } @Override public void actionPerformed(ActionEvent e) { if (going) { if (!loop) { if (frames > 0) { cl.next(Sprite.this); frames--; } else { timer.stop(); } } else { cl.next(Sprite.this); } } } public void setGoing(boolean going) { this.going = going; if (going) { frames = files.length; timer.restart(); } else { timer.stop(); } } public void setLoop(boolean loop) { this.loop = loop; } }
Klasa U
package planszaakson; import java.awt.*; import java.awt.geom.*; import java.awt.image.*; import java.io.*; import javax.imageio.*; class U { private U() { } public static final int FRAME_WIDTH = 640; public static final int FRAME_HEIGHT = 480; public static final int ANIMFRAME_WIDTH = 45; public static final int ANIMFRAME_HEIGHT = 87; public static final Dimension DIM = new Dimension(FRAME_WIDTH, FRAME_HEIGHT); public static final Dimension DIM1 = new Dimension(ANIMFRAME_WIDTH, ANIMFRAME_HEIGHT); public static final int SCREEN_WIDTH = Toolkit.getDefaultToolkit() .getScreenSize().width; public static final int SCREEN_HEIGHT = Toolkit.getDefaultToolkit() .getScreenSize().height; /** * tworzy listę plików File typu "*ext" w podanym katalogu * * @param dir String - katalog zawierający pliki * @param ext String - rozszerzenie, np. "png" * @return File[] - lista plików w podanym katalogu */ public static File[] listFiles(String dir, String ext) { File file = new File(dir); File[] files = null; if (file.isDirectory()) { FileExtensionFilter fef = new FileExtensionFilter(file, ext); files = file.listFiles(fef); } else { System.out.println("Podaj nazwę katalogu z obrazkami"); } return files; } /** * Wczytuje obrazek z pliku i zamienia go na BufferedImage * Ta metoda, z nieznanych mi przyczyn nie chciała działać z filtrami * <code>RescaleOp</code> i <code>ConvolveOp</code>, czyli np. blur(), * sharp(), brightness() * * @param plik String - wskazuje plik z obrazkiem * @return BufferedImage - zwraca obrazek buforowany znajdujący się w pliku */ public static BufferedImage fileToBimage(String plik) { File f = new File(plik); BufferedImage bimage = null; try { bimage = ImageIO.read(f); } catch (IOException e) { e.printStackTrace(); } return bimage; } /** * Filtr odwracający BufferedImage w poziomie (wzdłuż osi pionowej) * * @param bimage BufferedImage - filtrowany obrazek * @return BufferedImage - przefiltrowany obrazek */ public static BufferedImage flipHorizontally(BufferedImage bimage) { AffineTransform tx = AffineTransform.getScaleInstance(-1, 1); tx.translate(-bimage.getWidth(null), 0); AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); return op.filter(bimage, null); } }
Klasa FileExtensionFilter
package planszaakson; import java.io.*; /** * Filtr plików z danym rozszerzeniem * np. dla png wpisujemy "png" */ class FileExtensionFilter implements FilenameFilter { /** * Tworzy obiekt filtra dla podanego katalogu i rozszerzenia */ public FileExtensionFilter(File dir, String ext) { accept(dir, ext); } @Override public boolean accept(File dir, String ext) { return ext.endsWith(ext) || ext.endsWith(ext.toUpperCase()); } }
Klasa PlanszaAkson
package planszaakson; import java.awt.*; import java.awt.event.*; import javax.swing.*; class PlanszaAkson extends JFrame implements KeyListener { private static final long serialVersionUID = 5509515767913681613L; private static int martyx = U.FRAME_WIDTH / 2;//posx private static int martyy = U.FRAME_HEIGHT / 2;//posy private static final int walkxspeed = 6;//xspeed private static final int walkyspeed = 3;//yspeed private static int walkmode = 0; private Sprite anim; public PlanszaAkson() { setLayout(null); setTitle("Plansza aksonometryczna"); setResizable(false); setPreferredSize(U.DIM); setBackground(Color.WHITE); getContentPane().setBackground(Color.WHITE); setBounds((U.SCREEN_WIDTH - U.FRAME_WIDTH) / 2, (U.SCREEN_HEIGHT - U.FRAME_HEIGHT) / 2, U.FRAME_WIDTH, U.FRAME_HEIGHT); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); anim = new Sprite("R", "png", false); anim.setBounds(martyx - U.ANIMFRAME_WIDTH / 2, martyy - U.ANIMFRAME_HEIGHT, U.ANIMFRAME_WIDTH, U.ANIMFRAME_HEIGHT); add(anim); addKeyListener(this); setVisible(true); } @Override public void keyPressed(KeyEvent e) { int keyCode = e.getKeyCode(); switch (keyCode) { case KeyEvent.VK_RIGHT: this.remove(anim); walkmode = 1; martyx += walkxspeed; martyy += walkyspeed; anim = new Sprite("R", "png", false); break; case KeyEvent.VK_LEFT: this.remove(anim); walkmode = 2; martyx -= walkxspeed; martyy -= walkyspeed; anim = new Sprite("L", "png", false); break; case KeyEvent.VK_UP: this.remove(anim); walkmode = 3; martyx += walkxspeed; martyy -= walkyspeed; anim = new Sprite("L", "png", true); break; case KeyEvent.VK_DOWN: this.remove(anim); walkmode = 4; martyx -= walkxspeed; martyy += walkyspeed; anim = new Sprite("R", "png", true); break; } if (walkmode > 0) { anim.setBounds(martyx - U.ANIMFRAME_WIDTH / 2, martyy - U.ANIMFRAME_HEIGHT, U.ANIMFRAME_WIDTH, U.ANIMFRAME_HEIGHT); add(anim); anim.setGoing(true); repaint(); } } @Override public void keyReleased(KeyEvent e) { e.consume(); walkmode = 0; } @Override public void keyTyped(KeyEvent e) { e.consume(); walkmode = 0; } }
Klasa Main
package planszaakson; import javax.swing.*; class Main { public static void main(String[] args) { SwingUtilities.invokeLater(PlanszaAkson::new); } }
Wynik
Po uruchomieniu kodu otrzymujemy ramkę. (Rys. 139)
Użycie strzałek zostało pokazane na wideo na początku.
Uwagi
Pomysł klas AnimFrame
i Sprite
był wzorowany na rozwiązaniu przedstawionym w książce
Brackeen D i in., 2004. Java.Tworzenie gier, Wyd. Helion.
Pomysł planszy aksonometrycznej z postacią Martynki był wzorowany na pomyśle z książki Turner B. i in. 2001.
Flash 5. Gry i kreskówki. F/x. Efekty specjalne. Wyd. Helion. Postacią animowaną był tam Walter.