IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
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í.

Tvorba Snake 2D - krok za krokem v Javě

Tento turiál bude pojednávat jak si počínat při tvorbě hry jako je tato: http://www.itnetwork.cz/…ad-snake-2d/ V podstatě se bude jednat o stejnou funkčnost, akorát s tím rozdílem, že to napíšu znova s přehlednějším kódem.

Vytvoření základu

Nejdříve si nachystáme třídy/objekty které se budou vyskytovat ve hře.

Jako první si vytvoříme základní objekt od kterýho dále budeme dědit.

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še co bude potřeba, jako souřadnice x/y a pro vykreslování barvu, velikost a samotnou metodu na vykreslení, ostatní objekty budou chtít jen malé úpravy.

Dále si napíšeme třídu s objektem, který se bude náhodně umísťovat na herní ploše a za který budeme získávat 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());
    }

}

Zde jsme přidali navíc jen proměnné maxX a maxY pro generování nové pozice aby se ponus nevygeneroval mimo hrací pole, díle instance třídy Random rand pro náhodné umístění a metoda locateBonus pro náhodné umístění.

Dále budeme potřebovat samotného hada, nejdříve si uděláme výčtový typ směru abychom věděli jakým směrem se had bude pohybovat.

package org.fugiczek.snake2d.game;

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

A teď samotná třída s hadem.

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 tady nám toho přibylo o něco více. Potřebujeme list body s částmi těla, barvu těla colorBody abychom odlišili barvu hlavy od těla a směr direct abychom věděli kterým směrem had půjde. Museli jsme přepsat metodu na vykreslení aby se nám vykreslila hlava i s tělem. Kdybychom ji nepřepsali, vykreslila by se jen hlava. Přibyli metody na získávání proměnných, metoda na rozšíření těla, která se bude volat když seberem bonus (nová část se dá na pozici hlavy a hlava se posune) a samozřejmě přibyly metody na pohyb.

No herní objekty by jsme měli mít, teď si napíšeme nějakou třídu statickou která nám bude kontrolovat kolize.

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 není snad co komentovat, jedna metoda kontroluje zda jsme neprohrali, tedy jestli jsme nezajeli mimo herní pole nebo nenajeli sami na sebe a druhá kontroluje zda jsme sebrali bonus.

Tohle by bylo z přípravy snad vše. Teď se vrhneme na grafické zpracování.

Grafické zpracování

Nejdříve si uděláme třídu s hlavním oknem, na který se poté 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 poté si vytvoříme jakoby plátno s herní 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);
            }
        }
    }

}

Při tvorbě takové třídy je vhodné nejdříve si inicializovat všechny možné proměnné/ herní objekty. Dále si napsat herní smyčku, něco jako tato:

public void run() {

        cycleTime = System.currentTimeMillis();

        while(inGame){

            updateLogic();

            updateGui();

            synchFrameRate();

        }

        gameOver();

    }

A jednotlivě si napsat dané metody. Když se u nějaké zasekneme, tak se vrhneme na další a později se k ní vrátíme. Poté si můžeme napsat adaptér na zpracování výstupu z klávesnice/myši.

Spouštěcí třída

Na dokončení nám již chybí jen spouštěcí třída.

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);
            }
        });
    }
}

Snad není potřeba vysvětlovat :D

Závěr

Než začnete psát nějakou hru je dobrý si promyslet jak ta hra bude fungovat, protože přepisování kódu když máte již půlku hotovou není moc milá věc. Když Vám něco nejde, dejte si pár hodin pauzu, poté půjde všechno líp, proleží se Vám to hlavou. Až budete mít hru hotovou je dobrý si projít části kódu kde je hodně logiky/výpočtů, třeba vás napadně způsob lepšího a úspornějšího napsání.

To je ode mě vše. Pokud Vám něco nebude jasné napište do komentářů. (nevím jestli Vám to bude dělat taky ale pokud se Vám neukáže bonus tak je schovanej nahoře o jedno políčko víš :D mám upravený vzhled windowsů tak nevím jestli je to chyba, pokud Vám to bude vadit tak si to zpravte :D, v MainBoard, 17. řádek -> setSize(new Dimension(wid­th+3,height+3));, je možný že Vám ty rozměry prostě nějak nebudou sedět a bonus třeba nepujde vidět, jak jsem říkal je to způsobeno tím že mám jiný vzhled, pokud nepomůže úprava velikosti tak si posuňte jpanel pomocí metody setBounds ale při tom musíte mít nastavený layout na null).


 

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

Staženo 1370x (17.99 kB)
Aplikace je včetně zdrojových kódů v jazyce Java

 

Všechny články v sekci
Tvorba her v Java Swing
Článek pro vás napsal Fugiczek
Avatar
Uživatelské hodnocení:
9 hlasů
Aktivity