IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

Tvorba Snake 2D - krok za krokom v Jave

Tento turiál bude pojednávať ako si počínať pri tvorbe hry ako je táto: http://www.itnetwork.cz/...ad-snake-2d/ V podstate sa bude jednať o rovnakú funkčnosť, akurát s tým rozdielom, že to napíšem znova s prehľadnejším kódom.

Vytvorenie základu

Najskôr si nachystáme triedy / objekty ktoré sa budú vyskytovať v hre.

Ako prvý si vytvoríme základný objekt od kterýho ďalej budeme dediť.

package org.fugiczek.snake2d.game;

import java.awt.Color;
import java.awt.Graphics2D;

/**
 * Univerzální herní objekt
 * @author Fugiczek
 * @version 1.1
 */
public class GObject {

    /**
     * x-ová souřadnice
     */
    private int x;
    /**
     * y-ová souřadnice
     */
    private int y;
    /**
     * velikost v pixelech při vykreslení
     */
    private int sizeInPX;
    /**
     * barva objektu (na vykreslení)
     */
    private Color color;

    /**
     * Konstruktor na vytvoření
     * @param x x-ová souřadnice
     * @param y y-ová souřadnice
     * @param sizeInPX velikost v pixelech při vykreslení
     * @param color barva objektu (na vykreslení)
     */
    public GObject(int x, int y, int sizeInPX, Color color){
        setX(x);
        setY(y);
        setSizeInPX(sizeInPX);
        setColor(color);
    }

    /**
     * Vykreslení objektu
     * @param g2 instance třídy Graphics2D na vykreslení
     */
    public void draw(Graphics2D g2){
        g2.setColor(color);
        g2.fillRect(x, y, sizeInPX, sizeInPX);
    }

    /**
     * @return x-ová souřadnice
     */
    public int getX() {
        return x;
    }

    /**
     * @param x nová x-ová souřadnice
     */
    public void setX(int x) {
        this.x = x;
    }

    /**
     * @return y-ová souřadnice
     */
    public int getY() {
        return y;
    }

    /**
     * @param y nová y-ová souřadnice
     */
    public void setY(int y) {
        this.y = y;
    }

    /**
     * @return velikost v PX
     */
    public int getSizeInPX() {
        return sizeInPX;
    }

    /**
     * @param sizeInPX nová velikost v PX
     */
    public void setSizeInPX(int sizeInPX) {
        this.sizeInPX = sizeInPX;
    }

    /**
     * @return barva pro vykreslení
     */
    public Color getColor(){
        return color;
    }

    /**
     * @param color nová barva pro vykreslní
     */
    public void setColor(Color color){
        this.color = color;
    }

}

Tento objekt má všetko čo bude potrebné, ako súradnice x / y a pre vykresľovanie farbu, veľkosť a samotnú metódu na vykreslenie, ostatné objekty budú chcieť len malé úpravy.

Ďalej si napíšeme triedu s objektom, ktorý sa bude náhodne umiestňovať na hernej ploche a za ktorý budeme získavať body.

package org.fugiczek.snake2d.game;

import java.awt.Color;
import java.util.Random;

/**
 * Bonus za kterým se budeme honit ;)
 * @author Fugiczek
 * @version 1.1
 */
public class Bonus extends GObject{

    /**
     * Šířka herního pole v PX
     */
    private final int maxX;
    /**
     * Výška herního pole v PX
     */
    private final int maxY;

    /**
     * Instance třídy Random pro náhodné rozmisťování
     */
    private Random rand;

    /**
     * Konstruktor na vytvoření
     * @param sizeInPX velikost v pixelech při vykreslení
     * @param color barva objektu (na vykreslení)
     * @param maxX Šířka herního pole v PX
     * @param maxY Výška herního pole v PX
     */
    public Bonus(int sizeInPX, Color color, int maxX, int maxY){
        super(0, 0, sizeInPX, color);
        this.maxX = maxX;
        this.maxY = maxY;
        rand = new Random();
    }

    /**
     * Umístí bonus na náhodnou pozici
     */
    public void locateBonus(){
        int tmp;
        tmp=rand.nextInt(maxX/getSizeInPX());
        setX(tmp*getSizeInPX());
        tmp=rand.nextInt(maxY/getSizeInPX());
        setY(tmp*getSizeInPX());
    }

}

Tu sme pridali navyše len premenné MAXX a MAXY pre generovanie novej pozície aby sa ponus nevygeneroval mimo hracie pole, diele inštancie triedy Random rand pre náhodné umiestnenie a metóda locateBonus pre náhodné umiestnenie.

Ďalej budeme potrebovať samotného hada, najskôr si urobíme Výučbový typ smeru aby sme vedeli akým smerom sa had bude pohybovať.

package org.fugiczek.snake2d.game;

public enum Direction {
    UP, DOWN, LEFT, RIGHT
}

A teraz samotná trieda s hadom.

package org.fugiczek.snake2d.game;

import java.awt.Color;
import java.awt.Graphics2D;
import java.util.ArrayList;
import java.util.List;

/**
 * Had se všemi jeho vlastnostmi
 * @author Fugiczek
 * @version 1.1
 */
public class Snake extends GObject{

    /**
     * List s hadovými částmi těla
     */
    private List<GObject> body;
    /**
     * Barva těla hada
     */
    private Color colorBody;
    /**
     * Směr kudy had jde
     */
    private Direction direct;

    /**
     * Konstruktor na vytvoření
     * @param x x-ová souřadnice
     * @param y y-ová souřadnice
     * @param sizeInPX velikost v pixelech při vykreslení
     * @param color barva hlavy (na vykreslení)
     * @param colorBody barva těla (na vykreslení)
     */
    public Snake(int x, int y, int sizeInPX, Color color, Color colorBody) {
        super(x, y, sizeInPX, color);
        body = new ArrayList<>();
        setColorBody(colorBody);
        setDirect(Direction.DOWN);
    }

    /**
     * Přepsaná třída na vykreslení, vykresluje hlavu a tělo dohromady
     */
    @Override
    public void draw(Graphics2D g2){
        g2.setColor(getColor());
        g2.fillRect(getX(), getY(), getSizeInPX(), getSizeInPX());

        for(GObject ob : body){
            ob.draw(g2);
        }
    }

    /**
     * @return barva těla
     */
    public Color getColorBody() {
        return colorBody;
    }

    /**
     * @param colorBody nová barva těla
     */
    public void setColorBody(Color colorBody) {
        this.colorBody = colorBody;
    }

    /**
     * @return směr hada
     */
    public Direction getDirect() {
        return direct;
    }

    /**
     * @param direct nový směr hada
     */
    public void setDirect(Direction direct) {
        this.direct = direct;
    }

    /**
     * Rozšíří tělo hada o jednu novou část, dá ji na místo hlavy a hlavu posune
     */
    public void expandBody(){
        body.add(0, new GObject(getX(), getY(), getSizeInPX(), getColorBody()));
        moveHead();
    }

    /**
     * @return list s tělem
     */
    public List<GObject> getBody(){
        return body;
    }

    /**
     * Pohne s celým tělem včetně hlavy
     */
    public void move(){
        moveBody(); // první musí být pohyb těla protože vychází ze souřadnic hlavy
        moveHead();
    }

    /**
     * Pohyb hlavy v závislosti na směru
     */
    private void moveHead(){
        switch(getDirect()){
        case LEFT:
            setX(getX()-getSizeInPX());
            break;
        case RIGHT:
            setX(getX()+getSizeInPX());
            break;
        case UP:
            setY(getY()-getSizeInPX());
            break;
        case DOWN:
            setY(getY()+getSizeInPX());
            break;
        }
    }

    /**
     * Posouvá tělem na základě souřadnic minulé části
     */
    private void moveBody(){
        int tmpX=getX(), tmpY=getY(), tmp; // pomocné proměnné

        for(GObject obj : body){
            tmp = obj.getX();
            obj.setX(tmpX);
            tmpX = tmp;
            tmp = obj.getY();
            obj.setY(tmpY);
            tmpY = tmp;
        }

    }
}

Tak tu nám toho pribudlo o niečo viac. Potrebujeme list body s časťami tela, farbu tela colorBody aby sme odlíšili farbu hlavy od tela a smer direct aby sme vedeli ktorým smerom had pôjde. Museli sme prepísať metódu na vykreslenie aby sa nám vykreslila hlava is telom. Keby sme ju nepomýlili, vykreslila by sa len hlava. Pribudli metódy na získavanie premenných, metóda na rozšírenie tela, ktorá sa bude volať keď zoberte bonusy (nová časť sa dá na pozíciu hlavy a hlava sa posunie) a samozrejme pribudli metódy na pohyb.

No herné objekty by sme mali mať, teraz si napíšeme nejakú triedu statickú ktorá nám bude kontrolovať kolízie.

package org.fugiczek.snake2d.utilities;

import org.fugiczek.snake2d.game.Bonus;
import org.fugiczek.snake2d.game.GObject;
import org.fugiczek.snake2d.game.Snake;

/**
 * Třída obsahuje metody na kontroly různých kolizí.
 * @author Fugiczek
 * @version 1.1
 */
public class Collisions {

    private Collisions(){} //nepotřebujeme aby se vytvářeli instance, proto privátní konstruktor

    /**
     * Metoda zjišťuje jestli had nezajel mimo hrací pole, nebo nenarazil sám do sebe.
     * @param snake instance třídy Snake
     * @param maxX Maximální šířka hracího pole
     * @param maxY Maximální výška hracího pole
     * @return Vrací true jestli se souřadnice jeho hlavy rovnají nějakým souřadnicím jeho těla.
     * Dále vrací true jestli souřadnice jeho hlavy jsou mimo herní pole. Pokud se žádná z těchto
     * podmínek nepotvrdí, vrací false.
     */
    public static boolean checkCollision(Snake snake, int maxX, int maxY){
        for(GObject obj : snake.getBody()){
            if((snake.getX()==obj.getX()) && (snake.getY()==obj.getY())){
                return true;
            }
        }

        if(snake.getX()<0){
            return true;
        }
        if(snake.getX()>=maxX){
            return true;
        }
        if(snake.getY()<0){
            return true;
        }
        if(snake.getY()>=maxY){
            return true;
        }

        return false;
    }

    /**
     * Kontroluje zda had najel na bonus
     * @param snake Objekt hada
     * @param bonus Objekt bonusu
     * @return Vrací true, když se souřadnice hlavy hada a bonusu rovnají. Vrací false, když ne.
     */
    public static boolean checkBonus(Snake snake, Bonus bonus){
        if(snake.getX()==bonus.getX() && snake.getY()==bonus.getY()){
            return true;
        }
        else{
            return false;
        }
    }

}

Na tom nie je snáď čo komentovať, jedna metóda kontroluje či sme neprehrali, teda či sme nezašiel mimo herné pole alebo nenarazili sami na seba a druhá kontroluje či sme zobrali bonus.

Toto by bolo z prípravy snáď všetko. Teraz sa vrhneme na grafické spracovanie.

Grafické spracovanie

Najskôr si urobíme triedu s hlavným oknom, na ktorý sa potom dá herná plocha.

package org.fugiczek.snake2d.gui;

import java.awt.Dimension;
import javax.swing.JFrame;

/**
 * Třída, která přidává JPanel s hlavní části této hry a nastavuje základní vlastnosti okna.
 * @author Fugiczek
 * @version 1.1
 */
public class MainBoard extends JFrame{

    private static final long serialVersionUID = 7959263521913348215L;

    public MainBoard(String title, int width, int height){
        setTitle(title);
        setSize(new Dimension(width+3,height+3));
        setLocationRelativeTo(null);
        setResizable(false);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setVisible(true);
        createBufferStrategy(2);
        add(new GameBoard(width, height, getBufferStrategy()));
    }

}

A potom si vytvoríme akoby plátno s hernou plochou.

package org.fugiczek.snake2d.gui;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.image.BufferStrategy;

import javax.swing.JPanel;

import org.fugiczek.snake2d.game.Bonus;
import org.fugiczek.snake2d.game.Direction;
import org.fugiczek.snake2d.game.Snake;
import org.fugiczek.snake2d.utilities.Collisions;

/**
 *
 * @author Fugiczek
 * @version 1.1
 */
public class GameBoard extends JPanel implements Runnable{

    private static final long serialVersionUID = 7806414151208424260L;

    /**
     * Šířka herního pole
     */
    private final int WIDTH;
    /**
     * Výška herního pole
     */
    private final int HEIGHT;
    /**
     * Instance třídy Snake
     */
    private Snake snake;
    /**
     * Instance třídy Bonus
     */
    private Bonus bonus;
    /**
     * Informace zda jsme ještě ve hře, nebo jsme prohráli
     */
    private boolean inGame;

    /**
     * instance třídy BufferStrategy na vykreslování s metodou double-buffer
     */
    private BufferStrategy bs;

    /**
     * FPS (1000/FRAME_DELAY), ovlivňuje rychlost hry
     */
    private final int FRAME_DELAY = 100;
    /**
     * Jak dlouho běžel jeden cyklus, pomocná proměnná při synchronizaci FPS
     */
    private long cycleTime;

    /**
     *
     * @param width šířka herního pole
     * @param height výška herního pole
     * @param bs instance třídy bufferstrategy na vykreslování
     */
    public GameBoard(int width, int height, BufferStrategy bs){
        addKeyListener(new TAdapter());
        setFocusable(true);
        setIgnoreRepaint(true);
        WIDTH = width;
        HEIGHT = height;
        this.bs = bs;

        gameInit();
    }

    /**
     * Nastavení hry a její zapnutí
     */
    private void gameInit(){
        inGame = true;
        snake = new Snake(50, 50, 10, Color.GREEN, Color.GRAY);
        bonus = new Bonus(10, Color.YELLOW, WIDTH, HEIGHT);
        bonus.locateBonus();

        Thread animace = new Thread(this, "Game");
        animace.start();
    }


    /**
     * Hlavní smyčka, kde probíhá furt dokola aktualizace herní logiky, překreslování a synchronizace FPS
     */
    @Override
    public void run() {

        cycleTime = System.currentTimeMillis();

        while(inGame){

            updateLogic();

            updateGui();

            synchFrameRate();

        }

        gameOver();

    }

    /**
     * Synchronizace FPS
     */
    private void synchFrameRate() {
        cycleTime += FRAME_DELAY;
        long difference = cycleTime-System.currentTimeMillis();
        try {
            Thread.sleep(Math.max(0, difference));
        }
        catch(InterruptedException e) {
            e.printStackTrace();
        }
        cycleTime = System.currentTimeMillis();
    }

    /**
     * Vykreslení herní plochy
     */
    private void updateGui() {
        Graphics2D g2 = (Graphics2D)bs.getDrawGraphics();

        g2.setColor(Color.BLACK); //vyčištění
        g2.fillRect(0, 0, WIDTH, HEIGHT);

        snake.draw(g2);
        bonus.draw(g2);

        g2.dispose();

        bs.show();

        Toolkit.getDefaultToolkit().sync();
    }

    /**
     * Kontrola kolizí a posun hada
     */
    private void updateLogic() {
        if(Collisions.checkCollision(snake, WIDTH, HEIGHT)){
            inGame=false;
        }else if(Collisions.checkBonus(snake, bonus)){
            snake.expandBody();
            bonus.locateBonus();
        }else{
            snake.move();
        }
    }

    /**
     * Zobrazení obrazovky s nápisem prohry a se skórem
     */
    private void gameOver(){
        Graphics2D g2 = (Graphics2D)bs.getDrawGraphics();

        String zprava="Prohrál jsi!";
        String skore="Dosáhl jsi skóre: " + snake.getBody().size();
        Font font = new Font("Helvetica", Font.BOLD, 20);
        FontMetrics metr = this.getFontMetrics(font);

        g2.setColor(Color.BLACK); //vyčištění
        g2.fillRect(0, 0, WIDTH, HEIGHT);

        g2.setColor(Color.WHITE);
        g2.setFont(font);
        g2.drawString(zprava, (WIDTH - metr.stringWidth(zprava))/2, (HEIGHT/2)+25);
        g2.drawString(skore, (WIDTH - metr.stringWidth(skore))/2, (HEIGHT/2)-25);

        g2.dispose();

        bs.show();

        Toolkit.getDefaultToolkit().sync();

        try {
            Thread.sleep(2000);
        }
        catch(InterruptedException e) {
            e.printStackTrace();
        }

        System.exit(0);
    }

    /**
     * Soukromá třída která zpracovává zmáčknuté klávesy
     * @author Fugiczek
     * @version 1.1
     */
    private class TAdapter extends KeyAdapter{
        public void keyPressed(KeyEvent e){
            //int hodnota zmáčknuté klávesy
            int key=e.getKeyCode();
            if ((key == KeyEvent.VK_UP || key==KeyEvent.VK_W) && (snake.getDirect()!=Direction.DOWN)) {
                snake.setDirect(Direction.UP);
            }
            if ((key == KeyEvent.VK_RIGHT || key==KeyEvent.VK_D) && (snake.getDirect()!=Direction.LEFT)) {
                snake.setDirect(Direction.RIGHT);
            }
            if ((key == KeyEvent.VK_DOWN || key==KeyEvent.VK_S) && (snake.getDirect()!=Direction.UP)) {
                snake.setDirect(Direction.DOWN);
            }
            if ((key == KeyEvent.VK_LEFT || key==KeyEvent.VK_A) && (snake.getDirect()!=Direction.RIGHT)) {
                snake.setDirect(Direction.LEFT);
            }
        }
    }

}

Pri tvorbe takejto triedy je vhodné najskôr si inicializovať všetky možné ukazovatele / herné objekty. Ďalej si napísať herný slučku, niečo ako táto:

public void run() {

        cycleTime = System.currentTimeMillis();

        while(inGame){

            updateLogic();

            updateGui();

            synchFrameRate();

        }

        gameOver();

    }

A jednotlivo si napísať danej metódy. Keď sa u nejakej zasekne, tak sa vrhneme na ďalšie a neskôr sa k nej vrátime. Potom si môžeme napísať adaptér na spracovanie výstupe z klávesnice / myši.

Spúšťacie trieda

Na dokončenie nám už chýba len spúšťací trieda.

package org.fugiczek.snake2d;

import javax.swing.SwingUtilities;

import org.fugiczek.snake2d.gui.MainBoard;

/**
 * Hlavní spouštěcí třída
 * @author Fugiczek
 * @version 1.1
 */
public class Snake2D {

    public static void main(String[]args){
        SwingUtilities.invokeLater(new Runnable(){
            public void run(){
                new MainBoard("Snake2D v1.1 by Fugizcek", 300, 250);
            }
        });
    }
}

Snáď nie je potreba vysvetľovať : D

Záver

Než začnete písať nejakú hru je dobrý si premyslieť ako tá hra bude fungovať, pretože prepisovanie kódu keď máte už polovicu hotovú nie je moc milá vec. Keď Vám niečo nejde, dajte si pár hodín pauzu, potom pôjde všetko lepšie, preleží sa Vám to hlavou. Až budete mať hru hotovú je dobrý si prejsť časti kódu kde je veľa logiky / výpočtov, možno vás napadnú spôsob lepšieho a úspornejšieho napísanie.

To je odo mňa všetko. Pokiaľ Vám niečo nebude jasné napíšte do komentárov. (neviem či Vám to bude robiť taky ale ak sa Vám neukáže bonus tak je schovaný hore o jedno políčko vieš : D mám upravený vzhľad Windows tak neviem či je to chyba, ak Vám to bude vadiť tak si to správa : D , V Mainboard, 17. riadok -> setSize (new Dimension (width + 3, height + 3)) ;, je možný že Vám tie rozmery proste nejako nebudú sedieť a bonus treba nepôjde vidieť, ako som hovoril je to spôsobené tým že mám iný vzhľad, ak nepomôže úprava veľkosti tak si posuňte jpanel pomocou metódy setBounds ale pri tom musíte mať nastavený layout na null).


 

Stiahnuť

Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami

Stiahnuté 1371x (17.99 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java

 

Všetky články v sekcii
Okenné aplikácie v Java Swing
Článok pre vás napísal Fugiczek
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Aktivity