Per-pixel collision with arrays

I'm fairly new to processing but I've been progressing well with learning how to use it. I've been working on a simple flappy bird esque game and what I want to do is to have the bird and "pipes" collisions be recognized on a pixel-level, ive seen Peter Lagers code for pp_collision but i can't seem to get it to work for the obstacles. can someone tell me how to incorporate the classes into pp_collision(PImage imgA, float aix, float aiy, PImage imgB, float bix, float biy)

for souls https://i.imgur.com/PZm7ivN.png and https://i.imgur.com/wvOkeEZ.png . I would love to know how to get collisions on a pixel level for animated sprites (ie. the souls array) as well but its secondary which is why i commented its code out.

//int numFrames = 2;  // The number of frames in the animation
//int currentFrame = 0;
//PImage[] souls = new PImage[numFrames];


int gameState; //0 = startscreen, 1 = in-game, 2 = game over, 3 = restart-screen
int o = 240;


Ghost ghost;
Obstacle[] obstacles = new Obstacle[2];
Score score;
// Game states
boolean gameStarted = false;
boolean gameOver = false;
boolean gameRestart = false;


void setup() {
  //frameRate(30);
  fullScreen(FX2D);
  //size(1280, 720, FX2D);

  ghost = new Ghost(width/2, height/2);
  obstacles[0] = new Obstacle(width, random(100, height-100));
  obstacles[1] = new Obstacle(width*1.5+25, random(100, height-100));
  score = new Score();

  //startTimer = new Timer (0);

  //for (int k = 0; k < numFrames; k++) {
   // String imageName = "Soul_" + nf(k, 2) + ".png";
   // souls[k] = loadImage(imageName);
  //}
}


void draw() {
  background(175); 

  if (gameState==0) {
    gameRestart = true;
    drawMenuScreen();
    score.highscores();
    //startTimer.countUp();
    //fill (255);
    //text (startTimer.getTime(), 60, 60);
  } 
  if (gameState==0 && mousePressed) {
    if (gameRestart == true)
      //timerReset();
    score.reset();
    gameState = 1;
  }


  if (gameState==1) { 
    gameStarted = true;
    gameRestart = false;
    ghost.draw();
    for (Obstacle o : obstacles) { 
      o.draw();
      //Souls();
    }
    score.draw();
    detectCollision();

    if (gameState==1 && mousePressed) {
      //startTimer.countUp();
      //fill (255);
      //text (startTimer.getTime(), 60, 60);
    }
    if (gameState==1 && mousePressed) {
      ghost.jump();
    }
  }


  if (gameState==2) {
    //startTimer.countUp();
    //fill (255);
    //text (startTimer.getTime(), 60, 60);
    gameStarted = false;
    gameRestart = false;
    drawGameOver();
    ghost.reset();
    for (Obstacle obs : obstacles) { 
      obs.reset();
    }
  }
  //if (gameState==2 && startTimer.getTime()>=3.5) {
  if (gameState==2 && mousePressed) {
    if (gameStarted == false && gameRestart == false);
    //timerReset();
    gameState=3;
  }


  if (gameState==3) {
    gameRestart = true;
    drawMenuScreen();
    score.highscores();
  } 
  if (gameState==3 && mousePressed) {  
    if (gameRestart == true)
      score.reset();
    gameState = 1;
  }
}






 class Score {
  private int score = 0;
  private int highscore;
  private boolean scoreIncreased = false;

// Methods for increasing scores. If score is NOT increased

  void highscores(){
    if (score>highscore)
      highscore=score;
      else if (gameState==0 || gameState==3) {
      textAlign(CENTER);
      fill(255);
      textSize(width/60);
      text("HIGHSCORE: " + highscore, width/2, height/2 + height/8.25);
    }  
  }

  void increase() {
    if (!scoreIncreased) {
      score += 1;
      scoreIncreased = true;
    }
  }

  void reset() {
    score = 0;
    scoreIncreased = false;
  }

  void allowScoreIncrease() {
    scoreIncreased = false;
  }


  void draw() {
    pushStyle();
    if (gameState==2) {
      textAlign(CENTER);
      fill(0);
      textSize(width/60);
      text("SCORE: " + score, width/2, height/2 + 65);
    } 
      else if (gameState==1) {
      //rectMode(CORNER);
      textAlign(CENTER);
      fill(255);
      textSize(width/60);
      text("Score: " + score, width/2, 40);
    }
    popStyle();
  }
}



 class Obstacle {
  float initX;
  float topX;
  float topY;
  float w = 120; // original was 50
  PImage obstacle1, obstacle2;

  Obstacle(float initialTopX, float initialTopY) {
    initX = initialTopX;
    topX = initialTopX;
    topY = initialTopY;
    obstacle1 = loadImage("https://" + "i.imgur.com/9Dnn4sI.png");
    obstacle2 = loadImage("https://" + "i.imgur.com/d83OfMi.png");
  }


  void draw() {
    pushStyle();
    imageMode(CORNERS);
    image(obstacle1, topX, topY, topX+w, height-1);
    image(obstacle2, topX, 0, topX+w, topY - 180);
    popStyle();

     // Controls speed of object x movements (namely for obstacles)
    // topX -= 4.25;
    topX -= 9.5;
  }

  void reset() {
    topX = initX;
    topY = random(100, height-100);
  }

  void reposition() {
    topX = width;
    topY = random(100, height-100);
  }
 } 




class Ghost {
  float x;
  float y;
  float size = 85;
  float vy = 0;
  float ay = 0.63;
  PImage ghost;

  Ghost(float initialX, float initialY) {
    x = initialX;
    y = initialY;
    ghost = loadImage("https://" + "i.imgur.com/GPRyMO7.png");
  }

  void draw() {
    pushStyle();
    imageMode(CENTER);
    image(ghost, x, y, size, size);
    popStyle();


    y += vy;
    vy += ay;
  }

  void reset() {
    y = 200;
    vy = 0;
  }

  void jump() {
    vy = -9.5;
  }
}






// void Souls(){   
//   currentFrame = (currentFrame+1) % numFrames;  // Use % to cycle through frames
//   image(souls[(currentFrame) % numFrames], width/2, height/2);
//}




void drawGameOver() {
  pushStyle();
  fill(200, 200, 200, 200);
  noStroke();
  rect(-20, -20, width + 40, height + 40);
  score.draw ();
  popStyle();
}

void drawMenuScreen() {
  fill(0);
  noStroke();
  rect(-20, -20, width + 40, height + 40);
  ;
}



void detectCollision() {
  // Did the ghost come out of the screen?
  if (ghost.y > height || ghost.y < 0) {
    gameState=2;
  }

  for (Obstacle obstacle : obstacles) {
    if (ghost.x - ghost.size/2.0 > obstacle.topX + obstacle.w) {
      score.increase();
    }

    if (obstacle.topX + obstacle.w < 0) {
      obstacle.reposition();
      score.allowScoreIncrease();
    }

    //if (obstacle.detectCollision(ghost)) {
      //gameState=2;
    //}
  }
}

Can you help with incorporating the collision detection method below to the work on the ghost class and the obstacles array in my code

  obstacle2 = loadImage("up2.png");
  obstacle1x = (width - obstacle1.width)/2;
  obstacle1y = (height/3 - obstacle1.height)/2;
//  obstacle2x = (width - obstacle2.width)/4;
//  obstacle2y = (height - obstacle2.height)/4;
//    f2x = (width - ghost.width)/2;
//      f2y = (height - ghost.height)/2;
}

void draw(){
  background(0);
  image(obstacle1,obstacle1x,obstacle1y);
  obstacle1x = obstacle1x-2;
  if (obstacle1x <= -150){
    obstacle1x = 900;}
  image(ghost,mouseX,mouseY);
  if(pp_collision(obstacle1,obstacle1x,obstacle1y,ghost,mouseX,mouseY)){
    stroke(255,64,64);
    strokeWeight(1);
    noFill();
    rect(obstacle1x,obstacle1y,obstacle1.width,obstacle1.height);
    rect(mouseX,mouseY,ghost.width,ghost.height);
  }
}

final int ALPHALEVEL = 20;

boolean pp_collision(PImage imgA, float aix, float aiy, PImage imgB, float bix, float biy) {
  int topA, botA, leftA, rightA;
  int topB, botB, leftB, rightB;
  int topO, botO, leftO, rightO;
  int ax, ay;
  int bx, by;
  int APx, APy, ASx, ASy;
  int BPx, BPy; //, BSx, BSy;

  topA   = (int) aiy;
  botA   = (int) aiy + imgA.height;
  leftA  = (int) aix;
  rightA = (int) aix + imgA.width;
  topB   = (int) biy;
  botB   = (int) biy + imgB.height;
  leftB  = (int) bix;
  rightB = (int) bix + imgB.width;

  if (botA <= topB  || botB <= topA || rightA <= leftB || rightB <= leftA)
    return false;

  // If we get here, we know that there is an overlap
  // So we work out where the sides of the overlap are
  leftO = (leftA < leftB) ? leftB : leftA;
  rightO = (rightA > rightB) ? rightB : rightA;
  botO = (botA > botB) ? botB : botA;
  topO = (topA < topB) ? topB : topA;


  // P is the top-left, S is the bottom-right of the overlap
  APx = leftO-leftA;   
  APy = topO-topA;
  ASx = rightO-leftA;  
  ASy = botO-topA-1;
  BPx = leftO-leftB;   
  BPy = topO-topB;

  int widthO = rightO - leftO;
  boolean foundCollision = false;

  // Images to test
  imgA.loadPixels();
  imgB.loadPixels();

  // These are widths in BYTES. They are used inside the loop
  //  to avoid the need to do the slow multiplications
  int surfaceWidthA = imgA.width;
  int surfaceWidthB = imgB.width;

  boolean pixelAtransparent = true;
  boolean pixelBtransparent = true;

  // Get start pixel positions
  int pA = (APy * surfaceWidthA) + APx;
  int pB = (BPy * surfaceWidthB) + BPx;

  ax = APx; 
  ay = APy;
  bx = BPx; 
  by = BPy;
  for (ay = APy; ay < ASy; ay++) {
    bx = BPx;
    for (ax = APx; ax < ASx; ax++) {
      pixelAtransparent = alpha(imgA.pixels[pA]) < ALPHALEVEL;
      pixelBtransparent = alpha(imgB.pixels[pB]) < ALPHALEVEL;

      if (!pixelAtransparent && !pixelBtransparent) {
        foundCollision = true;
        break;
      }
      pA ++;
      pB ++;
      bx++;
    }
    if (foundCollision) break;
    pA = pA + surfaceWidthA - widthO;
    pB = pB + surfaceWidthB - widthO;
    by++;
  }
  return foundCollision;
}

Answers

  • You could just use dist() - see reference

    You could for loop over the pipes (are they in an array?) and check each against the bird with if(dist(birdX,birdY,.....

  • edited May 2018

    Here's an example of some pixel based collision. Perhaps I should point out this way of doing it on the CPU is very slow and not really good for a commercial game, needs to be done in shaders.

    A solution to this might be to check for bounding box collisions, create a collision pgraphics that is only the size of the overlap (far less pixels), and draw your images there offset.

    Basically, you draw your image twice. Once to the screen, once tinted and transparent to a collision pgraphics. Then we check all the pixels in the collision pgraphics for pixels that contain both the bird(tinted to be only red) and the pipe(tinted to be only green).

    PGraphics pgCollision;
    color blue = color(0, 0, 255, 255),
          transparent = color(0,0,0,0);
    
    
    PImage pipe = loadImage("http://" + "flappycreator.com/default/tube2.png");
    PImage bird = loadImage("http://" + "flappycreator.com/default/bird_sing.png");
    
    void setup() {
      size(800, 800, P2D);
      pgCollision = createGraphics(width, height, P2D);
    }
    
    void draw() {
      clear();
    
      // draw tinted semi-transparent images
      pgCollision.beginDraw();
      pgCollision.clear();
      pgCollision.tint(255, 0, 0, 200); // draw bird as transparent red
      drawBird(pgCollision);
      pgCollision.tint(0, 255, 0, 200); // pipes as transparent green
      drawPipe(pgCollision);
      pgCollision.endDraw();
      noTint();
    
      // check all pixels for ones that contain both red and green
      pgCollision.loadPixels();
      for (int i = 0; i < width*height; i++) {
        color c = pgCollision.pixels[i];
        if ((red(c)>0)&&(green(c)>0))
          pgCollision.pixels[i] = blue; // collision detected
          else pgCollision.pixels[i] = transparent; // optional, slow
      }
      pgCollision.updatePixels();  
    
      drawPipe(g);
      drawBird(g);
      image(pgCollision, 0, 0);
    }
    
    void drawPipe(PGraphics pg) {
      pg.image(pipe, 200, 600);
    }
    
    void drawBird(PGraphics pg) {
      pg.image(bird, mouseX, mouseY);
    }
    
  • edited May 2018

    .

  • edited May 2018

    Thanks for the link Chrisir, im still reading.. "You could for loop over the pipes (are they in an array?) and check each against the bird with if(dist(birdX,birdY,....." - yes theyre' in an array, i just updated my question (new to this lol)

    Hey lmccandless I tried running your code example and it would'nt run at all even after replacing the loadImage lines, am I doing something wrong?

  • am I doing something wrong?

    did you copy the 2 images into your sketch's data folder ?

  • edited May 2018

    Yes i did, but it gives a java.lang.reflect.invocationtargetexception when I click run.

  • I dunno

    Line number? Other error?

  • edited May 2018

    No line errors specified as it runs but then crashes? i guess. The console shows:

    The sketch path is not set. java.lang.RuntimeException: Files must be loaded inside setup() or after it has been called. at processing.core.PApplet.createInputRaw(PApplet.java:7030) at processing.core.PApplet.createInput(PApplet.java:7002) at processing.core.PApplet.loadBytes(PApplet.java:7292) at processing.core.PApplet.loadImage(PApplet.java:5428) at processing.core.PApplet.loadImage(PApplet.java:5350) at aPIXELCOLLISIONS.(aPIXELCOLLISIONS.java:23) at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at processing.core.PApplet.runSketch(PApplet.java:10691) at processing.core.PApplet.main(PApplet.java:10467) The sketch path is not set. java.lang.RuntimeException: java.lang.reflect.InvocationTargetException at processing.core.PApplet.runSketch(PApplet.java:10697) at processing.core.PApplet.main(PApplet.java:10467) Caused by: java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at processing.core.PApplet.runSketch(PApplet.java:10691) ... 1 more

    etc

  • Did you save the sketch?

    data folder with images (short path!!) OR use the full html url

  • edited May 2018

    100% sure its saved, everythings saved in C:\Users\AAA\Documents\Processing\aPIXELCOLLISIONS\data.

    edit i just tried it line for line on openprocessing and got an exception (clear is not defined)...not sure whats the problem here if it works for you both

  • edited May 2018

    Try saving it, put your images in the same folder, replacing the image links with your own images.

    Also I edited my first post with a fix for the formatting

  • edited May 2018

    Thanks for the reply Imccandless, i just copied the code you edited in without saving or editing anything and it works. HOWEVER, If I edit these lines like this (or use any images from my data folder or any other folder)

        PImage pipe = loadImage("tube2.png");
        PImage bird = loadImage("bird_sing.png");
    

    it doesn't work and gives the exception previously stated

  • Did you publish your latest code? There is neither class Score nor Timer defined.

    The error could be because you are loading resources from your data folder outside the scope of setup or draw.

    Kf

  • If it works, I am happy

  • Make sure your image names exactly match(case included) and put them in the same folder as the sketch

  • edited May 2018

    Did you publish your latest code? There is neither class Score nor Timer defined.

    hi kfrajer I just updated it. no collisions yet.

    The error could be because you are loading resources from your data folder outside the scope of setup or draw.

    thats what the error sugests but i just don't know how that could be the case. it loads the net link fine but once I call the images from the data folder or any other folder it just errors out. It mentions something about 'The sketch path is not set.' in the error but I'm just not sure how to correct this.

    Make sure your image names exactly match(case included) and put them in the same folder as the sketch

    That's the thing, it is. I even tried different folders and creating new sketches, only works with the net link you gave

  • Ok, you need to take the code you provide above and merge it into a single tab, the main tab. Then, using the code you provided, review it and make sure it runs as I can still find some pieces missing. Either you reduce your code to an MCVE or you make sure the code you provide runs. Please keep in mind we do not have your images so you can provide some information about your images and reduce the number of images used to a minimum. If you can access the images via urls, please provide them.

    Kf

  • edited May 2018 Answer ✓

    Took the code a lot further, it runs multiple pipes and the collision pgraphic is minimally sized for speed.

    For me, I go from 1000fps to ~600fps when checking for collision;

    FX2D runs much faster but is more limited, holds at 1000fps for me. To convert replace P2D with FX2D on line 12 and remove the P2D 3rd argument entirely on line 53.

    color blue = color(0, 0, 255, 255), 
      transparent = color(0, 0, 0, 0);
    
    PImage pipe = loadImage("http://" + "flappycreator.com/default/tube2.png");
    PImage bird = loadImage("http://" + "flappycreator.com/default/bird_sing.png");
    
    Sprite sbird;
    ArrayList<Sprite> spipes;
    float fps = 0;
    
    void setup() {
      size(800, 800, P2D);
      //bird.resize((int)(bird.width*2), ((int)bird.height*2));
      spipes = new ArrayList<Sprite>();
      for (int i = 0; i < 6; i++) {
        Sprite sPipe = new Sprite(pipe, color(255, 0, 0, 200));
        sPipe.loc = new PVector(i*120+50, 500+random(250));
        spipes.add(sPipe);
      }
      sbird = new Sprite(bird, color(0, 255, 0, 200));
      frameRate(1000);
      noCursor();
    }
    
    void draw() {
      clear();
      sbird.loc = new PVector(mouseX,mouseY);
      sbird.draw();
      for (Sprite s : spipes) {
        s.draw();
        PixelCollision check = sbird.pixelCollision(s);
        if (check!=null) {
          image(check.pg, check.loc.x, check.loc.y);
          stroke(0,255,0,255);
          noFill();
          rect( check.loc.x, check.loc.y, check.pg.width, check.pg.height);
          noStroke();
          stroke(255);
          fill(255,0,255,255);
          ellipse(check.hit.x, check.hit.y, 5, 5);
        }
      }
      fill(255);
      fps = (fps*119 + frameRate)/120.0;
      text((int)fps + " fps", 10, 10);
    }
    
    class PixelCollision {
      PGraphics pg;
      PVector loc;
      PVector hit;
      PixelCollision(int w, int h, PVector nloc) {
        pg = createGraphics(w, h, P2D);
        loc = nloc.copy();
      }
    }
    
    class Sprite {
      PImage img;
      PVector loc = new PVector(0, 0);
      color tint;
      Sprite(PImage pimg, color colTint) {
        img = pimg;
        tint = colTint;
      }
    
      void draw() {
        draw(g);
      }
    
     boolean overlap(Sprite s) {
        if ((loc.x < s.loc.x+s.img.width) && (loc.x+img.width > s.loc.x) &&
          (loc.y < s.loc.y+s.img.height) && (loc.y+img.height > s.loc.y)) {
          return true;
        }
        return false;
      }
    
      void draw(PGraphics pg) {
        pg.pushMatrix();
        pg.translate(loc.x, loc.y);
        pg.image(img, 0, 0);
        //pg.rect(0, 0, bird.width, bird.height);
        pg.popMatrix();
      }
    
      PixelCollision pixelCollision(Sprite s) {
        PixelCollision pc = null;
        if (overlap(s)) {
    
          float x = max(loc.x, s.loc.x);
          float y = max(loc.y, s.loc.y);
          float w =  min(loc.x+img.width, s.loc.x+s.img.width);
          float h =  min(loc.y+img.height, s.loc.y+s.img.height);
    
          pc = new PixelCollision( ceil(w-x), ceil(h-y), new PVector(x, y));
          PGraphics pg = pc.pg;
          pg.beginDraw();
          pg.clear();
    
          pg.tint(tint);
          pg.image(img, -(x-loc.x), -(y-loc.y));
    
          pg.tint(s.tint);
          pg.image(s.img, -(x-s.loc.x), -(y-s.loc.y));
    
          pg.endDraw();
          pg.loadPixels();
          int i = 0;
          for (; i < pg.width*pg.height-1; i++) {
            color c = pg.pixels[i];
            if ((red(c)>0)&&(green(c)>0)) {
              //pg.pixels[i] = blue;
              break;
            }
          }
          if (i== pg.width*pg.height-1) pc.hit = null;
          else pc.hit = new PVector(x+ (i%pg.width), y + floor((i-(i%pg.width))/pg.width));
          /* for (; i < pg.width*pg.height; i++) { // uncomment this and above to show collisions, slows code
           color c = pg.pixels[i];
           if ((red(c)>0)&&(green(c)>0)) {
           pg.pixels[i] = blue;
           }
           }
           pg.updatePixels(); */
        }
        if ((pc != null) && (pc.hit == null)) return null;
        return pc;
      }
    }
    
  • edited May 2018

    Ok, you need to take the code you provide above and merge it into a single tab, the main tab. Then, using the code you provided, review it and make sure it runs as I can still find some pieces missing. Either you reduce your code to an MCVE or you make sure the code you provide runs. Please keep in mind we do not have your images so you can provide some information about your images and reduce the number of images used to a minimum. If you can access the images via urls, please provide them.

    Kf

    Got it kfrajer , * UPDATED* so I simplified it a bit leaving just the essential parts to emphasize what im trying to do. Id like to detect collision between the ghost and the pillars but neglecting the transparencies that is. As the obstacles are arrays that're linked i dont know how to do this effectively

    Took the code a lot further, it runs multiple pipes and the collision pgraphic is minimally sized for speed.

    Excellent, Thanks Imccandles your code is exactly what i may be thinking about, However, there's only one issue, I can't use any stored images with it for whatever reason.

    edit: I got it to work this time loading the PImages from the data folder in setup...doing that didn't work with the previous code...but it works now smh.

Sign In or Register to comment.