Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.
Pouze tento týden sleva až 80 % na e-learning týkající se Swiftu. Zároveň využij výhodnou slevovou akci až 30 % zdarma při nákupu e-learningu - více informací.
swift week + discount 30
Avatar
qw3rtz
Člen
Avatar
qw3rtz:28.12.2021 22:13

Zdravím, chtěl bych Vás požádat, jestli byste se mi nepodívali na takovou trochu trapnou hru, kterou jsem zkusil udělat. Vím, že ten design není nic moc, ale to teď moc neřeším, spíš by mě zajímalo jestli je to správně programátorsky (jestli tam nejsou nejaký blbosti), příp. co byste upravili nebo udělali jinak 😀

_____________­________________________­_____
Třída Hráče


package gui2;

import java.awt.Color;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JPanel;

public class Player extends Entity {

    public Player(int size, Color color, JPanel jp) {
        super(color, size, jp);
        setEntityX((jp.getWidth() - getEntitySize()) / 2);
        setEntityY((jp.getHeight() - getEntitySize()) / 2);

        jp.addMouseMotionListener(new MouseAdapter() {
            @Override
            public void mouseMoved(MouseEvent e) {
                setEntityX(e.getX() - getEntitySize() / 2);
                setEntityY(e.getY() - getEntitySize() / 2);
            }
        });
    }
}

_____________­_______________________
Třída protihráče


package gui2;

import java.awt.Color;
import java.util.Random;
import javax.swing.JPanel;

public class Enemy extends Entity {

    private int xMove;
    private int yMove;

    public Enemy(Color color, int size, JPanel jp) {
        super(color, size, jp);
        setEntityX((jp.getWidth() - getEntitySize()) / 4);
        setEntityY((jp.getHeight() - getEntitySize()) / 4);
        xMove = getMove();
        yMove = getMove();

    }

    public void move() {
        setEntityX(getEntityX() + xMove);
        setEntityY(getEntityY() + yMove);
    }

    public int getMove() {
        Random rd = new Random();
        return (2 * rd.nextInt(2) - 1) * (rd.nextInt(3) + 1);
    }

    public int getXMove() {
        return xMove;
    }

    public int getYMove() {
        return yMove;
    }

    public void update1() {
        int a = super.update();
        switch (a) {
            case 0:
            case 2:
                xMove *= -1;
                yMove = getMove();
                break;

            case 1:
            case 3:
                xMove = getMove();
                yMove *= -1;
                break;
        }
    }
}

_____________­_________________
Třída obecné Entity

package gui2;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;
import javax.swing.JPanel;

public abstract class Entity {

    private int x;
    private int y;
    private int size;
    private Color color;
    private JPanel jp;

    public Entity(Color color, int size, JPanel jp) {
        this.size = size;
        this.color = color;
        this.jp = jp;
    }

    public void render(Graphics g) {
        g.setColor(getEntityColor());
        g.fillRect(getEntityX(), getEntityY(), getEntitySize(), getEntitySize());
        g.drawRect(getEntityX(), getEntityY(), getEntitySize(), getEntitySize());
    }

    public int update() {
        if (getEntityX() < 0) {
            setEntityX(0);
            return 0;
        }
        if (getEntityY() < 0) {
            setEntityY(0);
            return 1;
        }
        if (getEntityX() + getEntitySize() > jp.getWidth()) {
            setEntityX(jp.getWidth() - getEntitySize());
            return 2;
        }
        if (getEntityY() + getEntitySize() > jp.getHeight()) {
            setEntityY(jp.getHeight() - getEntitySize());
            return 3;
        }
        return -1;
    }

    public Rectangle getBounds() {
        return new Rectangle(getEntityX(), getEntityY(), getEntitySize(), getEntitySize());
    }

    public Color getEntityColor() {
        return color;
    }

    public int getEntityX() {
        return x;
    }

    public int getEntityY() {
        return y;
    }

    public int getEntitySize() {
        return size;
    }

    public void setEntityX(int x) {
        this.x = x;
    }

    public void setEntityY(int y) {
        this.y = y;
    }

    public void setEntitySize(int size) {
        this.size = size;
    }

    public void setEntityColor(Color color) {
        this.color = color;
    }
}

_____________­_______________________
Třída Menu


/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package gui2;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JSpinner.DefaultEditor;
import javax.swing.JTextArea;
import javax.swing.SpinnerListModel;

public class Menu extends JPanel {

    private JButton[] jbs;
    private JButton jbZpet;
    private JButton jbReset;
    private boolean render = true;
    private JLabel[] jls;
    private NewClass.Settings gameSettings = new NewClass.Settings();
    private JTextArea jl = new JTextArea(10, 10);
    private JButton jbNastav = new JButton("Uložit nastavení");
    private JSpinner spinner;
    private JSpinner spinner2 = new JSpinner(new SpinnerListModel(Colors.values()));
    private JSpinner spinner3;
    private GridBagConstraints c;
    private JFrame jf;

    public Menu(JFrame jf) {
        JPanel jp = this;
        this.jf = jf;
        Integer[] a = new Integer[11];
        Integer[] b = new Integer[11];
        for (int i = 0; i < a.length; i++) {
            a[i] = 20 + i;
            b[i] = 3 + i;
        }

        ((DefaultEditor)spinner2.getEditor()).getTextField().setColumns(5);
        spinner = new JSpinner(new SpinnerListModel(a));
        ((DefaultEditor)spinner.getEditor()).getTextField().setColumns(2);
        spinner3 = new JSpinner(new SpinnerListModel(b));
        ((DefaultEditor)spinner3.getEditor()).getTextField().setColumns(2);

        add(jbNastav);
        jbNastav.setVisible(false);
        jbNastav.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                setSettings((Integer) spinner3.getValue(), ((Colors) spinner2.getValue()).getColor(), (Integer) spinner.getValue());
                jbZpet.doClick();
            }

        });

        add(jl);
        jl.setVisible(false);

        setPreferredSize(new Dimension(500, 400));
        setLayout(new GridBagLayout());

        c = new GridBagConstraints();

        jls = new JLabel[3];
        jls[0] = new JLabel("Počet nepřátel: ");
        jls[1] = new JLabel("Barva hráče: ");
        jls[2] = new JLabel("Velikost hráče a nepřátel: ");

        for (int i = 0; i < jls.length; i++) {
            c.gridx = 0;
            c.gridy = i;
            c.insets = new Insets(10, 10, 10, 10);
            add(jls[i], c);
            jls[i].setVisible(false);
        }

        jbZpet = new JButton("Zpět");
        jbReset = new JButton("Smazat");

        c.gridx = 0;
        c.gridy = jls.length;
        add(jbZpet,c);
        jbZpet.setVisible(false);
        jbZpet.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                odeberTlacitkaStat();
                addSettingsComp(false);
                jl.setVisible(false);
                pridejMenu();
                visibleNastav(false);
            }

        });

        c.gridx = 0;
        c.gridy = 1;
        add(jbReset, c);
        jbReset.setVisible(false);
        jbReset.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                jl.setText("Žádné statistiky nejsou k dispozici - nebyly zapsány.");
                try {
                    new BufferedWriter(new FileWriter("stat.txt", false));
                } catch (IOException ex) {
                    JOptionPane.showConfirmDialog(jp, "Chyba při formátování souboru!");
                }
            }

        });

        jbs = new JButton[4];
        jbs[0] = new JButton("Hrát");
        jbs[1] = new JButton("Nastavení");
        jbs[2] = new JButton("Statistiky");
        jbs[3] = new JButton("Konec");

        for (int i = 0; i < jbs.length; i++) {
            c.gridx = 0;
            c.gridy = i;
            add(jbs[i], c);
            jbs[i].setVisible(true);
        }

        jbs[0].addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                jf.add(new NewClass(gameSettings, (Menu) jp, jf));
                jf.remove(jp);
                jf.revalidate();
                jf.pack();
            }

        });
        jbs[1].addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                odeberMenu();
                odeberTlacitkaStat();
                addSettingsComp(true);
            }

        });
        jbs[2].addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                jl.setLocation((getWidth() - jl.getWidth()) / 2, (getHeight() - jl.getHeight()) / 2);
                jl.setEditable(false);
                jl.setText(null);
                try {
                    BufferedReader br = new BufferedReader(new FileReader("stat.txt"));
                    String t = "";
                    boolean isEmpty = true;

                    while ((t = br.readLine()) != null) {
                        isEmpty = false;
                        String[] ar = t.split(";");
                        jl.append("Hráč: " + ar[0] + " - Čas: " + ar[1] + "\n");
                    }
                    if (t == null && isEmpty) {
                        jl.setText("Žádné statistiky nejsou k dispozici - nebyly zapsány.");
                    }
                    odeberMenu();
                    pridejTlacitkaStat();
                    br.close();
                } catch (FileNotFoundException ex) {
                    JOptionPane.showMessageDialog(jp, "Chyba při čtení do souboru - soubor nenalezen!", "CHYBA", JOptionPane.ERROR_MESSAGE);
                } catch (IOException ex) {
                    JOptionPane.showMessageDialog(jp, "Chyba při čtení do souboru!", "CHYBA", JOptionPane.ERROR_MESSAGE);
                }

                jl.setVisible(true);
            }

        });
        jbs[3].addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.exit(0);
            }

        });
    }

    public void visibleZpet(boolean vis) {
        jbZpet.setVisible(vis);
    }

    public void visibleNastav(boolean vis) {
        jbNastav.setVisible(vis);
    }

    public void setSettings(int numOfEnemy, Color cPlayer, int size) {
        gameSettings.setNumOfEnemy(numOfEnemy);
        gameSettings.setColorPlayer(cPlayer);
        gameSettings.setSize(size);
    }

    public void pridejTlacitkaStat() {
        jbZpet.setVisible(true);
        jbReset.setVisible(true);
    }

    public void odeberTlacitkaStat() {
        jbZpet.setVisible(false);
        jbReset.setVisible(false);
    }

    public void pridejMenu() {
        for (JButton jb : jbs) {
            jb.setVisible(true);
        }
    }

    public void odeberMenu() {
        for (JButton jb : jbs) {
            jb.setVisible(false);
        }
    }

    public void addSettingsComp(boolean show) {
        c.gridx = 1;
        c.gridy = 0;
        add(spinner3, c);
        c.gridx = 0;
        c.gridy = 0;
        add(jls[0], c);
        jls[0].setVisible(show);
        spinner.setVisible(show);
        c.gridx = 1;
        c.gridy = 1;
        add(spinner2, c);
        c.gridx = 0;
        c.gridy = 1;
        add(jls[1], c);
        jls[1].setVisible(show);
        spinner2.setVisible(show);
        c.gridx = 1;
        c.gridy = 2;
        add(spinner, c);
        c.gridx = 0;
        c.gridy = 2;
        add(jls[2], c);
        jls[2].setVisible(show);
        spinner3.setVisible(show);
        c.gridx = 0;
        c.gridy = 3;

        add(jbZpet, c);
        jbZpet.setVisible(show);
        c.gridx = 1;
        c.gridy = 3;
        add(jbNastav, c);
        jbNastav.setVisible(show);
    }

    public JButton getJbZpet() {
        return jbZpet;
    }

    private enum Colors {
        RED(Color.RED), GREEN(Color.GREEN), BLUE(Color.BLUE);

        private Color c;

        private Colors(Color c) {
            this.c = c;
        }

        public Color getColor() {
            return c;
        }
    }
}

_____________­________________________­______
Třída která řídí herní logiku


/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package gui2;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;

public class NewClass extends JPanel {

    private Player pl;
    private Enemy[] en;
    private boolean running;
    private boolean isRunning = true;
    private String cas = "";
    private JButton jb;
    private boolean odpocet;
    private boolean colisionOn = false;
    private Menu menu;
    private JButton playAgain;

    public NewClass(Settings s, Menu menu, JFrame jf) {
        odpocet = true;
        running = false;
        this.menu = menu;

        JPanel jp = this;

        playAgain = new JButton("Spustit znovu");
        add(playAgain);
        playAgain.setVisible(false);
        playAgain.addActionListener(new ActionListener(){
            @Override
            public void actionPerformed(ActionEvent e) {
                jf.remove(jp);
                jf.repaint();
                jf.add(new Menu(jf));
                jf.revalidate();
                jf.pack();
            }
        });

        jb = new JButton("Zapsat do statistiky");
        jb.setVisible(false);
        jb.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                boolean write = false;
                try {
                    BufferedWriter bw = new BufferedWriter(new FileWriter("stat.txt", true));
                    String s = JOptionPane.showInputDialog("Zadej své jméno: ");

                    String text = "";
                    for (char a : s.toCharArray()) {
                        write = true;
                        if ((' ') == a) {
                            text += "_";
                        } else {
                            text += a;
                        }
                    }
                    bw.write(text);
                    bw.write(";");
                    bw.write(cas);
                    bw.newLine();
                    bw.flush();
                    bw.close();
                } catch (IOException ex) {
                    JOptionPane.showMessageDialog(jp, "Chyba při zápisu do souboru! Zkuste znovu.", "CHYBA", JOptionPane.ERROR_MESSAGE);
                } catch (NullPointerException ex) {
                    JOptionPane.showMessageDialog(jp, "Nebylo zadáno jméno! Zkuste znovu.", "CHYBA", JOptionPane.ERROR_MESSAGE);
                }
                if(write){
                   jb.setEnabled(false);
                }

            }

        });
        add(jb);

        add(menu.getJbZpet());

        setPreferredSize(new Dimension(500, 400));

        pl = new Player(s.getSize(), s.getColorPlayer(), this);

        en = new Enemy[s.getNumOfEnemy()];
        Random rd = new Random();
        for (int i = 0; i < en.length; i++) {
            Color c = null;

            switch (rd.nextInt(4)) {
                case 0:
                    c = Color.RED;
                    break;
                case 1:
                    c = Color.BLACK;
                    break;
                case 2:
                    c = Color.GREEN;
                    break;
                case 3:
                    c = Color.ORANGE;
                    break;
                default:
                    c = Color.black;

            }
            en[i] = new Enemy(c, s.getSize(), this);
        }

        Thread t = new Thread() {
            @Override
            public void run() {
                while (isRunning) {
                    for (Enemy e : en) {
                        e.move();
                    }
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException ex) {
                        Logger.getLogger(NewClass.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            }
        };

        new Thread() {
            @Override
            public void run() {
                double poc = System.nanoTime() / Math.pow(10, 9);
                double ticks = 0;

                repaint();

                try {
                    Thread.sleep(5000);
                } catch (InterruptedException ex) {
                    Logger.getLogger(NewClass.class.getName()).log(Level.SEVERE, null, ex);
                }


                colisionOn = true;
                long start = System.currentTimeMillis() / 1000;

                while (isRunning) {
                    double akt = System.nanoTime() / Math.pow(10, 9);
                    ticks += (akt - poc) * 60;

                    while (ticks >= 1) {
                        ticks--;
                        if(odpocet){
                            t.start();
                            odpocet = false;
                        }
                        repaint();
                    }

                    pl.update();

                    for (Enemy e : en) {
                        e.update1();
                    }

                    if (colisionOn && checkColision()) {
                        cas = (System.currentTimeMillis() / 1000 - start) + "s";
                        isRunning = false;
                        repaint();
                    }
                    poc = akt;
                }
            }
        }.start();
    }

    public static class Settings {

        private int numOfEnemy = 3;
        private Color colorPlayer = Color.BLUE;
        private int size = 20;

        /**
         * @return the numOfEnemy
         */
        public int getNumOfEnemy() {
            return numOfEnemy;
        }

        /**
         * @param numOfEnemy the numOfEnemy to set
         */
        public boolean setNumOfEnemy(int numOfEnemy) {
            if (numOfEnemy > 3) {
                this.numOfEnemy = numOfEnemy;
                return true;
            }
            return false;
        }

        /**
         * @return the colorPlayer
         */
        public Color getColorPlayer() {
            return colorPlayer;
        }

        /**
         * @param colorPlayer the colorPlayer to set
         */
        public void setColorPlayer(Color colorPlayer) {
            this.colorPlayer = colorPlayer;
        }

        /**
         * @return the size
         */
        public int getSize() {
            return size;
        }

        /**
         * @param size the size to set
         */
        public boolean setSize(int size) {
            if (size <= 25 || size >= 15) {
                this.size = size;
                return true;
            }
            return false;
        }

    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);

        if (odpocet) {
            Font font = new Font(Font.SANS_SERIF, Font.BOLD, 30);
            g.setFont(font);
            g.setColor(Color.RED);
            g.drawString("Kolize zapnuty", (getWidth() - g.getFontMetrics(font).stringWidth(String.valueOf("Kolize zapnuty"))) / 2, getHeight() / 2);
        } else if (isRunning && !odpocet) {
            pl.render(g);

            for (Enemy e : en) {
                e.render(g);
            }
        } else if (!isRunning) {
            Font font = new Font(Font.SANS_SERIF, Font.BOLD, 25);

            String gm = "GAME OVER";
            int fm1 = g.getFontMetrics(font).stringWidth(gm);
            int fm2 = g.getFontMetrics(font).stringWidth(cas);

            g.setColor(Color.BLUE);
            g.setFont(font);
            g.drawString(gm, (getWidth() - fm1) / 2, getHeight() / 3);
            g.setColor(Color.BLUE);
            g.drawString(cas, (getWidth() - fm2) / 2, getHeight() / 2);

            jb.setVisible(true);
            jb.setLocation((getWidth() - jb.getWidth()) / 2, ((getHeight() - jb.getHeight()) * 2) / 3);

            playAgain.setLocation((getWidth() - playAgain.getWidth()) / 2, ((getHeight() - playAgain.getHeight()) * 3) / 4);
            playAgain.setVisible(true);
        }
    }

    public boolean checkColision() {
        boolean kolize = false;
        for (Enemy en1 : en) {
            kolize = pl.getBounds().intersects(en1.getBounds());
            if (kolize) {
                return true;
            }
        }
        return kolize;
    }

}

_____________­_______________
Třída s metodou main, která nastavuje JFrame


/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package gui2;

import javax.swing.JFrame;

public class Gui2 {

    public Gui2() {
        JFrame jf = new JFrame("Hra");
        jf.setDefaultCloseOperation(3);
        jf.setVisible(true);

        Menu menu = new Menu(jf);
        jf.add(menu);
        jf.pack();
    }

    public static void main(String[] args) {
        new Gui2();
    }

}

Děkuji Vám moc za rady

 
Odpovědět
28.12.2021 22:13
Avatar
Atrament
Supertvůrce
Avatar
Atrament:29.12.2021 14:31

Tak předně gratulace k projektíku dotaženému do zdárného konce. Hra funguje a to je to co se počítá ;) Sám nejsem herní vývojář, takže těm věcem kolem renderování a fps a game loopu tak úplně nerozumím, ale pár připomínek jako Java programátor se zkušenostmi se Swingem bych měl.

Nemáš správně ten threading. Už na začátku děláš chybu v tomhle:

JFrame jf = new JFrame("Hra");
jf.setDefaultCloseOperation(3);
jf.setVisible(true);

Takhle ti to gui neběží na tom 'správném' vlákně. Je to častá a velmi záludná chyba, která se nemusí projevit při stovce spuštění, ale pak se o to výrazněji projeví při tom stoprvním :) Takže správně je vždy:

SwingUtilites.invokeLater(() -> {
    JFrame jf = new JFrame("Hra");
    jf.setDefaultCloseOperation(3);
    jf.setVisible(true);
}

Dále tam potom vytváříš nějaká vlákna pomocí Thread(), to taky není dobré ve Swing aplikaci, použil bych místo toho spíš SwingWorker. Teď ti to funguje ok, ale kdybys třeba chtěl v budoucnu to herní okno obohatit o nějaké JLabely, které by ukazovaly třeba aktuální skore, nebo tak něco, tak bys velice rychle narazil, při pokusu o jejich update... Ano práce s vlákny ve Swingu je prostě hrozná :)

Zapracuj na pojmenování proměnných a tříd. Třída pojmenovaná NewClass, to raději přejdu bez dalšího komentáře :) Také jména proměnných jako c nebo jp pl - neboj se toho a nazvi je pořádně klidně nějakým dlouhatánským názvem, ale tak aby na první pohled bylo jasné co to je. Zdrojový kód daleko víc čteš než píšeš a s tím psaním ti pomůže ide. Zato s pochopením vlastního zdrojáku po pár měsících ti pomůžou jenom správné názvy proměnných.

BufferedWriter bw = new BufferedWriter(new FileWriter("stat.txt", true));
String s = JOptionPane.showInputDialog("Zadej své jméno: ");

String text = "";
for (char a : s.toCharArray()) {
    write = true;
    if ((' ') == a) {
        text += "_";
    } else {
        text += a;
    }
}
bw.write(text);
bw.write(";");
bw.write(cas);
bw.newLine();
bw.flush();
bw.close();

zastaralý a zbytečně ukecaný způsob jak získat od uživatele jméno, nahradit případné mezery podtržítkem a zapsat do souboru. Zkus to takhle moderněji:

Path path = Path.of("stat.txt");
String jmeno = JOptionPane.showInputDialog("Zadej své jméno: ").replace(' ', '_');
String line = jmeno + ";" + cas;
Files.writeString(path, line + System.lineSeparator(), StandardOpenOption.CREATE, StandardOpenOption.APPEND);

To umístění souboru stat.txt taky není šťastné. Teď ho máš tam, kde je aplikace a chápu že to tak teďka stačí. Ale představ si, že tu hru budeš chtít nějak distribuovat, použiješ nějaký nástroj na vytvoření klasického instalátoru, který ti tu hru nainstaluje na Windows někam do Program Files. V tu ránu ti ten zápis do souboru nebude fungovat, protože normální uživatel, který si tu hru spustí tam nemá práva zápisu.

To je momentálně z mé strany prozatím asi tak všechno. Vzato kolem a kolem dobrá práce!

 
Nahoru Odpovědět
29.12.2021 14:31
Avatar
qw3rtz
Člen
Avatar
Odpovídá na Atrament
qw3rtz:29.12.2021 17:48

Moc děkuji za zhodnocení
Jen bych měl dotaz k tomu lambda výrazu, k čemu vlastně slouží 😄 ("SwingUtilites­.invokeLater(() ..."), zkusil jsem se podívat do dokumentace, ale nevím jestli jsem to úplně pochopil. Vytvoří vlákno pro ten JFrame, a slouží k lepší práci s JComponentama, kvůli případné aktualizaci toho GUI ? Nebo jaký je mezi tímto a klasickým vláknem rozdíl?😀
Děkuji

 
Nahoru Odpovědět
29.12.2021 17:48
Avatar
Atrament
Supertvůrce
Avatar
Odpovídá na qw3rtz
Atrament:29.12.2021 18:04

Jde o to že, Swing není tzv. thread safe, takže pokud máš v aplikaci více vláken a pokusíš se z některého z nich přistoupit k některé Swingové komponentě, může ale nemusí to skončit nějakou výjimkou. Všechno nastavování čehokoliv ve Swingu musí být správně prováděno na tzv. EDT vlákně, jedině tehdy je to bezpečné. K tomu slouží ta metoda invokeLater() - provede setVisible() na EDT vlákně.

Jak říkám dá se nad tím mávnout rukou, protože obzvláště v jednoduchých příkladech to funguje i tak, ale je to cesta do pekel, obvykle se to jako chyba projeví v tom nejméně vhodném okamžiku.
Více o tom https://docs.oracle.com/…y/index.html

 
Nahoru Odpovědět
29.12.2021 18:04
Avatar
Lubor Pešek
Člen
Avatar
Lubor Pešek:3. ledna 21:39

S tím, co píše Atrament nejde víc, než souhlasit. Jen ještě něco doplním. Kromě toho šíleného pojmenování (Na kterém jde vidět vývoj začátečníka - nejdřív sis s tím dal práci, dokonce i balíček jsi vytvořil:), ale pak už jsi se snažil, aby to nějak fungovalo a to, co jsi našel na netu, tak jsi hodil do nějaké třídy a už to šlapalo :D Dost mi to někoho připomíná (mluvím o sobě:D) ).
Tak je tam úsměvná ta "čeglish". Držíš se správné konvence - pojmenovávat anglicky, ale třeba jeden boolean máš pojmenovaný "kolize".

  • Určitě bych chtěl ocenit ty komentáře. Snaha tu je, i když komentář by ti měl něco napovědět (třeba jak metodu používat, varovat před špatným použitím či říct, jaké vstupy očekáváš, pokud to není patrné z kódu). Komentář typu:
/**
         * @return the colorPlayer
         */
        public Color getColorPlayer() {
            return colorPlayer;
        }

Je na prd. Co vlastně vidíš? Mám tu metodu, u které vidím, že mi vrací barvu a jmenuje se getColorPlayer. No a ty mi napíšeš v komentáři, že vrací colorPlayer.
To má pro mě nulovou hodnotu takový komentář. Ale dal bych ti zeleného bludišťáka za snahu. Aspoň jde už co napravovat -> takže na to se můžeš také zaměřit, jak psát dobré komentáře;)

  • Další věc (ale tu nemůžeš asi ještě vědět), je ta třída Entity. Až budeš pokročilejší, tak zjistíš, že entita je v programátorském světě už oficiálně brána jako třída databáze. V podstatě když řekneš entita, tak každý zkušený programátor si okamžitě vybaví, že to bude třída obsahující pouze proměnné, které budou mít stejný název, jako hlavička tabulky v databázi, že tam budou pouze přístupové metody (nebo Lombok) a uvidí tam pouze anotace, které mi dokáží prozradit strukturu tabulky v databázi.

Takže to ber jako info pro tebe - Entita není dobrý název z tohoto hlediska.

  • Jinak určitě oceňuji použití abstraktní třídy a hlavně, že ji i využíváš. To není u začátečníků obvyklé a není to špatné.
  • Dochází k logické chybě u té aplikace. Když si aplikaci spustíš a nic si nezahraješ a zvolíš možnost "Statistiky", tak ti sice vyskočí správná hláška, ale pak program pokračuje dál, jako kdyby se nic nestalo a navíc zmizí možnost spuštění hry.

To je hodně kritický nedostatek a měl bys jej opravit. Respektive nezmizí, ale ten seznam tuto možnost překryje. Tabulátorem se dá na ni překliknout, ale málo koho to napadne.

  • Další věc, co bys mohl vylepšit, tak veškerou konfiguraci a texty vytáhnout z kódu. Nepsat to tam natvrdo, ale vytvářet si třeba .properties soubory (ještě lepší v dnešní době .yaml soubory), které ti zajistí konfiguraci. Jeden si bude pamatovat názvy (tím bys měl i jednoduchou přípravu, kdybys chtěl třeba zavádět více jazyků do hry) no a druhý konfigurák na barvy, rozměry, atd.

Potom nějaký osobní setting, který by se mohl měnit a který by si ukládal aktuální nastavení uživatele.

  • Dost šikovné je, když se ti aplikace spustí uprostřed obrazovky.

Toho můžeš docílit dvěma způsoby. Buďto přes Toolkit a Dimension balíčky:

private final Toolkit tool = Toolkit.getDefaultToolkit();
private final Dimension src = tool.getScreenSize();

...
//v metodě či konstruktoru
JFrame frame = new JFrame();
frame.setSize(800, 800); //i tyto údaje bys pak tahal z osobního configu
frame.setLocation(src.width / 2 - frame.getWidth() / 2, src.height / 2 - frame.getHeight() / 2);
...

No a nebo úplně jednoduše:

JFrame frame = new JFrame();
frame.setSize(800, 800); //i tyto údaje bys pak tahal z osobního configu
frame.setLocationRelativeTo(null); // <- pokud tuhle metodu zavoláš na null, tak to zarovná v rámci plochy na střed
  • Myslím, že jsi špatně pochopil dědičnost. Konkrétně tvoje třída Menu, která dědí z JPanelu, tak jej nijak nerozšiřuje jak by měla. Třeba se tu objevuje metoda visibleNastav (opět šílený čeglish, ze kterého bych tedy ani omylem nepoznal, že "nastav" je název proměnné pro tlačítko -.-)

Trošku to souvisí s tou logikou celé aplikace.
Ke každé aplikaci (ať už hře, webu či čemukoliv) bys měl přistupovat s tím, že by se v aplikaci měl vždy objevit nějaký "manager". Třída, která bude vše řídit, spracovávat, uchovávat.
V podstatě i ve webových technologiích, tak to je hlavní smysl Javy. Java je rize mozek všeho a pak už jen provolává ty zbytky.
Musíš mít roztřízené v projektu: Logiku, Komponenty, práci se soubory, distribuci - (tu dělíš na frontend,případně desktop nebo mobil). Pak máš další vytřídění pro databázi atd. atd. atd.
Ty v podstatě tu dědičnost používáš stylem: Hele já si teď vezmu třídu člověk, jemu dám baťoh, k tomu přidělám dva plechy, k tomu přišrubuji dvě tyče a mám robota.

Ano, funguje to, ale smysl dědičnosti je rozšiřovat stávající funkcionalitu. To, že to takto jde neznamená, že je to správně. Až příliš jsi to konkretizoval a teď si představ, že ze tvé nové třídy (Menu) by měl někdo dědit.

Konkrétně tvoje metoda setSettings. Má pouze tři parametry, které se ti zrovna teď hodí. Ale co když bych chtěl třídu menu použít jinde a budu pracovat třeba se soubory a potřeboval bych nastavit umístění souboru? To mi tvoje metoda setSettings bude na prd a nikdy ji nevyužiju.

  • No a poslední věc, co jsem si zkoušel, tak nemáš to moc objektové. Ano, máš sice dost tříd, ale zkusil jsem si jen tak z hecu vytvořit třeba jinou třídu Framu a jen tak z legrace si zkusil vytvořit novou třídu hráče. Ano, podařilo se mi to vyrendrovat, ale byl jsem přinucen v podstatě naprogramovat novou třídu "NewClass", protože to máš až moc závislé jeden na druhém.

Možná trošku špatně chápeš smysl OOP.
Mám na to hezký příklad. Když si vezmeš dva objekty - židli a zem, tak ty musíš programovat nezávisle každý z objektů.
Země by neměla propustit ŽÁDNÝ objekt (ani židli, ani letadlo ani barák). Prostě je tu nějaký povrch a tím nemá nikdo co projít. Ano, pak máš třeba třídu krtek a ta se ti do povrchu zavrtá a tudíž jim projde :D ale to je už další rozvoj třídy Země :)
Podstata je, že třída Země bude programována samostatně a pokud třída židle bude chybět, nic se nestane.
To samé třída Židle. Ta má samozřejmě svoji grafickou podobu, ale ať ji položíš na cokoliv, tak ona se nepropadne a bude hledat pevný povrh.
Takže musí platit obojí pravidlo. Třída Země nepropustí žádný objekt (ani židli) a třída Židle neprojde žádným povrchem (ani Zemí).
Oba dva objekty jsou nezávislé na sobě.

Tvá třída Player je až moc svázaná se třídou NewClass. A to je špatně.
Samozřejmě logika, jak bude třída NewClass se třídou Player pracovat, tak ta už musí být hodně konkrétní, ale tady se bavíme o blbém zobrazení.
Ty nejsi schopen třídu Player jednoduše zobrazit. Přitom třída Player (i když docela složitě) reprezentuje objekt panelu, kterým nějak posouváš.

Prostě ten logický koncept je docela zmatečný a měl bys nad tím postupovat trošku víc racionálněji.
V podstatě když bych to převedl do skutečného světa, tak ty, kdybys takovouto hru měl vytvářet ve skutečnosti, tak ty místo toho, abys vytvořil nový objekt, který bude bude představovat čtvereček, se kterým bude uživatel pohybovat, tak ty vytvoříš ovládač, který bude uživatel ovládat a tento ovládač bude posunovat nějakou deskou, na kterou se obvykle dávají jiné věci.

Když bych to řekl jednoduše - zkus se na celou aplikaci dívat z pohledu nějakého řízení.
Zkus si to předělat na tuto logiku:
Mám třídu, která mě bude dělat managera a všechno řídit. Bude se starat o to, že se nějak spustí hra (ale nebude vytvářet spouštění!! Ona to jen provolá, když bude potřeba). Bude vyhodnocovat výsledky (ale opět nebude nic vypočítávat, jen v pravý čas zavolá, kdy bude potřeba něco vyhodnotit). To samé zobrazení atd.

Prostě typický manager :D Umí hovno, ale je velmi důležitý, protože všechno dokáže zařídit tak, aby to fungovalo.
No a potom k tomu přiděláš potřebné třídy. Třidu, která POUZE vykreslí hráče a nic víc. Tato třída se bude zabývat pouze tím, co se vztahuje k samotnému hráči (barvu, pozici, posun, případně bude vracet informace o tom všem. A to je vše).
To samotné nějaký timer. Ten si bude uchovávat čas a nic víc (teoreticky na to bys ani nepotřeboval třídu, ale dobrou soukromou metodu v managerovi - že by měl něco jako vlastní stopky).
To samé hlavní okno. Manager ho dokáže vytvořit, manager ho nastaví a on bude s oknem pracovat, až když se něco stane. A manager ho vyhodnotí.
Okno sice bude mít nějaká tlačítka, ale co budou tlačítka vykonávat, tak pokud půjde o nějakou logiku, tak to vyhodnotí manager, pokud půjde o nějakou grafiku, tak to si okno zpracuje samo.
Manager také bude řešit, jestli hra skončila, v jakém jede vlákně, atd. atd. atd.

Tohle je způsob, kterým bys měl uvažovat - dělit program do logických oddílů a pokud možno takových, aby bylo jasné, co který oddíl bude řešit. Tento bude řešit logiku hry, tento grafiku, tento práci se soubory, atd.

Schválně - jak by ti to dlouho trvalo, když bys třeba teď chtěl přidat druhého hráče?, který by třeba hrál stylem - náhodné pohyby (takovou primitivní umělou inteligenci). Kam bys takového hráče přidal?
Nebo třeba další věc - kdybys chtěl rozšířit Menu možnosti. Co vše bys musel dodržet a jak dlouho by ti v kódu trvalo, než najdeš, co vše je se vším spjato?

No a samozřejmě v projektu chybí unit testy:) + spousta věcí by se syntakticky dala zapsat lépe. Na to ti doporučím plugin Sonarlint (v podstatě když se ti podaří správně nastavit, tak ti to bude kontrolovat kód a bude ti říkat, co syntakticky napsat jinak).
Kupříkladu tě sonarlint bude upozorňovat na to, že máš v konstruktoru 7 parametrů a kód se stává nečitelným. Nebo naopak moc zanořených ifů a cyklů v sobě. Nebo nepoužité metody, atd. Je to užitečná věc, která tě naučí psát čisté kódy. Někdy je úprava pomocí sonaru lehká, někdy to vyžaduje uvažovat nad změnou celé logiky.

Každopádně dobrá práce. Projevils snahu, máš výsledek a tvůj topic jen dokazuje, že se chceš zlepšovat;) Tento přístup si zachovej, vzdělávej se dál a dál a budeme mít o jednoho velmi šikovného programátora navíc, což je bezva!!

Nahoru Odpovědět
3. ledna 21:39
Existují dva způsoby, jak vyřešit problém. Za prvé vyhoďte počítač z okna. Za druhé vyhoďte okna z počítače.
Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!
Avatar
qw3rtz
Člen
Avatar
Odpovídá na Lubor Pešek
qw3rtz:5. ledna 21:16

Děkuji Vám moc za odpověď. 🙂
Asi o víkendu zkusím zapracovat na tich chybách a přeprogramuju to, až bych to měl, tak bych to sem zkusil ještě dát ?
Jen bych měl ještě dotaz 😀
,,Konkrétně tvoje třída Menu, která dědí z JPanelu, tak jej nijak nerozšiřuje jak by měla." - takže místo dědičnosti bych si měl v té třídě udělat instanci JPanelu a pracovat s ní ? Tím rozšířením se myslí třeba Overide nějaké metody ?

 
Nahoru Odpovědět
5. ledna 21:16
Avatar
Lubor Pešek
Člen
Avatar
Odpovídá na qw3rtz
Lubor Pešek:5. ledna 22:26

Jo, rozšířit třídu znamená zdědit a přidat nové metody, ale podstata předka by měla zůstat. Override znamená přepsání metody, takže bys jen změnil stávající funkcionalitu.
Ono celkově to dědění je velmi nebezpečná věc. Implementace je velice jednoduchá, ale je potřeba vždy zvážit, jestli je potřeba dědit (čili rozšířit) z dané třídy. To je právě na programátorovi, aby to posoudil. Pokud to jen trošku půjde, tak je dobré se dědičnosti vyhnout. Už jen z toho principu, že dědit v Javě můžeš pouze jednou a tak bys to měl šetřit opravdu do takových případů, kdy to bude nevyhnutelné.

Konkrétně v tomto případě, co jsi udělal ty, tak jsi vůbec nepotřeboval žádné vlastnosti toho JPanelu. V podstatě jsi to využil pouze jako komponentu, na kterou jsi navrkal další tlačítka.
Takže jsi mohl přímo použít prázdnou JComponent třídu a na ní si to všechno naházet. No a nebo není vůbec potřeba vytvářet na to speciální třídu, ale pokud jde o 4 tlačítka, tak jsi to mohl v rámci jedné metody přihodit přímo v hlavní třídě.

No a kdyby jsi chtěl speciální třídu na to, tak právě kdybys měl nějakého managera, který by to zpracovával, tak bys mohl vytvořit obyčejnou třídu a přes toho managera by sis vytáhl componentu (třeba nějaký hlavní JPanel), na který bys to nasáčkoval. No a tím pádem bys vůbec nepotřeboval žádného potomka swing component, ale v metodě dané třídy by sis hezky vytvořil tlačítka a pak bys je jednoduše přidal na JPanel, který dostaneš v rámci nějaké metody (ať už statické či singletona) z managera.

Těch způsobů je samozřejmě moc a neexistuje žádný mustr. Ty musíš vždy uvažovat tak, abys pokud možno programoval co nejobecněji a taková jednoduchá kontrola, jestli programuješ objektově, tak je v podstatě v tom, že si to, co naprogramuješ, vyhodíš z projektu. No a teď se ptáš sám sebe - funguje to samostatně nebo je to hodně provázané s projektem? Pokud je to provázané, je to chyba, protože případná editace bude v budoucnu hodně složitá.

To samé i naopak. Pokud mám nějaký panel, na který vkládám komponenty, tak se sám sebe ptej - můžu tam přidat jakoukoliv komponentu a nebo to musím specifikovat?
Krásným případem tohoto jsou třeba piškvorky. Když si uděláš nějakou herní plochu, tak se pak zeptej sám sebe: Tak, teď tam mám křížek a kolečko. Jak by bylo obtížné tam implementovat, abych tam přidával nějaké jiné tvary?
A další věc - když bych tam chtěl přidávat nové tvary, tak jak by bylo problematické, aby se správně vykreslovaly? A šlo by, aby tvary byly libovolné, nebo musí být přesně specifické? (kupříkladu dost častá chyba, kdy samotný tvar ať už křížek či kolečko je vypočítáváno na základě kliknutí na plochu). Atd. atd. atd.

PS: Toto je dost častý problém, který programátor řeší. Jak zobecnit a zpřehlednit kód. Jak udělat strukturu celého programu, jak vytvořit logické celky a dávat to do správných balíků.

Třeba konkrétně u tebe - dej si tu práci a sám si řekni, kolik času by ti zabralo, abys čtverečky přeměnil na kolečka. Potom aby to byl třeba složitější tvar (třeba dvě kolečka v sobě, nebo kolečko s hvězdičkou uprostřed).
Určitě toho, čeho bys chtěl docílit, tak by mělo vypadat tak, že podobu samotného objektu, kterým budeš pohybovat, tak aby se programovalo naprosto nezávisle na ostatním kódu. V podstatě aby samotný hráč byla komponenta (klidně prázdný nebo neviditelný patvar), který bude vždy fungovat stejně. No a to, jak si to vykreslíš, nesmí hrát roli.

To samé další věc - jak by ti dlouho trvalo, aby hráč měl nějakou podobu a "Enemy" úplně jinou?
Jak dlouho by trvalo, kdybys chtěl do hry přidat další elementy (kupříkladu, aby menu bylo hezky celou dobu nahoře v okně, jak to u aplikací bývá? Kdybys tam chtěl třeba přidat nějakou překážku. Odraz kostky je pevně svázán jenom s okrajem plochy? Nebo se odrazí od čehokoliv? (třeba nepřítel od nepřítele) atd.

Je to všechno možné rozšíření, ale na takovýchto věcech nejlépe poznáš a nejlépe se budeš učit, jak programovat.

Kdysi dávno tu byla diskuze ohledně nějaké nové Javy. Nevím, kolikátka to byla, jestli 8 nebo 9. Šlo o to, že se borec ptal na nějaké info ohledně přechodu na novou verzi Javy. Tak jeden "profesionál" mu odvětil, že nechápe, proč z toho dělá takovou vědu, když novější Java zvládá bez problémů vždy i nový kód. Na to dostal dotyčný krásnou reakci:
"Je vidět, že jsi nikdy nic nevyvíjel a nepracoval s frameworkem. Není nic příjemného přecházet na novou verzi u frameworků, které třeba jinak využívají různé technologie atd."
A popravdě o tom je celý i vývoj. Nejlepší, co můžeš udělat je, že začneš projektík. Velice jednoduchý - prostě se spustí, něco udělá a ukončí se. No a pak si budeš dávat vlastní tasky, jak to budeš měnit, vylepšovat, přidělávat a předělávat. Třeba i blbá změna barviček. Změna textů všude. Změna fontů atd.

Tohle si někdy zkus. Prvně to dělej ručně. A třeba už jen blbá změna fontů tě donutí k tomu, že než abys všechno měnil furt ručně a prohledával, kde všude máš ještě měnit textíky, tak si sám řekneš - budu to ukládat v nějaké společné kolekci, v nějakém souboru, prostě někde společně. No a najednou máš novou featuru - funkci, kterou dáš i uživateli - změna fontu v celé aplikaci.
To samé třeba lokalizace do jiných jazyků. Najednou ti zmizí všechny texty aplikace z kódu, ale budeš to mít hezky v nějakém souboru. A potom budeš rád, že změna lokalizace na jiný jazyk pro tebe bude znamenat pouze změna jednoho souboru za druhý.

Atd. atd. atd. Jestli máš čas, chuť a zápal, tak jdi tímto směrem. Začni si mini projektík a vylepšuj ho. Sám dojdeš postupně na to, co bude pro tebe lepší a budeš hledat chytřejší řešení. O tom celé programování je. No a pak začneš používat modernější technologie (opět příklad - začneš s properties soubory a pak přejdeš na XML, pak na JSON a pak na yaml. A nebo některý z nich přeskočíš, ale vyzkoušej si to, to ti radím, ať se seznámíš s výhody i nevýhody).

Krásný příklad takového vývoje je třeba obyčejná kalkulačka. Začni konzolově - že jen vypíšeš součty. Pak to přesuneš do metod. Pak uděláš projekt, ve kterém výpočty a logika budou v jenom balíku a zobrazování v dalších balících (a jakékoliv zobrazování bude používat právě tuto již naprogramovanou logiku).
Kdybys to začal přímo ve swingu, tak asi první, co by tě napadlo, tak že každé tlačítko pro výpočet bude mít pevně spjatou logiku s daným výpočtem (že tlačítko + bude sečítat atd.)
No když ti řeknu - převeď to na FX nebo na web aplikaci, tak můžeš začít prakticky od začátku.
Joooo, když budeš mít početní logiku separátně a nějaká třida ti vždy vrátí výstup podle vstupu, tak to můžeš pak používat v konzoli, ve swingu, v FX, v mobilní aplikaci, webové aplikaci nebo to vracet jako nějakou servisu.

A zase jsme u toho případu s tím "managerem" :) Opět je dobré mít nějaký centrální mozek, přes který to všechno proudí a který si hraje s logikou a pak už ty jednotlivé dílčí nástroje (výpočty, komponenty, různé algoritmy, business logiku, JPA, práci s modely a všechno možné a nemožné), tak rvi do samostatných oddílů.

Nahoru Odpovědět
5. ledna 22:26
Existují dva způsoby, jak vyřešit problém. Za prvé vyhoďte počítač z okna. Za druhé vyhoďte okna z počítače.
Avatar
qw3rtz
Člen
Avatar
qw3rtz:4. února 21:38

Zdravím,
konečně jsem se opět dostal k tomu projektíku a předělal jsem ho. 😀
Víc jsem to rozdělil, zkusil jsem si napsat i JUnit4 testík na herní soubory (zatím tam je teda jen jeden), napsal jsem komentáře a ještě mě napadlo pár dalších věcí, tak to tam ještě potom zkusím dodělat. 🙂
Odkaz (už je tam moc tříd :D): Projekt - odkaz na úschovnu

Snad je to už lepší :D

A ještě bych měl takový vedlejší dotaz, jestli nemá náhodou někdo zkušenost s touto třídou:
VektorováGrafika1
nebo s touto:
VektorováGrafika2
zkoušel/zkouším udělat aplikaci která by bitmapu převedla na vektorovou grafiku, našel jsem si tyto třídy, ale ta vektorizace u tich obrázků není nic moc (mění to barvy a dokresluje si to čáry, občas to rozostří celý obrázek) 😀

Děkuji za rady.

 
Nahoru Odpovědět
4. února 21:38
Avatar
qw3rtz
Člen
Avatar
Odpovídá na qw3rtz
qw3rtz:4. února 21:59

Ještě mě teď napadlo, jak je to s bezpečností (u této aplikace je to asi zbytečné řešit, ale třeba u jiných), používal jsem nakonec .properties soubory (na xml jsem si zatím netroufl :D ), který se dají snadno přepsat, to se pak ukládají do složek, který může přepisovat jen správce nebo jdou nějak zakázat úpravy? :)
Děkuji

 
Nahoru Odpovědět
4. února 21:59
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 9 zpráv z 9.