Animation mit Thread(s)

blz

Bekanntes Mitglied
Hi,
ich möchte ein einfaches Applet schreiben, bei dem sich (zunächst) zwei Kreise bewegen und jeweils von den Rändern abprallen.
Dazu habe ich eine Klasse "class MyFenster extends Applet" und eine Klasse "Circle extends Thread" angelegt.
Später sollen es mehr Kreise werden und später sollen die Kreise bei gegenseitiger Berührung auch platzen.

Jetzt bin ich mir nicht ganz sicher mit der logischen Programmorganisation:
Ergibt das so, wie ich es vom Prinzip aufgebaut habe, Sinn?

Die run() Methode und eine draw Methode befinden sich in Circle und sehen etwa so aus (noch nicht ganz fertig, aber vom Prinzip):

Java:
    public void run() {
      
        //später: solange Circlezahl > 1
        while (true) {
          
            if (x < 0) {
              
            }
            if (y < 0) {
              
            }
            if (x > frameWidth) {
              
            }
            if (x > frameHeight) {
              
            }
          

            //Aufruf des Threads
            f1.bewege(x, y, xAlt, yAlt, objectWidth, objectHeight, this);
          
            xAlt = x;
            yAlt = y;
        }

    }

Java:
 public void draw(Graphics g) {
        g.drawOval(x, y, objectWidth, objectHeight);
    }

in MyFenster gibt es eine "paint" und eine "synchronized bewege" Methode.

Sinnvoll??

(was ich in den Methode übergebe ist noch nicht ganz ausgedacht ... - hängt eben von dem Aufbau ab.)
 
Hm, vermutlich war die Frage äußert blöd gestellt ...
Ich versuchs nochmal neu:

Hi,
ich möchte ein einfaches Applet schreiben, bei dem sich (zunächst) zwei Kreise bewegen und (später) jeweils von den Rändern abprallen.
Dazu habe ich eine unten stehende Klassen angelegt.
Später sollen es mehr Kreise werden und später sollen die Kreise bei gegenseitiger Berührung auch platzen.
Die Kreise werden zwar gezeichnet und laufen auch in eine Richtung, aber ich befürchte dennoch, dass das ganze völlig falsch angelegt ist.
(Ich habe schon gelesen, dass ich später mit Clipping und DoubleBuffering arbeiten muss und natürlich muss ich dann auch die bisher gezeichneten Kreise wieder löschen, aber das lasse ich jetzt mal außer acht.)

Hier mein Code:

Java:
import java.awt.Graphics;

public class Circle extends Thread {
 
    int x;
    int y;
    int xAlt;
    int yAlt;
    int dx;
    int dy;

    int frameWidth;
    int frameHeight;
    int objectWidth;
    int objectHeight;

    private MyFenster f1;

//  Constructors
    public Circle(MyFenster f1, int x, int y, int dx, int dy) {
        this.x = x;
        this.y = y;
    
        this.dx = dx;
        this.dy = dy;
    
        this.f1 = f1;
    
        objectWidth = 100;
        objectHeight = 100;
        frameWidth = f1.getWidth();    
        frameHeight = f1.getHeight();    
    
        xAlt = x;
        yAlt = y;
    }

    public void run() {   
        //später: solange Circlezahl > 1
        while (true) {
            //später
//            if (x < 0) {
//            
//            }
//            if (y < 0) {
//            
//            }
//            if (x > frameWidth) {
//            
//            }
//            if (x > frameHeight) {
//            
//            }
        
            x=x+dx*5;
            y=y+dy*5;
                    
            f1.bewege(x, y, xAlt, yAlt, objectWidth, objectHeight, this);
        
            xAlt = x;
            yAlt = y;
        }
    }
    
    public void draw(Graphics g) {
        g.drawOval(x, y, objectWidth, objectHeight);
    }
}

Java:
import java.applet.Applet;
import java.awt.BorderLayout;
import java.awt.Graphics;
import java.awt.Image;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JPanel;


public class MyFenster extends Applet
{

    int x;
    int y;
    int objectWidth;
    int objectHeight;
     
    private List<Circle> circles;
    Circle akt;

    private static Image puffer;
    Graphics pufferg;

    public void init() {                           
        setSize(800, 600);
    
        setLayout(new BorderLayout());
      
        circles = new ArrayList<Circle>();
    
        circles.add(new Circle(this, 100, 100, 1, 1));    
        circles.get(0).start();
    
        circles.add(new Circle(this, 150, 150, -2, 1));
        circles.get(1).start();
    
    
        setVisible(true);    
    }

    public void update(Graphics g) {
        paint(g);
    }

    public void paint(Graphics g) {
        //pufferg.setClip(g.getClip());
        akt.draw(g);
    }

    public void bewege(int xNeu, int yNeu, int xAlt, int yAlt, int objectWidth, int objectHeight, Circle akt) {
        this.akt = akt;
            
        repaint(xAlt, yAlt, objectWidth, objectHeight);
        repaint(xNeu, yNeu, objectWidth, objectHeight);
           
        try {
            Thread.currentThread().sleep(20);
        } catch (InterruptedException e) {
        }
    }

}

Vielleicht bekomm ich ja eure professionelle Meinung dazu! Bin für jede Hilfe dankbar!

[PS: was ich zum Bsp ganz und gar nicht verstehe, ist, dass manchmal ein Kreis ausgelassen wird - die Schrittlänge ist doch immer gleich!?]
 
Zuletzt bearbeitet:
Also ich muss sagen ich hab mich mit den grafischen Sachen nie befasst und kann dir diesbezüglich nicht helfen.

Das Kreise teilweise übersprungen werden liegt wohl daran, dass beide Threads wie wild die repaint-Methode gleichzeitig aufrufen und sich da irgendwie beeinflussen.
Hab mal eine kleine Anpassung gemacht:
Java:
public synchronized void bewege(int xNeu, int yNeu, int xAlt, int yAlt, int objectWidth, int objectHeight, Circle akt) {
        notify();
        this.akt = akt;
         
        repaint(xAlt, yAlt, objectWidth, objectHeight);
        repaint(xNeu, yNeu, objectWidth, objectHeight);
         
        try {
            //Thread.sleep(20);
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(500);
            wait();
        } catch (InterruptedException e) {
        }
    }

Die Methode erstmal synchronized gemacht, sodass nur ein Thread 'rein darf'. Wenn der eine Circle dann repainted wurde macht der Thread ein wait() und der andere Thread kriegt den Lock. Der Thread ruft dann direkt wieder notify() und weckt den anderen Thread auf. Repainted aber vorher den Circle erst und macht dann selbst ein wait(). Somit ist praktisch sichergestellt, dass die Threads immer abwechselt zeichnen.

Ich habe das sleep() mal auf eine halbe Sekunde hochgesetzt, dann sieht man dass die Kreise schön gleichmäßig und immer abwechselnd gezeichnet werden.
 
Ich würde auch nicht aus jedem Circle ein Thread machen, sondern nur einen einzigen Thread machen, der alle Circles durchläuft, ganz grob also so:
Java:
public class CircleWorker extends Thread {
  private List<Circle> circles;
  public void run() {
    while(!circles.isEmpty()) {
     for(Circle c : circles) {
        c.updatePosition(); // ändert nur x/y position
     }
     // einmaliges repaint pro runde...
    }
  }
}
 
@Tarrew: alles klar, danke, das funktioniert schon mal ganz gut. hatte die methode schon mal synchronized, aber kein wait und notify --> daher hats elend gelaggt.
aber könntest du das wait und das notify noch mal kurz an dem bsp erläutern? weil intuitiv find ich, würden die beiden methoden andersrum gehören ... (?)

@InfectedBytes: ebenfalls danke, ist wahrscheinlich sinnvoll, nur will ich (was ich noch nicht geschrieben habe) jedem kreis einen individuellen speed zuweisen, insofern kann ich das mit deinem lösungsvorschlag leider nicht realisieren
 
ich hätte jetzt gedacht, ich zeichne jeden kreis immer einen pixel weiter drüben und muss dementsprechend die schnelleren kreise hochfrequenter zeichnen als die langsamen - dann würden pro runde ja die langsameren wieder an die gleiche stelle gezeichnet werden - ist es nicht eleganter, jeden nur zu zeichnen, wenn sich was verändert hat?
 
Nicht unbedingt, da der ständige Threadwechsel einen nicht zu unterschätzenden Overhead produziert. Durch den einzelnen Thread sparst du dir dies, auch wenn du ggf, etwas mehr zeichnen musst. Außerdem kannst du natürlich weiterhin nur zeichnen, wenn sich die position um mindestens einen pixel verändert hat.
 
hm, klingt plausibel! werd ich mir überlegen. könntest du mir trotzdem auch nochmal das wait und notify aus tarrews bsp oben erklären. das prinzip ist mir klar, aber ich check weder dieses bsp noch andere aus dem www.
 
Wenn zwei Threads versuchen auf die gleiche synchronized Methode des gleichen Objekts zuzugreifen, so wird nur einer Threads reingelassen, der andere muss solange warten, bis der erste Thread die Methode wieder verlassen hat. Alternativ kann ein Thread auch manuel wait aufrufen, wodurch er sich als wartend markiert und die Methode nicht weiter ausführt, dadurch kann dann ein anderer Thread die Methode betreten.
Der Thread der wait aufgerufen hat, kann nun aber erst weiter machen, wenn ein Thread (der auf dem selben Objekt synchronisiert) notify bzw. notifyAll aufruft.

Das Beispiel von Tarrew finde ich nicht ganz so optimal, da im grunde ein reines synchronized ohne wait/notify reichen würde. Ein "problem" ist zudem, dass der Thread für 500ms schlafen gelegt wird und danach das wait aufruft
 
es scheint aber schon sehr viel zu bringen, dass er wait und notify angibt. ohne die signale ruckelt die animation enorm, es wird nie qausigleichzeitig gezeichnet, sondern immer nur ein kreis und dann gewartet.
 
Ein normales synchronized hätte auch gereicht, das stimmt. Allerdings habe ich das wait/notify eingebaut, damit die Threads immer abwechselnd den Lock bekommen.

Macht man die Methode nur synchronized, dann wird die JVM versuchen so viel wie möglich zu optimieren und bei anderen Programmen ist mir aufgefallen, dass es meistens so ist, dass die Threads nicht abwechseldn den Lock kriegen sondern immer 1 Thread ~20-50x und dann der andere Thread. Es wird also wesentlich seltener gewechselt.

Das lange Sleep diente nur zur Veranschaulichung, dass der Lock immer abwechselnd vergeben wird. Setzt man den Wert runter wird es natürlich wesentlich flüssiger.
 
das verstehe ich immer noch nicht:
wenn ich wait und notify auskommentiere, läuft es absolut nicht flüssig - insofern reicht doch ein normales synchronized nicht ...?!! (s. aktueller code)

Java:
import java.awt.Graphics;

public class Circle extends Thread {
   
    int x;
    int y;
    int xAlt;
    int yAlt;
    int dx;
    int dy;
  
    int counter;
    int speed;
  
    int frameWidth;
    int frameHeight;
    int objectWidth;
    int objectHeight;
  
    private MyFenster f1;
  
//  Constructors
    public Circle(MyFenster f1, int x, int y, int dx, int dy) {
        this.x = x;
        this.y = y;
      
        this.dx = dx;
        this.dy = dy;
      
        this.f1 = f1;
      
        objectWidth = 100;
        objectHeight = 100;
        frameWidth = f1.getWidth();      
        frameHeight = f1.getHeight();  
      
        xAlt = x;
        yAlt = y;
      
        counter = 25;
        speed = 20;      
    }
  
    public void run() {     
        //später: solange Circlezahl > 1
        while (true) {
            if (x <= 0) {
                dx = -dx;
                speed = 1;
                counter = 0;
            }
            if (y <= 0) {
                dy = -dy;
                speed = 1;
                counter = 0;
            }
            if ((x + objectWidth + 1) >= frameWidth) {
                dx = -dx;
                speed = 1;
                counter = 0;
              
            }
            if ((y + objectHeight + 1) >= frameHeight) {
                dy = -dy;
                speed = 1;
                counter = 0;
            }
          
          
          
            if (speed < 20 && counter == 0) {
                speed++; //langsamer, da ms-delay höher wird
                counter = 50;
            }
          
            counter--;
          
            x = x + dx;
            y = y + dy;
                      
            f1.bewege(this);
          
            xAlt = x;
            yAlt = y;
        }
    }
      
    public void draw(Graphics g) {
           g.drawOval(x, y, objectWidth, objectHeight); 
    }  
  
    public int GetSpeed() {
        return speed;
    }      
  
    public int GetXAlt() {
        return xAlt;
    }
  
    public int GetYAlt() {
        return yAlt;
    }
  
    public int GetXNeu() {
        return x;
    }
  
    public int GetYNeu() {
        return y;
    }
  
    public int GetObjectWidth() {
        return objectWidth;
    }
  
    public int GetObjectHeight() {
        return objectHeight;
    }
}

Java:
import java.applet.Applet;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.util.ArrayList;
import java.util.List;

import sun.java2d.loops.DrawRect;

public class MyFenster extends Applet
{ 
    private List<Circle> circles;
    Circle akt;
    private static Image puffer;
    Graphics pufferg;     
    Color color;
  
    public void init() {                                
        setSize(1024, 768);
        setLayout(new BorderLayout());
      
        circles = new ArrayList<Circle>();
              
        circles.add(new Circle(this, 100, 100, 1, 1));      
        circles.get(0).start();      
        circles.add(new Circle(this, 150, 150, -1, 1));
        circles.get(1).start();
             
        setVisible(true);      
    }
  
    public void update(Graphics g) {
        paint(g);
    }
  
    public void paint(Graphics g) {
//        g.setColor(Color.WHITE);
//        g.fillRect(akt.GetXAlt(), akt.GetYAlt(), (akt.GetXAlt() + akt.GetObjectWidth()), (akt.GetYAlt() + akt.GetObjectHeight()));
//        g.setColor(Color.BLACK);
        akt.draw(g);    
    }
  
    public synchronized void bewege(Circle akt) {
        notify();
        this.akt = akt;

        repaint(akt.GetXNeu(), akt.GetYNeu(), akt.GetObjectWidth(), akt.GetObjectHeight());
       
        try {
            System.out.println(Thread.currentThread().getName());
            System.out.println(akt.GetSpeed());
            wait();
            Thread.sleep(akt.GetSpeed());
          
        } catch (InterruptedException e) {
        }
    }

}
 
Ein synchronized reicht damit nur ein Thread auf die Methode zugreift.

Ein synchronized reicht aber nicht um es flüssig darzustellen, da ein Thread immer ~20x den Lock hintereinander bekommt und dann erst gewechselt wird. Deswegen das wait/notify. Damit die Threads immer abwechselnd zeichnen.
 
ok. aber wie schon oben angedeutet: wieso in dieser reihenfolge?
ich verstehe es so, dass der eine Thread in der methode anfängt und sofort notify auslöst. das heißt doch, er weckt den anderen wartenden thread auf. aber das ergibt doch keinen sinn, da er die methode ja noch gar nicht abgearbeitet hat, also noch gar nicht freigeben kann.
andersherum ergibt es doch für einen thread, der mit der methode schon durch ist, keinen sinn, noch zu warten.
ich verstehe also nicht, warum wait und notify nicht genau vertauscht sind!
(zwischendurch mal vielen dank für die Hilfe an euch beide!)
 
Okay. Also am Anfang wollen beide Threads die Methode aufrufen.

Nehmen wir jetzt an das wait() und notify() wäre vertauscht.
Ein Thread würde den Lock kriegen und in die Methode gehen. Danach würde er das wait() aufrufen. Dann würde er den Lock freigeben und der zweite Thread würde in die Methode gehen und auch wait() machen.
Dann schlafen beide Threads und werden nie aufgeweckt werden.

Deswegen diese Reihenfolge:
Ein Thread bekommt den Lock und macht ein notify um einen schlafenden Thread aufzuwecken. Beim ersten Mal hat das keine Wirkung, da noch kein Thread ein wait() gemacht hat. Jetzt zeichnet der Thread also und macht danach ein wait(). Damit wird nur sichergestellt, dass der zweite Thread den Lock kriegt. Dieser ruft direkt notify() auf um den schlafenden Thread zu wecken. Beachte: Der aufgeweckete Thread hat noch nicht den Lock, kann also nicht weiterlaufen.

Der zweite Thread zeichnet also und macht dann das wait(). Jetzt kann der eben aufgeweckte Thread den Lock kriegen und weiterlaufen. Er geht also wieder in die Methode und macht das notify() um den Thread aufzuwecken.
Das ganze geht dann die ganze Zeit hin und her, sodass sichergestellt ist, dass die Threads immer abwechselnd die Methode aufrufen.
 
ok, danke!!! das ist schon mal gut!
dh im prinzip also, das notify sagt zum dem Thread, der in der warteschlange als nächstes dran ist: "Hey, jetzt bin ich grad drin, ich sperr mal ab, aber halt dich schonmal bereit, du bist als nächstes dran!" ?
und das wait() macht nichts anderes als den Lock wieder freizugeben?

oder ist es sinnvoller, sich ganz platt notify() einfach als lock() und wait() als unlock() vorzustellen, und dann ist einfach der nächste thread in der line dran?
 
In diesem Kontext passt das erste eher 😉

Wenn du es gerne umgangssprachlich hast, ist es so:
Ruft ein Thread notify() sagt er dem anderen: Ich habe den Lock gekriegt, du kannst dich bereithalten.
Und beim wait(): Ich bin fertig, du kannst jetzt repainten.

Das notify() kannst du auch hinpacken wo du willst. Es muss nur vor dem wait() sein. Wenn du es direkt vor das wait() machst ist es vllt intuitiver. Nach dem Motto: Ich habe repainted und bin fertig, jetzt darfst du sofort.
 
Jetzt ist doch noch etwas unklar:

Ich habe die Version mit einem Thread pro Circle hingekriegt, und sie läuft auch. wenn ich allerdings den Speed auf maximal setze (also den Thread jeweils nur 1 ms schlafen lasse) flackert der Bildschirm trotz double buffering.
Daher habe ich versucht die von InfectedBytes vorgeschlagene Variante mit einem einzigen Thread umzusetzen.
Allerdings bekomme ich die nicht zum Laufen, da ich den repaint()-Aufruf in CircleWorker nicht so machen kann, wie ich es mir vorstelle ... ?

Java:
import java.awt.Color;
import java.awt.Graphics;

public class Circle {

    int x;
    int y;

    int dx;
    int dy;

//    int counter;
//    int speed;

    int objectWidth;
    int objectHeight;

    public Circle(int x, int y, int dx, int dy) {
        this.x = x;
        this.y = y;

        this.dx = dx;
        this.dy = dy;
    
        objectWidth = 100;
        objectHeight = 100;

//        counter = 25;
//        speed = 20;
    }

    public void UpdatePosition(int frameWidth, int frameHeight) {

        if (x <= 0) {
            dx = -dx;
//            speed = 1;
//            counter = 0;
        }
        if (y <= 0) {
            dy = -dy;
//            speed = 1;
//            counter = 0;
        }
        if ((x + objectWidth + 1) >= frameWidth) {
            dx = -dx;
//            speed = 1;
//            counter = 0;

        }
        if ((y + objectHeight + 1) >= frameHeight) {
            dy = -dy;
//            speed = 1;
//            counter = 0;
        }

//        if (speed < 20 && counter == 0) {
//            speed++; // langsamer, da ms-delay höher wird
//            counter = 50;
//        }

//        counter--;

        x = x + dx;
        y = y + dy;    
    }



    public void draw(Graphics g, int i) {
        if (i == 0) {
            g.setColor(Color.WHITE);
        } else if (i == 1) {
            g.setColor(Color.BLACK);        
        }
    
        g.drawOval(x, y, objectWidth, objectHeight);
    }

//    public int GetSpeed() {
//        return speed;
//    }

//    public int GetXNeu() {
//        return x;
//    }
//
//    public int GetYNeu() {
//        return y;
//    }

//    public int GetObjectWidth() {
//        return objectWidth;
//    }
//
//    public int GetObjectHeight() {
//        return objectHeight;
//    }
}

Java:
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.util.ArrayList;
import java.util.List;

public class CircleWorker extends Thread {

    private List<Circle> circlesOld;
    private List<Circle> circlesNew;

    MyFenster f1;

    // Offscreen-Image fuer Double-Buffering
    private Image offscreenImage;
    // Referenziert das Graphics-Objekt des offscreen images. Hierauf kann
    // gezeichnet werden.
    Graphics OIgraphics;

    Dimension dim;

    CircleWorker() {
        circlesNew = new ArrayList<Circle>();
        circlesOld = new ArrayList<Circle>();

        circlesNew.add(new Circle(100, 100, 1, 1));
        circlesNew.add(new Circle(150, 150, -1, 1));

        circlesOld.add(new Circle(100, 100, 1, 1));
        circlesOld.add(new Circle(150, 150, -1, 1));

        f1 = new MyFenster();
        dim = f1.getSize();

        // Erzeugen eines Offscreen-Images fuer das Double-Buffering
        offscreenImage = f1.createImage(dim.width, dim.height);
        // Graphics-Objekt des Offscreen-Images fuer Zeichenoperationen
        OIgraphics = offscreenImage.getGraphics();
    }

    public void run() {
        while (true) {

            while (!circlesNew.isEmpty()) {
                for (Circle c : circlesNew) {
                    c.UpdatePosition(dim.width, dim.height);

                    // neuen Kreis schwarz zeichnen
                    c.draw(OIgraphics, 1);
                }
            }

            // Hier kommt es zum Fehler, falls circlesNew und circlesOld nicht
            // gleich lang sind
            for (int i = 0; i < circlesOld.size(); i++) {
                // alten Kreis weiss ueberzeichnen
                circlesOld.get(i).draw(OIgraphics, 0);
                // Position der alten Kreise auf die der aktuell neuen setzen
                circlesOld.set(i, circlesNew.get(i));
            }

            repaint();

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

    public void update(Graphics g) {
        paint(g);
    }

    public void paint(Graphics g) {

        // das Offscreen-Image mit überzeichnetem alten und dem neuen Kreis wird
        // auf den Bildschirm gezeichnet
        g.drawImage(offscreenImage, 0, 0, f1);
    }
}

Java:
import java.applet.Applet;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.util.ArrayList;
import java.util.List;

import sun.java2d.loops.DrawRect;

public class MyFenster extends Applet
{
    public void init() {                               
        setSize(1024, 768);
        setLayout(new BorderLayout());
        setVisible(true);     
    
        CircleWorker cw = new CircleWorker();
        cw.start();
    } 
}

Kann es sein, dass man repaint() nie aus einer Endlosschleife aufrufen darf?
Dh selbst wenn ich das while (true) weglasse, geht es nicht, weil das run() selbst eine Endlosschleife ist? (Kann ich also repaint() nie aus run() aufrufen?)
Das ist Quatsch, oder?

Es liegt wohl daran, dass meine Klasse nicht von Component erbt und somit die repaint() Methode nicht erbt.

Aber wie müsste ich dann sinnvoll umstrukturieren?
Circleworker extends Applet implements Runnable?
 
Zuletzt bearbeitet:
Sorry, blödsinnige Frage, hat sich erledigt. Natürlich funktioniert repaint aus der Schlafe, Begründung s.o. Mein letzter Vorschlag funktioniert auch.
 
Und noch eine Bemerkung: InfectedBytes hatte sooo recht mit seinem Einwand, dass die vielen Threads unnötig Overhead produzieren. Habe es mittels seinem Vorschlag realisiert und es läuft bei jedem Speed flüssig!
 

Zurück
Oben