Kollision soll nur an oberer Seite (Breite) möglich sein

Hallo Leute,

folgendes Problem:
Ich habe zwei Figuren. Beide haben um sie herum eine rechteckige Bounding Box.
Nun möchte ich beide auf Kollision prüfen.

Bisher habe ich ganz klassisch die Methode “overlaps”, also simples überschneiden benutzt.
Jedoch möchte ich die Kollision noch besser dem Spiel anpassen. Da man sich eh nur nach rechts und links bewegen kann und die “Figur” von oben herabfällt, muss man diese auffangen.
Und jetzt soll die Kollision quasi nur zählen, wenn die herabfallende Figur die “Breite” der Spielfigur trifft.
Das Ganze jetzt umzusetzen müsste eigentlich relativ einfach sein, jedoch stehe ich gerade auf dem Schlauch.

Ich benutze als Game-Framwork libGDX, die Rectangle Klasse, die ich benutze, kann man hier nachlesen:
http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/math/Rectangle.html

Ich benutze dabei OpenGL, das Koordinatensystem (hier im 2D Raum) beginnt (wie in der Schule) in der unteren linken Bildschirmecke.

Danke im Voraus!

LG

[QUOTE=Logan]Da man sich eh nur nach rechts und links bewegen kann und die “Figur” von oben herabfällt, muss man diese auffangen.
Und jetzt soll die Kollision quasi nur zählen, wenn die herabfallende Figur die “Breite” der Spielfigur trifft. [/QUOTE]

Den Teil müßtest du ggf. nochmal genauer erklären. Man soll horizontal durch Hinternisse durchlaufen können, aber nicht von oben durchfallen, ist das richtig? Wie sieht denn die Kollisionsantwort im Moment aus?

Nein, die Spielfigur kann man nur entlang der X-Achse bewegen, die Sachen, die man auffangen soll, bewegen sich entlang der y-Achse von oben nach unten. Wenn ich jetzt, wie bisher, einfach die overlaps Methode benutze, also abfrage, ob die beiden Bounding Boxen sich überschneiden, kommt es dazu, dass die Kollision zählt, obwohl die herunterfallenden Figuren die Höhe z.B. an der rechten Seite getroffen haben. Ich möchte das ganze noch so einschränken, dass die Kollision nur zählt, wenn die oberste Seite, also die Breite zuerst getroffen wurde.

LG

Wie läuft denn die Bewegung ab? Da fällt irgendwas von oben runter, und man kann sich per Tastendruck bewegen… also, da gibt es einige Zweideutigkeiten. Aber ganz pragmatisch:


wenn ((alte untere Kante des Gegenstandes > obere Kante des Spielers) UND
      (neue untere Kante des Gegenstandes < obere Kante des Spielers) UND
      Gegenstand ist horizontal über dem Spieler)
dann war das so eine Kollision

Was meinst du genau damit, wie läuft sie ab ? Die Bewegung der Gegenstände und des Spielers ist immer gleichmäßig, Sie treffen also immer im 90° Winkel aufeinander.
Ich habe keine Information über die alte untere Kante, ich habe bloß den linken unteren Eckpunkt und oberen linken Eckpunkt jeder Figur.

Nun, ein bißchen code könnte helfen, und zwar code von der Stelle, wo du die Kollisionserkennungsmethode aufrufst. Und … wenn du sagst, dass du nur den unteren linken und den oberen linken Eckpunkt hast, klingt das, als würdest du nicht mal die Breite der Figur kennen - bei einem Rectangle sollte diese Information aber irgendwie vorliegen…

Doch, die Breite und die Höhe habe ich natürlich, übrigens 16*16 Pixel (Quadrat).
Der Codeausschnitt bringt das ganze hier nicht voran:

    //weiter Code hier
}```

LG

Ich meinte schon ein paar Zeilen davor und dahinter :wink: Man müßte wissen, wie die alte und neue Position gespeichert sind und verwendet werden, und wo genau dort die Kollisionsabfrage stattfindet, und was bei einer Kollision passiert (bzw. was passiert, wenn keine Kollision stattfand) etc.

Also gut, ich weiß zwar nicht was du genau sehen willst, aber hier mal ein Ausschnitt:

private Rectangle bucket; //Deklaration der Spielfigur und der Tropfen die herunterfallen
private Array<Rectangle> raindrops;

// in der "Erstellungsmethode"
//Raindropliste anlegen und ersten Regentropfen spawnen
raindrops = new Array<Rectangle>();
spawnRaindrop();

//Die Methode zum Spawnen
private void spawnRaindrop() {
        Rectangle raindrop = new Rectangle();
        raindrop.x = MathUtils.random(0, 800 - 64);
        raindrop.y = 480;
        raindrop.width = 64;
        raindrop.height = 64;
        raindrops.add(raindrop);
        lastDropTime = TimeUtils.nanoTime();
}

//in der Rendermethode
//Regentropfen bewegen lassen, zerstören und entfernen
Iterator<Rectangle> iter = this.raindrops.iterator();
while (iter.hasNext()) {
    Rectangle raindrop = iter.next();
    raindrop.y -= 200 * Gdx.graphics.getDeltaTime();
    if (raindrop.y + 64 < 0)
            iter.remove();
    if (raindrop.overlaps(this.bucket)) {
            this.dropsGathered++;
            this.dropSound.play();
            iter.remove();
     }
}

So das war eigentlich alles relevante.

LG

Ja, das kommt so etwa hin. Es wäre nur noch interessant, wo die Bewegung des “Buckets” stattfindet - also da wird ja wohl der x-Wert durch einen Tastendruck verändert? Das scheint an einer anderen Stelle stattzufinden. Und es wundert mich etwas, dass die Bewegung der Tropfen in der Render-Methode zu stehen scheint. Eigentlich sollte die Spiellogik und das Rendern einigermaßen getrennt sein.

Wenn ich es richtig verstanden habe, soll ja das hier als “Kollision” gelten (d.h. der Tropfen wird aufgefangen)



Vor der Bewegung des Tropfens:

     TT 
     TT 
        
    
    
    EEEE
    EEEE
    EEEE
    EEEE
    
    
    
Nach der Bewegung des Tropfens:


    
     TT
    ETTE
    EEEE
    EEEE
    EEEE    
    

Aber das hier soll NICHT als Kollision erkannt werden - weil der Eimer sich seitlich gegen den Tropfen bewegt:


    
Vor der Bewegung des Eimers (nach links):

     TT
     TT   EEEE
          EEEE
          EEEE
          EEEE
    
    
    
Nach der Bewegung des Eimers (nach links):


    
     TT
    ETTE
    EEEE
    EEEE
    EEEE        

Stimmt das so weit? Falls ja, besteht die Schwierigkeit darin, dass (wie man sieht) der ENDzustand in beiden Fällen gleich ist. Man müßte dann also die Bewegung mit in die Entscheidung einbeziehen. Also, ob die Überschneidung durch das raindrop.y -= 200 * Gdx.graphics.getDeltaTime(); entstanden ist, oder durch die Bewegung des Eimers (wie und wo auch immer die stattfindet)

Ja,
das was du mit dieser Skizze beschrieben hast, stimmt so in etwa.
Hier, wie die Bewegung des Buckets abläuft:

if (Gdx.input.isTouched()) { //Bewegen, wenn an eine Stelle auf dem Touchscren berührt wurde/ die Maus gedrückt wurde
            this.touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
            this.camera.unproject(touchPos);
            this.bucket.x = touchPos.x - 64 / 2;
}
if (Gdx.input.isKeyPressed(Keys.LEFT))
        	this.bucket.x -= 400 * Gdx.graphics.getDeltaTime();
if (Gdx.input.isKeyPressed(Keys.RIGHT))
        	this.bucket.x += 400 * Gdx.graphics.getDeltaTime();

Genau den von dir beschriebenen Endzustand müsste man dann halt irgendwo setzen und dann auch dementsprechend abfragen können, bloß wo und wie. Vielleicht ist es auch einfacher als es ist und ich komme gerade nicht drauf.

LG

Nochmal: Ich glaube, das wird schwierig, wenn zwei voneinander unabhängige Bewegungen (nämlich die vom Tropfen und die vom Eimer) an unterschiedlichen Stellen gemacht werden. NUR aus einen einzelnen Zustand kann man ja nicht erkennen, ob der Tropfen „von oben“ oder „von der Seite“ in den Eimer geflogen ist.

Ich kenne mich mit libGDX nicht so aus, dass ich da irgendwelche Besonderheiten oder Details kennen würde, aber … üblicherweise hat man ja einen „game loop“:

while (gameIsRunning())
{
    doGameLogic();
    display();
}

Es ist immernoch nicht klar, wo die Eimer-Bewegung stattfindet, aber… ich befürchte, dass man (zumindest ich) da ohne mehr Überblick über den Spielablauf ohnehin nicht viel konkreteres dazu sagen kann: Du musst feststellen, ob der Tropfen von oben oder von der Seite in den Eimer geflogen ist. Dazu braucht man Informationen über den alten und den neuen Zustand, sowohl des Tropfens als auch des Eimers. Irgendwelcher pseudocode im Sinne von

if (onlyDropMoved)
{
    if (newDropPosition.intersects(newBucket) &&
        oldDropPosition.isDirectlyAbove(newBucket)) 
    { 
        // Collision!
    }
} else if (dropAndBucketMoved) {
    // Well...
}

wird dir ja vermutlich nicht weiterhelfen.

Aber vielleicht hat jemand anderes eine bessere Kristallkugel als ich :wink:

Libgdx hat keine richtige GameLoop, für die Spiellogik ist u.a. die render Methode zuständig (die pro Frame aufgerufen wird), siehe hier: https://github.com/libgdx/libgdx/wiki/The-life-cycle
Mein Problem wäre dann nur, wie soll ich die alte Position speichern (wenn Sie in jedem Frame aktualisiert wird) und wie soll ich eine Methode schreiben, um zu checken, ob ein Sprite genau darüber ist ?

LG

OK, das ist zumindest “ungewöhnlich”, aber wohl der vielseitigen Unterstützung verschiedenster Backends geschuldet. Trotzdem nochmal: Wo machst du die Behandlung der Eingaben? Auch in der Render-Methode?

Ja, die habe ich auch in der Render-Methode, siehe vorletzen Eintrag.

LG

zwei Stellen sind an Code zu sehen:
der Eimer wird bewegt:
this.bucket.x -= 400 * Gdx.graphics.getDeltaTime(); (mehrere unterschiedliche Fälle)

sowie der/ die mehreren Tropfen werden bewegt:
raindrop.y -= 200 * Gdx.graphics.getDeltaTime();

wenn beides unabhängig voneinander passiert, dann musst du an beiden Stellen prüfen,
falls beides genau einmal pro Frame/ pro Durchlauf passiert, reicht bestimmt einmal

da wohl mindestens mehrere Tropfen beteiligt sind lohnt sich eine separate Methode,
aufgerufen für jeden Tropfen zusammen mit dem Eimer

void checkCollision(Tropfen mit alter Position usw., 
        Tropfen-y-Delta oder neue Position, 
        Eimer mit alter Position usw., 
        Eimer-x-Delta oder neue Position)

in der Methode weißt du wo die Objekte vorher waren und wo danach, beide Situationen,
nun brauchst du nur noch ein Regelwerk für Entscheidung Auffangvorgang erfolgreich oder nicht,

  • evtl. hier Abbruch nötig, falls Tropfen eh schon aufgefangen,
    aber dann sollte besser gar nicht die Methode aufgerufen werden, der Tropfen nicht wie bisher gezeichnet werden usw.

  • ist das neue y vom Tropfen viel zu hoch oder das alte bereits zu niedrig im Vergleich zum Eimer, dann hat es nicht geklappt,
    ansonsten noch nicht gleich erfolgreich, weiter prüfen,

  • aus dem konstanten oder doch bewegten Eimer ein großen oder kleinen korrekten Auffangbereich ausrechnen,
    maximal Eimerbreite, vielleicht 0 wenn zu schnell über ganzen Bildschirm
    → prüfen ob der Tropfen in seiner Ausdehnung exakt in diesem Bereich liegt, oder vielleicht zu 75%

  • usw. was immer dir noch nötig erscheint, welche Regel du auch immer haben willst,
    du hast alle Zeit und alle Rechenkraft der Welt, das genau nach Wunsch hinzubiegen

führe in der Oberfläche Tests durch, lasse dir ein exaktes Log aller daraus intern berechneten Frames mit Einzelaktionen ausgeben
und im Detailbereich die Rechnungen dieser Methode,

  • wenn es augenscheinlich falsch geklappt hat, dann untersuchen warum
  • wenn es trotz guter Anzeige kein Einfangen gab dann auch auch untersuchen was hier an Werten ankam, welche Regeln wie griffen usw.

es besteht (augenscheinlich) keinerlei technische Schwierigkeit, einfach machen, dran arbeiten bis es funktioniert

Also, ein bißchen mit der Kristallkugel zusammengeratener Code… das wird so natürlich nicht funktionieren, aber … was soll man machen…

Man kann mit Slidern die Position und Bewegung des Eimers einstellen, und die Bewegung des Tropfens. Er gibt aus, ob der Tropfen gefangen wurde oder nicht. Wie man die benötigten Informationen in der render-Methode bokmmen KÖNNTE, ist in der auskommentierten Methode skizziert.

package bytewelt;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.geom.Rectangle2D;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

class Rectangle
{
    float x;
    float y;
    float width;
    float height;
}

public class DropCatcher
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }
    
    private static void createAndShowGUI()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        final DropCatcherPanel dropCatcherPanel = new DropCatcherPanel();
        dropCatcherPanel.setPreferredSize(new Dimension(600,600));
        
        JPanel panel = new JPanel(new GridLayout(0,2));
        
        panel.add(new JLabel("Bucket position"));
        final JSlider bucketPositionSlider = new JSlider(-100, 100, 0);
        bucketPositionSlider.addChangeListener(new ChangeListener()
        {
            @Override
            public void stateChanged(ChangeEvent e)
            {
                dropCatcherPanel.setBucketPositionX(
                    260+bucketPositionSlider.getValue());
            }
        });
        panel.add(bucketPositionSlider);
        
        panel.add(new JLabel("Bucket movement"));
        final JSlider bucketMovementSlider = new JSlider(-100, 100, 0);
        bucketMovementSlider.addChangeListener(new ChangeListener()
        {
            @Override
            public void stateChanged(ChangeEvent e)
            {
                dropCatcherPanel.setBucketMovementX(
                    bucketMovementSlider.getValue());
            }
        });
        panel.add(bucketMovementSlider);
        

        panel.add(new JLabel("Raindrop"));
        final JSlider raindropMovementSlider = new JSlider(0, 100, 0);
        raindropMovementSlider.addChangeListener(new ChangeListener()
        {
            @Override
            public void stateChanged(ChangeEvent e)
            {
                dropCatcherPanel.setRaindropMovementY(
                    raindropMovementSlider.getValue());
            }
        });
        panel.add(raindropMovementSlider);
        
        f.getContentPane().setLayout(new BorderLayout());
        f.getContentPane().add(panel, BorderLayout.NORTH);
        f.getContentPane().add(dropCatcherPanel, BorderLayout.CENTER);
        
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }
    

}


class DropCatcherPanel extends JPanel
{
    private Rectangle bucket;
    private Rectangle raindrop;
    private float bucketMovementX;
    private float raindropMovementY;
    
    DropCatcherPanel()
    {
        bucket = new Rectangle();
        bucket.x = 260;
        bucket.y = 500;
        bucket.width = 80;
        bucket.height = 80;

        raindrop = new Rectangle();
        raindrop.x = 280;
        raindrop.y = 400;
        raindrop.width = 40;
        raindrop.height = 40;
    }
    
    public void setBucketPositionX(float bucketPositionX)
    {
        bucket.x = bucketPositionX;
        repaint();
    }

    void setBucketMovementX(float bucketMovementX)
    {
        this.bucketMovementX = bucketMovementX;
        repaint();
    }
    
    void setRaindropMovementY(float raindropMovementY)
    {
        this.raindropMovementY = raindropMovementY;
        repaint();
    }
    
    @Override
    protected void paintComponent(Graphics gr)
    {
        super.paintComponent(gr);
        Graphics2D g = (Graphics2D)gr;
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, getWidth(), getHeight());
        
        g.setColor(new Color(128,128,128,128));
        g.fill(new Rectangle2D.Double(
            bucket.x, bucket.y, bucket.width, bucket.height));
        
        g.setColor(new Color(128,128,128,64));
        g.fill(new Rectangle2D.Double(
            bucket.x+bucketMovementX, bucket.y, bucket.width, bucket.height));
        
        g.setColor(new Color(0,0,255, 128));
        g.fill(new Rectangle2D.Double(
            raindrop.x, raindrop.y, raindrop.width, raindrop.height));
        
        g.setColor(new Color(0,0,255,64));
        g.fill(new Rectangle2D.Double(
            raindrop.x, raindrop.y+raindropMovementY, raindrop.width, raindrop.height));
        
        boolean wasCought = 
            raindropWasCought(
                raindrop, raindropMovementY, 
                bucket, bucketMovementX);
        
        g.setColor(Color.RED);
        g.drawString("Was cought? "+wasCought, 20,40);
    }



    private boolean raindropWasCought(
        Rectangle raindrop, float raindropMovementY, 
        Rectangle bucket, float bucketMovementX)
    {
        // Minimaler und maximaler x-Wert des Buckets
        // VOR der Bewegung...
        float bxMinOld = bucket.x;
        float bxMaxOld = bucket.x + bucket.width;

        // ...und NACH der Bewegung
        float bxMinNew = bxMinOld + bucketMovementX;
        float bxMaxNew = bxMaxOld + bucketMovementX;

        // Oberkante des Buckets
        float byMin = bucket.y; 

        // Minimaler und maximaler y-Wert des Drops
        // VOR der Bewegung....
        float dyMinOld = raindrop.y;
        float dyMaxOld = raindrop.y + raindrop.height;

        // ...und NACH der Bewegung
        float dyMinNew = dyMinOld + raindropMovementY;
        float dyMaxNew = dyMaxOld + raindropMovementY;

        // Minimaler und maximaler x-Wert des Drops
        float dxMin = raindrop.x;
        float dxMax = raindrop.x + raindrop.width;

        // Die eigentliche Abfrage: Der Tropfen gilt als "gefangen",
        // wenn er vor und nach der Bewegung zwischen dem 
        // linken und dem rechten Rand des Buckets war, und
        // NACH der Bewegung tiefer war als der obere Rand
        // des Buckets. 

        // Er wurde NICHT gefangen, wenn er nach der Bewegung
        // noch komplett oberhalb vom Bucket war.
        if (dyMaxNew < byMin) 
        {
            System.out.println(
                "Lower border of drop is "+dyMaxNew+
                ", upper border of of bucket is "+byMin);
            return false;
        }

        // Er wurde NICHT gefangen, wenn er vor oder
        // nach der Bewegung komplett links oder 
        // komplett rechts vom Bucket war.
        if (dxMax < bxMinOld) 
        {
            System.out.println(
                "Right border of drop is "+dxMax+
                ", left border of bucket before movement is "+bxMinOld);
            return false; 
        }
        if (dxMin > bxMaxOld) 
        {
            System.out.println(
                "Left border of drop is "+dxMin+
                ", right border of bucket before movement is "+bxMaxOld);
            return false; 
        }
        if (dxMax < bxMinNew) 
        {
            System.out.println(
                "Right border of drop is "+dxMax+
                ", left border of bucket after movement is "+bxMinNew);
            return false; 
        }
        if (dxMin > bxMaxNew) 
        {
            System.out.println(
                "Left border of drop is "+dxMin+
                ", right border of bucket after movement is "+bxMaxNew);
            return false; 
        }
        
        // Er wurde vermutlich auch nicht gefangen, wenn er
        // vor oder nach der Bewegung des Buckets teilweise
        // außerhalb des Buckets war
        if (dxMin < bxMinOld) 
        {
            System.out.println(
                "Left border of drop is "+dxMin+
                ", left border of bucket before movement is "+bxMinOld);
            return false; 
        }
        if (dxMax > bxMaxOld) 
        {
            System.out.println(
                "Left border of drop is "+dxMax+
                ", right border of bucket before movement is "+bxMaxOld);
            return false; 
        }
        if (dxMin < bxMinNew) 
        {
            System.out.println(
                "Left border of drop is "+dxMin+
                ", left border of bucket after movement is "+bxMinOld);
            return false; 
        }
        if (dxMax > bxMaxNew) 
        {
            System.out.println(
                "Left border of drop is "+dxMax+
                ", right border of bucket after movement is "+bxMaxOld);
            return false; 
        }
        

        return true;
    }
    
    
    
//    void updateGameStateInRenderMethod()
//    {
//        int bucketMovementX = 0;
//
//        // Berechne Bewegung des Eimers in X-richtung, 
//        // abhängig von touch und Tastendruck
//        f (Gdx.input.isTouched()) 
//        {
//            this.touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
//            this.camera.unproject(touchPos);
//            bucketMovementX = touchPos.x - 64 / 2 - this.bucket.x;
//        }
//        if (Gdx.input.isKeyPressed(Keys.LEFT))
//        {
//            bucketMovementX = - 400 * Gdx.graphics.getDeltaTime();
//        }
//        if (Gdx.input.isKeyPressed(Keys.RIGHT))
//        {
//            bucketMovementX = 400 * Gdx.graphics.getDeltaTime();
//        }
//
//        //Regentropfen bewegen lassen, zerstören und entfernen
//        Iterator<Rectangle> iter = this.raindrops.iterator();
//        while (iter.hasNext()) 
//        {
//            Rectangle raindrop = iter.next();
//
//            int raindropMovementY = -200 * Gdx.graphics.getDeltaTime();
//
//            if (raindrop.y + 64 + raindropMovementY < 0)
//            {
//                iter.remove();
//                // Tropfen wurde entfernt, gehe zurück
//                // zum Schleifenanfang
//                continue;
//            }
//
//            if (raindropWasCought(raindrop, raindropMovementY, bucket, bucketMovementX) 
//            {
//                this.dropsGathered++;
//                this.dropSound.play();
//                iter.remove();
//                // Tropfen wurde entfernt, gehe zurück
//                // zum Schleifenanfang
//                continue;
//            }
//
//            raindrop.y += raindropMovementY;
//
//        }
//
//        bucket.x += bucketMovementX;
//
//
//        // Hier kommt dann vermutlich das eigentliche Zeichnen...
//        // ...
//
//    }
}

OT: @Marco13 , ich würd mal gern in wissen… wie viel ist eigentlich package bytewelt; so drin? xD

OT: Gute Frage. Tatsächlich habe ich über eine ähnliche Frage kürzlich auch öfter nachgedacht. In „bytewelt“ nicht sooo viel, aber rein in bezug auf den Zeit/Entwicklungsaufwand stecken in „bytewelt“, „j*vaforum“, „stackoverflow“, „spotlight“ und all meinen anderen mehr oder weniger losen Snippetsammlungen sicher einige Personenjahre. Eigentlich schade, 95% davon vergammelt ungenutzt vor sich hin, aber es taugt zumindest als Fundgrube :smiley:

OT: wie wärs wenn du das mal als “Open Code Snippet Repository” oder OCSP irgendwo hochlädst? xD