Probleme mit AffineTransform

TheChemist

Bekanntes Mitglied
Hallo,
ich programmiere zur Zeit an einem kleinem 2D-Game. Spielfiguren werden dabei durch Rechtecke repräsentiert und sollen mit AffineTransform bewegt werden (nur Translation und Rotation). Das habe ich soweit hinbekommen, bin dabei aber auf ein Problem gestoßen.
So scheint die Transformation lediglich den Umriss des Rechtecks zu verändern. Drehe ich also um 30° und bewege dann ein Stück nach vorne wird das schön gezeichnet, die Koordinaten meines Spielobjektes bzw. Rechtecks bleiben aber unangerührt. Jetzt bin ich bisher erfolglos auf der Suche nach einer Möglichkeit die Koordinaten des Spielobjektes beim transformieren aktuell zu halten. Google und die API habe ich schon heftigst bemüht aber auf keine konkretes Ergebnis gestoßen, vielleicht sehe ich aber auch nur den Wald vor lauter Bäumen nicht mehr^^

Ich habe mal ein kleines Beispiel gecoded, das das Problem nochmal deutlich macht. Das rote Rechteck ist das Spielobjekt vor der Transformation dar, das gründe danach. Die Ausgabe der Koordinaten bleibt allerdings die Selbe.

EDIT: Achso, Sinn und Zweck der Sache ist übrigens eine simple Kollisionserfrage mit intersects() zu ermögliche. Wenn jemand eine andere Methode zur Kollisionserkennung kennt, die nicht auf die Koordinaten angewiesen ist, ist das auch eine Hilfe nehme ich an.

Java:
import javax.swing.SwingUtilities;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.BorderFactory;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;

public class Main {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI() {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.add(new MyPanel());
        f.pack();
        f.setVisible(true);
    }
}

class MyPanel extends JPanel {

    public MyPanel() {
        setBorder(BorderFactory.createLineBorder(Color.black));
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(800, 600);
    }

    @Override
    public void paintComponent(Graphics g) {
        //Upcast --> mehr Funktionen in Graphics2D
        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        //neue Transformation erstellen
        AffineTransform tx = new AffineTransform();
        //ein Testrechteck:
        Rectangle2D.Double r = new Rectangle2D.Double(50, 50, 200, 200);
        System.out.println("Rechteck rot: " + r);

        //r vor Transformation:
        g2d.setColor(Color.red);
        g2d.fill(r);

        //Transformationen konkatenieren
        tx.rotate(60 * Math.PI / 180.0, r.getCenterX(), r.getCenterY());
        tx.translate(100, 0);

        //Transformation anwenden
        g2d.transform(tx);

        //r nach Transformation:
        g2d.setColor(new Color(0, 180, 0, 200));
        g2d.fill(r);

        System.out.println("Rechteck grün: " + r);
    }
}
 
Zuletzt bearbeitet:
Ja, wenn man die AffineTransform von einem Graphics2D setzt, wird das Objekt nur gedreht gezeichnet - es IST aber nicht gedreht. Es gibt da mehrere mögliche Ansätze. Das erste "Problem" ist, dass das objekt ja als "Rechtangle" vorliegt. Das kann erstmal nicht (so wie man sich das vorstellt) gedreht sein, weil es ja nur eine Position und Größe hat, aber nichts, was eine Drehung repräsentieren könnte. Man könnte statt des Rectangles einen Path2D verwenden. DER kann mit einer AffineTransform gedreht werden, so dass sich auch die Eckpunkte verändern. Aber da ist der Überschneidungstest ggf. aufwändiger, muss man sich überlegen. Eine Alternative wäre, dass man vor dem Überschneidungstest das eine Objekt in den Raum des anderen Objektes transformiert: Wenn man zwei Objekte hat, X0 und X1, und die mit den Transformationen T0 und T1 transformiert, dann überschneiden die sich genau dann, wenn das UNtransformierte Objekt X0 mit (T0^-1)*X1 überschneidet. Hui. Das ist bestimmt falsch, nach 4 Stunden Schlaf und ohne Koffein. Aber so von der Grundidee her...
 
AffineTransform wirkt sich nur auf die Zeichenebene ab, wie Du schon gemerkt hast.
Wenn Du Deine Objekte mitrotieren willst, wird es aufwändiger. Wäre es u. U. eine Option mit vorgefertigten Bildern zu arbeiten und sich auf einige Zwischenbilder zu beschränken?
Oder ist es wichtig, daß Dein Objekt die Bewegungsrichtung um wenige Grad ändern kann?
 
@Quaxli
Also da die Objekte sich flüssig um den Mittelpunkt rotieren lassen sollen, ist es schon wichtig, dass auch eine Rotation um wenige Grad möglich ist. Immoment wollte ich auch erstmal bei einer einfachen Repräsentation durch 2D-Objekte bleiben.

@Marco13
Der Ansatz mit dem Path klingt eigentlich ganz interessant, ich muss sagen deine anderen Vorschlag konnte ich nicht ganz nachvollziehen^^ (ist mit T0^-1 die umkehrende Transformation gemeint?) Den Path2D werde ich mir mal anschauen und kucken ob ich es damit schaffe eine Kollisionsabfrage zu basteln.

Erstma danke an euch Zwei, vllt. hat ja jemand noch einen anderen Tipp.
 
Das Problem beim Path ist, wie gesagt, dass die Überschneidungstests nicht mehr so einfach sind. (Schon einfach zu Programmieren, mit Area und Schnittmengenberechnung, aber das wäre wohl ein Overkill). Das gilt aber für alle geometrischen Formen. Nur bei Rechtecken kommt man mit den paar Vergleichen aus, die in rectangle.intersects(otherRectangle) eben so gemacht werden.

Mit T0^-1 ist die Inverse Transformation gemeint - kann man wohl "umkehrend" nennen. Also, ganz grob müßte das schon so stimmen, wenn's nicht hinhaut kann ich später nochmal schauen.
 
@Marco13

Noch ne Frage: Wie habe ich hier "(T0^-1)*X1" das "*" interpretieren? Ich nehme an das Ergebnis davon ist wieder ein Rectangle? Wäre zumindest logisch, dann kann ich wieder einfach prüfen ob sie x0 und x1 überschneiden.

EDIT: Hab mein Modell mal bisschen angepasst, die checkIntersect()-Methode funktioniert allerdings nicht, macht auch nicht so arg viel Sinn was da steht, da muss ich nochmal drüber schauen...

[Java]
import java.awt.geom.NoninvertibleTransformException;
import javax.swing.SwingUtilities;
import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;

public class Main {

public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {

public void run() {
createAndShowGUI();
}
});
}

private static void createAndShowGUI() {
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(new MyPanel());
f.pack();
f.setVisible(true);
}
}

class MyPanel extends JPanel {

MyGameObject r1;
MyGameObject r2;

public MyPanel() {
r1 = new MyGameObject(50, 50, 150, 150, Color.red);
r2 = new MyGameObject(550, 50, 100, 100, Color.blue);
AffineTransform tx1 = new AffineTransform();
tx1.translate(250, 0);
r1.setTx(tx1);
AffineTransform tx2 = new AffineTransform();
tx2.translate(-250, 0);
r2.setTx(tx2);
}

@Override
public Dimension getPreferredSize() {
return new Dimension(800, 600);
}

@Override
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
r1.drawObject(g2d);
r2.drawObject(g2d);
g2d.drawString(checkIntersect(r1, r2) + "", 400, 300);
}

public boolean checkIntersect(MyGameObject go1, MyGameObject go2) {
AffineTransform t0 = go1.getTx();
AffineTransform t1 = go2.getTx();
try {
go2.setTx(t0.createInverse());
} catch (NoninvertibleTransformException ex) {
}
return go1.intersects(go2);
}
}

class MyGameObject extends Rectangle2D.Double {

AffineTransform tx;
Color color;

public MyGameObject(double x, double y, double width, double height,
Color color) {
super(x, y, width, height);
this.color = color;
}

public void drawObject(Graphics2D g) {
//Upcast --> mehr Funktionen in Graphics2D
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);

// vor Transformation:
g.setColor(color);
g.fill(this);
// nach Transformation
g.setTransform(tx);
g.fill(this);
g.setTransform(new AffineTransform());
}

public AffineTransform getTx() {
return tx;
}

public void setTx(AffineTransform tx) {
this.tx = tx;
}
}
[/Java]
 
Zuletzt bearbeitet:
Nyee... sorry ...das funktioniert natürlich nicht wenn man nachher wieder Rectangles für den Überschneidugstest braucht *kopfpatsch* 😳

Für beliebig gedrehte Rechtecke kann das etwas komplizierter werden, das ist dann z.B. hier 2D Rotated Rectangle Collision ganz gut beschrieben. Falls es nicht klappt... sowas kann man immer mal brauchen, vielleicht bastel' ich da morgen abend mal was.
 
Okay, der Link sieht echt gut aus. Ich nehme andie beliebig gedrehten Rechtecke realisiere ich schon wie vorher angesprochen mit dem Path2D? Dann werde ich in die Richtung mal weiter schauen.
 
Da gibt's dann mehrere Möglichkeiten, welche davon die "beste" ist, ist so im voraus schwer zu sagen. Aber eigentlich braucht man die "gedrehte geometrische repräsentation" vielleicht gar nicht. Eigentlich muss man ja nur die Vektoren transformieren, die man für die Berechnung der separating axes verwendet... Aber das muss man sich mal genauer ansehen.
 
Also nach ein bisschen mehr rumlesen habe ich rausgefunden, dass eine Area eigentlich das ist was ich suche, zumindest scheint es damit zu gehen, kann allerdings sein dass sich damit später Probleme auftun, aber naja.
Ich hab also meine Demoklasse bisschen umgeschrieben und habe dabei ein anderes Problem festgestellt. Warum überlagern sich die Shapes beim Bewegen? Vllt. kann ja jemand mal schnell das Beispiel komplieren und es sich anschauen was ich meine, ist direkt so ausführbar.

[Java]
import javax.swing.SwingUtilities;
import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;

public class Main {

public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {

public void run() {
createAndShowGUI();
}
});
}

private static void createAndShowGUI() {
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(new MyPanel());
f.pack();
f.setVisible(true);
}
}

class MyPanel extends JPanel implements Runnable {

MyGameObject r1;
MyGameObject r2;

public MyPanel() {
r1 = new MyGameObject(50, 50, 150, 150, Color.red);
r2 = new MyGameObject(550, 50, 100, 100, Color.blue);
AffineTransform tx2 = new AffineTransform();
tx2.rotate(45.0 * Math.PI / 180, r2.area.getBounds2D().getCenterX(), r2.area.getBounds2D().getCenterY());
r2.setTx(tx2);

Thread t = new Thread(this);
t.start();
}

@Override
public Dimension getPreferredSize() {
return new Dimension(800, 600);
}

@Override
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
r1.drawObject(g2d);
r2.drawObject(g2d);
g2d.drawString(checkIntersect(r1, r2) + "", 400, 300);
}

public boolean checkIntersect(MyGameObject go1, MyGameObject go2) {
Area a1 = go1.area;
Area a2 = go2.area;
Area a1Copy = (Area) a1.clone();
a1Copy.intersect(a2);
return !a1Copy.isEmpty();
}

public void run() {
while (true) {
r1.moveObject();
repaint();
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
}
}
}
}

class MyGameObject {

Path2D.Double representation;
Area area;
AffineTransform tx = new AffineTransform();
Color color;

public MyGameObject(double x, double y, double width, double height,
Color color) {
area = new Area(new Rectangle2D.Double(x, y, width, height));
this.color = color;
}

public void moveObject() {
area.transform(AffineTransform.getTranslateInstance(10, 0));
}

public void drawObject(Graphics2D g) {
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(color);
g.draw(area);
}

public AffineTransform getTx() {
return tx;
}

public void setTx(AffineTransform tx) {
area.transform(tx);
}
}
[/Java]
 
Spätere Probleme kann man teilweise vermeiden, wenn man sich übed die Schnittstellen im klaren ist - und diese ggf. sogar als Interfaces anbietet. Aber ich gehe davon aus, dass das nur ein Test war, ob es grundsätzlich geht.

Meinst du mit "überlagern" das, was durch die Zeile
Code:
    public void paintComponent(Graphics g) {
        [b]super.paintComponent(g);[/b]
verhindert wird?
 
Ja, das wars, oh man das war wieder zu einfach^^
Ich werde jetzt erst einmal mit den Areas arbeiten denke ich und schauen wie weit ich damit komme. Ein Problem das ich jetzt allerdings schon sehe wird sich bei der Rotation um den Mittelpunkt ergeben, der sich ja nicht so ohne weiteres bestimmen lässt. Zwar kann ich ihn annähern über den Mittelpunkt der BoundBox, aber das könnte teilweise ziemlich ungenau werden.

Aber naja, erstmal weiter schauen, vllt. klappt es ja doch.
 
Hmja, wie angedeutet: Du solltest dir genau überlegen, wie "MyGameObject" denn in der richtigen Anwendung aussehen sollte. Eigentlich sollte man sowas dann auch über ein interface beschreiben. Aber eine geeignete Klassenstruktur für die Kollisionserkennung potentiell beliebiger Implementierungen ist auf jeden Fall schwierig - da braucht man dann meistens doch einen etwas konkreteren Typ. Das Zeichnen kann man ja gut wegkapseln, aber bei den Kollisionen hängt die Erkennung eben von beiden Typen ab (wenn du z.B. ein Kreisförmiges Objekt haben willst, dann wäre die Kollisionserkennung mit einem anderen Kreisförmigen Objekt trivial, und es bräuchte auch keinen Path2D - aber bei der Erkennung einer Kollision Rechteck-Kreis muss man schon wissen, womit man es zu tun hat...)
 
Ja du hast recht. Ich werde mich allerdings erstmal damit zufrieden geben nur Rechtecke durch die Areas zu beschrieben. Sollte das irgendwann mal alles so funktionieren wie ich es mir vorstelle, werde ich vllt. versuchen noch andere Formen darzustellen. Ich werde erste Ergenisse hier vllt. mal hochladen wenn ich in nächster Zeit welche zu Stande bringe...
 

Zurück
Oben