Problem z animacją (rysowanie za pomocą Graphics)

0

Witam,

Otóż chciałem zacząć tworzyć jakąś prostą grę i mam problem z wyświetlaniem animacji. Na początek stworzyłem 2 klasy - jedną tworzącą okienko, drugą implementującą zachowania gracza i zaciąłem się na poruszaniem postaci.

W prosty sposób można poruszać postacią. Po prostu jest dodawany piksel przy każdym wciśnięciu klawisza strzałki. Jednak to mnie nie satysfakcjonuje, bo w momencie przytrzymania kursora, postać porusza się o piksel, po czym na chwilę przestaje się ruszać, i znowu rusza się (chodzi mi o opóźnienie klawiaturowe). Wyczytałem gdzieś, że warto stworzyć zmienną boolean, która daje true dla keyPressed i false dla keyReleased. W sumie to rozumiem, ale nie potrafię zaimplementować, bo teraz gdy wciskam strzałkę w prawo (bo właśnie na tym klawiszu chciałem wszystko wypróbować), program się na chwilę zatrzymuje i kulkę pokazuje dopiero w pozycji końcowej. Nie ma animacji...

Proszę o pomoc w tej sprawie, niżej wklejam moje klasy:

package gra;

import java.awt.event.*;
import javax.swing.*;

public class Main implements KeyListener {

    int x = 450;
    int y = 350;
    Player gracz;
    private boolean wcisniety = false;

    public static void main(String[] args) {
        Main gui = new Main();
        gui.stworzramke();
    }

    private void stworzramke() {
        JFrame ramka = new JFrame();
        ramka.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        ramka.setSize(900, 700);
        ramka.setVisible(true);
        ramka.addKeyListener(this);

        gracz = new Player();
        ramka.getContentPane().add(gracz);
    }

    public void keyTyped(KeyEvent e) {
    }

    public void keyPressed(KeyEvent e) {
        int klawisz = e.getKeyCode();
        if (klawisz == KeyEvent.VK_RIGHT) {
            wcisniety = true;
            gracz.ruchWPrawo(wcisniety);
        }
        if (klawisz == KeyEvent.VK_LEFT) gracz.ruchWLewo();
        if (klawisz == KeyEvent.VK_UP) gracz.ruchWGore();
        if (klawisz == KeyEvent.VK_DOWN) gracz.ruchWDol();
        gracz.repaint();
    }

    public void keyReleased(KeyEvent e) {
    }
}
package gra;

import javax.swing.*;
import java.awt.*;

public class Player extends JPanel {

    int x = 10;
    int y = 10;

    @Override
    public void paintComponent(Graphics g) {
        g.setColor(Color.yellow);
        g.fillRect(0, 0, this.getWidth(), this.getHeight());

        //Image obrazek = new ImageIcon("images/player.png").getImage();
        //g.drawImage(obrazek, x, y, this);

        g.setColor(Color.green);
        g.fillOval(x,y,40,40);
    }

    public void ruchWGore() {
        y=y-5;
    }

    public void ruchWDol() {
        y=y+5;
    }

    public void ruchWPrawo(boolean wcisniety) {
        while ((wcisniety = true)&&(x < 500)) {
            x++;
            repaint();
            try {
                Thread.sleep(5);
            } catch (Exception ex) { }
        }
    }

    public void ruchWLewo() {
        x=x-5;
    }

}
0

Nigdy nie umieszczaj metody Thread.sleep() w niczym co jest poruszane przez wątek Swinga. W ogóle istnienie tej metody powinieneś sobie wymazać z głowy.
Druga sprawa - jakiekolwiek pętle w obsłudze zdarzeń też odpadają. Repaint wewnątrz pętli, to wyjątkowo zły pomysł. Kawałek "wcisniety = true" to najwyraźniej pomyłka bo to jest bez sensu. Zamiast inkrementować x w pętli wystarczy po prostu napisać x = 500;. Tę metodę ruchWPrawo przekombinowałeś do bólu (4 błędy w 6 linijkach). Pozostałe metody obsługujące klawisze są ok.

Zmienne informujące o wciśniętych klawiszach są potrzebne po to, żeby gra w swoim ustalonym tempie zmieniała stan jakiegoś obiektu bo jeżeli wciśniesz i przytrzymasz klawisz, to dostaniesz tylko jeden komunikat keyPressed i kiedy puścisz tylko jeden keyReleased. O keyTyped możesz w grach od razu zapomnieć bo to zdarzenie przychodzi po sekwencji keyPressed i KeyReleased tego samego klawisza bez względu na to jaka długa była między nimi przerwa. Puszczanie repaint() w pętli mija się z celem ponieważ Swing może olać każde takie żądanie jeżeli jest ich za dużo (a w pętli jest zawsze). Przy czym wymuszanie odstępu w czasie za pomocą sleep, to jak gaszenie pożaru benzyną.

Schemat najprostszej animacji z uwzględnieniem zmian w grze jest taki:

  1. Sprawdzenie czasu (oznaczenie momentu początku przetwarzania "klatki gry")
  2. Sprawdzenie urządzeń wejściowych
  3. Zmiana stanu gry na podstawie stanu urządzeń wejściowych (tu jest realny postęp w grze)
  4. Sprawdzenie czy został jeszcze czas na wyrenderowanie animacji w bieżącej klatce animacji (np. dla 30 FPS będzie to 1/30 każdej sekundy)
  5. Opcjonalne wyrenderowanie animacji na buforze
  6. Sprawdzenie czy został jeszcze czas na przeniesienie bufora do pamięci ekranu i opcjonalne wykonanie tego. Tu gracz zobaczy nową klatkę gry.
  7. Zakończenie przetwarzania klatki i policzenie czasu końca przetwarzania "klatki gry". W idealnej sytuacji między punktem 1, a 7 powinna minąć 1/30 sekundy dla 30 aktualizacji gry na sekundę, 1/60 sekundy dla 60 aktualizacji itp.
    Jeżeli został jeszcze czas z bieżącej klatki, to wprowadza się opóźnienia tak aby kolejna klatka zaczęła się dokładnie na początku kolejnego odcinka czasu przeznaczonego na kolejną klatkę.
    Jeżeli okazało się, że jest odwrotnie i produkcja klatki przedłużyła się, to natychmiast zaczyna się kolejną klatkę (od punktu 1), przy czym z tej następnej "pożycza" czas spóźnienia z klatki, która minęła. W ten sposób kolejna klatka zakończy się we właściwym momencie, zakończy się przed czasem lub znowu zostanie spóźniona.
    Krótko mówiąc zrobiona tak gra powinna zachowywać się jak człowiek, który robi coś w jakimś rytmie. Jeżeli się spóźni, to 1-2 takty spróbuje nadgonić, a jeżeli to się nie uda, to porzuci jeden lub więcej taktów aby znowu dopasować się do rytmu.

Do tego schematu większość systemów GUI się nie nadaje zbyt dobrze. Wywołania obsługi zdarzeń są asynchroniczne (mogą się zdarzyć w w dowolnym momencie - np. kiedy gra rysuje), polecenia aktualizacji ekranu mogą zostać zignorowane, a nad momentem renderowania (paint) nie ma żadnej kontroli - tak samo jak żadnej kontroli nad procesem przenoszenia buforu do pamięci ekranu (Swing/AWT sam buforuje).

0

Polecam Ci sprawdzić:

  1. Wciśnij i przytrzymaj przycisk na obiekcie implementującym keylistenera i zobacz, czy otrzymujesz 1 komunikat
  2. Co robi keyTyped (wciśnij np strzałki, lub spację)
0

hmmm dlaczego metodę

public void keyReleased(KeyEvent e) {

zostawiasz pustą? Możesz ją wykorzystać do sprawdzenia, który z przycisków został zwolniony. np;

public void keyPressed(KeyEvent e) {
		if (e.getKeyCode() == KeyEvent.VK_LEFT) {
			left = true; 
			System.out.print("left");   //dla sprawdzenia dobrze jest wiedzieć w kompilatorze który przycisk został wciśnięty ;p
		}

i tutaj :

	public void keyReleased(KeyEvent e) {
		        if (e.getKeyCode() == KeyEvent.VK_LEFT) {
			left = false;
		}

W mojej grze wykorzystuje 2 metody ;) powinieneś nad tym pomyśleć.

1 użytkowników online, w tym zalogowanych: 0, gości: 1