How to avoid lag when using parallax effect in fullscreen ?

edited October 2017 in Library Questions

Hi,

I'm making a 2D game with, hopefully, side-scrolling. Following a discussion with Chrisir, I'm posting my lag issue here with a MCVE. I run the following code and when I add the parallax function in fullscreen, I get intense lag. When I run it at 960x540, its fine. My code is far from perfect at this stage so if you see anything to improve, please let me know.

//I wont use all of these for this MCVE, 
//but its usually what I import for Box2d and collisions.
import shiffman.box2d.*;
import org.jbox2d.common.*;
import org.jbox2d.dynamics.joints.*;
import org.jbox2d.collision.shapes.*;
import org.jbox2d.collision.shapes.Shape;
import org.jbox2d.common.*;
import org.jbox2d.dynamics.*;
import org.jbox2d.dynamics.contacts.*;

// A reference to our box2d world
Box2DProcessing box2d;

//Declaring my hero
Hero hero;

PImage img;
PVector vBG;

void setup() {
  //fullScreen();
  size(960, 540);

  // Initialize box2d physics and create the world
  box2d = new Box2DProcessing(this);
  box2d.createWorld();

  // We are setting a custom gravity
  box2d.setGravity(0, -0.0005);

  //Initializing hero
  hero = new Hero(88, 88, 20);

  img = loadImage("Starsinthesky.jpg");

  vBG = new PVector(0, 0);
}

void draw() {

  imageMode(CORNER);
  parallaxe(img, vBG, hero.velocity().x); 

  // Thru time
  box2d.step();

  //Show and move Hero
  hero.display();
  hero.mover();
  hero.boundary(); //This is what I call to limit the range of my here

}

Here is the parallax function :

void parallaxe(PImage img, PVector pos, float vit) {
  pos.sub(vit, 0, 0); 

  if(pos.x < width-img.width) {
    image(img, pos.x+img.width, pos.y);
  }

  if(pos.x > 0) {
    image(img, pos.x-img.width, pos.y);
  }

  if(pos.x < -img.width) {
    pos.x = width;
  }

  if (pos.x > width) {
    pos.x = width-img.width;
  }

  image(img, pos.x, pos.y);
}

And here is my movable hero class :

class Hero {
  Body body;
  float r;

  Hero(float x, float y, float r_) {
    r = r_;
    // This function puts the hero in the Box2d world
    makeBody(x, y, r);
    body.setUserData(this);
  }

  void display() {
    Vec2 pos = box2d.getBodyPixelCoord(body);
    pushMatrix();
    translate(pos.x, pos.y);
    fill(0, 44, 188);
    noStroke();
    ellipse(0, 0, r*2, r*2);
    popMatrix();
  }

  void mover() {
    Vec2 move = new Vec2(0, 0);
    Vec2 pos = body.getWorldCenter();

    if ((keyPressed == true) && ((key == 'w') || (key == 'W'))) {
      move.y += 100;
    }
    if ((keyPressed == true) && ((key == 's') || (key == 'S'))) {
      move.y -= 100;
    }
    if ((keyPressed == true) && ((key == 'a') || (key == 'A'))) {
      move.x -= 100;
    }
    if ((keyPressed == true) && ((key == 'd') || (key == 'D'))) {
      move.x += 100;
    }
    body.applyForce(move, pos);
  }

  Vec2 velocity() {
    return body.getLinearVelocity();
  }

  //Here is the method that would limit the movement of my hero if it worked.
  //Box2d coordinate system is in the middle of the screen.
  void boundary() {
    Vec2 pos = box2d.getBodyPixelCoord(body);
    if (pos.x < r) {
    pos.x = r;
    }
    if (pos.x > width-r) {
    pos.x = width-r;
    }
    if (pos.y < r) {
    pos.y = r;
    }
    if (pos.y > height-r) {
    pos.y = height-r;
    }
  }  

  // Here's our function that adds the particle to the Box2D world
  void makeBody(float x, float y, float r) {
    // Define a body
    BodyDef bd = new BodyDef();
    // Set its position
    bd.position = box2d.coordPixelsToWorld(x, y);
    bd.type = BodyType.DYNAMIC;
    body = box2d.createBody(bd);

    // Make the body's shape a circle
    CircleShape cs = new CircleShape();
    cs.m_radius = box2d.scalarPixelsToWorld(r);

    FixtureDef fd = new FixtureDef();
    fd.shape = cs;
    // Parameters that affect physics
    fd.density = 1;
    fd.friction = 0.01;
    fd.restitution = 0.3;

    // Attach fixture to body
    body.createFixture(fd);    
  }

}

I hope you won't mind that it uses Box2D, its part of the game I make. I'm not sure if this is a lag factor. At the moment, I have no guess as to what causes this. Any hint is welcomed.

Answers

  • parallaxe :

    you are using image(img.... three times - better set values and use image() only one. It could be it’s causing a lag (drawing image three times).

    Also in mover: if keypressed should be checked only once and then inside a long section the ifs for the keys like wasd; don’t use Upper Case like W

  • display()

    just use ellipse (pos.x,pos.y,......

    no pushMatrix no translate no popMatrix

  • You don’t need to Pass img as a Parameter , it’s a global variable

    In boundary you can use else if twice

  • istr something about drawing off the left and right of the screen being strangely slow...

    copy() might be better, copying only a screen's worth of pixels, not drawing outside. (textures might be possible as well)

    how big is the image?

  • edited October 2017

    Thank you all for the replies.

    @Chrisir The role of parallaxe() is to place an image left and right for when the player moves or else the background is not drawn anymore on the sides hence the call three times to image() (once for each side and once for the middle). Maybe I havent fully understood your point, but when I test it, three calls are needed. How would you write the keypressed segment?

    @koogs Originally, the background was 1920x1080. How would you make sure the player always has a background with say copy() ? Even if he moves fast and the background moves outside his range (left or right) ...

    I'm also wondering why it lags in fullscreen and not in "halfscreen".

  • edited October 2017

    You can optimize in general -- reduce number of buffers, reduce pixels, cache, tile things in and out... but have you tried adding millis() logging to your draw cycles to see which steps specifically are taking the most time?

  • Hi jeremy, I haven't used the millis() technique yet. I've put this code in draw() :

          if (frameCount % 60 == 0) {
            println(frameRate);
          }
    

    and could see the difference in frame rates. It drops as soon as I introduce player-dependant parallax. In fullscreen, without parallax, its a steady 60 fps. As soon as the parallax kicks in, it drops to as low as 10 fps. The same setup with parallax, in 960x540, runs at 42-60 fps. If I run the code with a constant background speed, in fullscreen, it stays at 59 fps.

    The parallaxe() function moves the background and calls image() three times to make sure the player has a background left/middle/right.

    So in this line parallaxe(img, vBG, hero.velocity().x);, the tricky part to run is hero.velocity().x.

    I have a gamer graphic card (GTX 750 Ti), not top but not bad at all.

  • Have you tried the hero without the library

  • @Chrisir good idea. I've just run the same code without box2d, with processing vectors instead, and I got 11-59 fps (fullscreen). I was ready to drop box2d but oh well...

  • ok, i've added some debug and run that now and i can see it was doing what i thought it was doing

    you have an image:

    landscape

    and you are drawing it twice (sometimes), to ensure you fill the screen (your player is in the red bit, the visible screen):

    landscape2

    this is obviously a waste.

    but you don't need to draw the entire image both times - you can copy sections. so you'd copy() the left of the original to the right of the visible screen, and the right of the original to the left of the visible. that way you aren't copying pixels that you'll never see. (copy() (without resize) is also more efficient than image() because it doesn't care about the mode you're in)

    landscape4

    (possibly easier is creating a tileable pgraphic that's twice the size of the visible screen and copy a screen-sized part of that to the visible screen every frame. use modulo operator to find the top left...

    copying 1920x1080 is going to take a while regardless - its a lot of data to shift.

  • Ah, I finally understood why you draw the same image twice/three times

    You are stitching the images together

  • edited October 2017

    @koogs Thanks, I see how to do it with copy() now but do you think this technique could bring the game back to 60fps ? The lag appears when I introduce the player's speed in the parallaxe() function so not sure how it relates to that. At constant scrolling speed, I have no lag with 2 backgrounds.

  • Answer ✓

    that has reminded me of something...

    PImage img;
    
    void setup() {
      size(1920, 1080);
      img = loadImage("landscape_big.jpg"); // 1920x1080
      frameRate(100);
    }
    
    void draw() {
      if (frameCount % 60 == 0) {
        println(frameRate);
      }
    
      // A
      image(img, -100, 0);
      image(img, width - 100, 0);
    
      // B
    //  image(img, -100.5, 0);
    //  image(img, width - 100.5, 0);
    }
    

    try this, using your same image... first with A uncommented, then with B uncommented (but A disabled)...

  • Answer ✓

    (you can apply this to your code by casting both x and y args to int in lines 5, 9 and 20 or parallaxe())

  • @koogs This is strange. The fps is much higher for A then for B yet I don't understand why xD So integer values of scrolling would do it?

    Just tested it and yes, behold, lag is gone. Fps is back up in fullscreen. Thanks a lot! You just cleared a mysterious roadblock right there. Now, how does this works?

  • My guess is that with fractional pixels it's doing something like a resize - each target pixel is filled with a blend of the two (or more?) source pixels that would contribute to the target pixel colour, which is a lot of work. Whereas with integer pixels the mapping is 1:1 and it's just a straight copy.

    (So I think copy () would've been faster because it'd be using integers by default. And I still think you shouldn't bother with the off screen pixels. But maybe the image call is cropping for you...)

  • Do you think this integer approach is universal?

    Then we should cast to int before all image and also point / line..... commands?

  • not sure it matters as much with lines, there won't be 1920x1080 pixels affected for one thing 8)

    plus you generally want some level of anti-aliasing or they will look steppy.

  • a tiny test using a 2 pixel image, one black and one white, at a non-integer position suggests that there's no anti-aliasing of images - you get two pixels of the original colours displayed at (1, 1)

    PImage img;
    
    void setup() {
      size(50, 50);
      img = loadImage("pixels.png");
      image(img, 1.5, 1.5);
    }
    

    conclusion: ???

  • edited October 2017

    Thanks for all the energy you guys have spent on this :)

Sign In or Register to comment.