Tile Based Map Parallax Scrolling

I’m in the process trying to add parallax scrolling to my game engine, however I cant quite get it working right, I’ve had results that seem to be close but there’s the problem with either the tiles drifting off screen or the tiles don’t offset properly.

I don’t set the Z of the layers manually the layers movement speed should be based on how wide the layer is relative to the actual stage width, I’ve read several forum posts on the subject and several articles on Parallax scrolling and this is what i have so far (with some messy code as its still in the process of being figured out properly). I’ve left in some commented out code that i experimented with

the Stage class chooses the layers to be rendered, the Scene class tells the screen class which sprite and position to render it at offset by the viewport camera.

I’ve been focusing on making it work along the X axis first so the Y axis code is very preliminary.

I hope my code is descriptive enough

Also some of the code that was originally in the Stage Class has been moved to Scene so if you’re wondering why there’s some duplicate code, that’s why

Also my code is inspired by the methods The Cherno and VanZeben (on youtube) use

sprites are currently 32x32

and any shifts by 5 is reflective of that size. (aka * or / 32)

and shiftX and shiftY are int == 5

also i forgot to mention

width and height = amount of tiles x and y
tilemap.getWidth and getHeight are also amount of tiles;

viewport width and height are in pixels

Edit:

I’m cleaning up my code and i’ll repost once done, i’m also re-looking over this reference material which is the formula for parallax i’m using

http://books.google.co.uk/books?id=7S4ZpTR0DGwC&lpg=PA234&ots=_YrKHjkv3Q&dq=efficient%20parallax%20scrolling%20algorithms%20java&pg=PA233#v=onepage&q=efficient%20parallax%20scrolling%20algorithms%20java&f=false

The Stage Class Render Code:

  public final void render(Screen screen) {
    final int camX = viewport.getCamX();
    final int camY = viewport.getCamY();

    final int x0 = camX >> scene.getShiftX();
    final int x1 = (camX + viewport.getWidth()) >> scene.getShiftX();
    final int y0 = camY >> scene.getShiftY();
    final int y1 = (camY + viewport.getHeight()) >> scene.getShiftY();
    
    final int xRange = x1+2;
    final int yRange = y1+2;
    
    int x, y;
    
   scene.renderBackground(screen, player);//, x0, y0, xRange, yRange);
   
   renderEntities(screen);

    for (y = y0; y < yRange; y++) {
      for (x = x0; x < xRange; x++) {
        scene.renderCollisionForegroundTile(screen, x, y);
        scene.renderForegroundTile(screen, x, y);
      }  
    }

    eventChangedRender(screen);
  }

The Scene Class Render Code:

  void renderBackground(Screen screen, Player player) {//, int y0, int x0, int xRange, int yRange) {
    final int camX = viewport.getCamX();
    final int camY = viewport.getCamY();
    
    final int x0 = camX >> shiftX;
    final int x1 = (camX + viewport.getWidth()) >> shiftX;
    final int y0 = camY >> shiftY;
    final int y1 = (camY + viewport.getHeight()) >> shiftY;
    
    final int xRange = x1+2;
    final int yRange = y1+2;
    
    int x, y;
    for (y = y0; y < yRange; y++) {
      for (x = x0; x < xRange; x++) {
        renderBackgroundTile(screen, player, x, y);
        renderCollisionTile(screen, x, y);
      }
    }
  }

  public final void renderBackgroundTile(Screen screen, Player player, int x, int y) {
    int i, spriteIndex;
    StageMap mapLayer;
    
    for (i = 0; i < backgroundLayers; i++) {
      mapLayer = mapLayers[i];
      
      double displacementX = viewport.getWidth() - pixelWidth-32;
      double displacementY = viewport.getHeight() - pixelHeight-32;
      
      double mapDisplacementX = viewport.getWidth() - (mapLayer.getWidth() << shiftX);
      double mapDisplacementY = viewport.getHeight() - (mapLayer.getHeight() << shiftY);
      
      int xOffset = (int)(viewport.getCamX() * (mapDisplacementX / displacementX));//(int)(player.getX() * (mapLayer.getDisplacementX() / displacementX));
      int yOffset = (int)(viewport.getCamY() * (mapDisplacementY / displacementY));
      
     // x = viewport.getCamX() * (viewport.getWidth() - mapLayers[i].getWidth()) / (viewport.getWidth() - pixelWidth);
      //xa = (xOffset >> shiftX);// ((x << shiftX) - (viewport.getCamX())) * (viewport.getWidth() - (mapLayers[i].getWidth()+64)) / (viewport.getWidth() - (pixelWidth+64)) >> shiftX;
      //ya = ((y >> shiftY) * (viewport.getHeight() - mapLayers[i].getHeight()) / (viewport.getHeight() - pixelHeight)) >> shiftY;
      
      spriteIndex = mapLayer.getSpriteID((xOffset-64 >> 5) + x, y);
      if (spriteIndex != voidSpriteIndex || i == 0) {
        
        //mapLayers[i].render(screen, viewport, sprites[spriteIndex], pixelWidth, pixelHeight, x << shiftY, y << shiftY, xOffset, 0);
        screen.renderSceneTile(viewport, sprites[spriteIndex], x << shiftY, y << shiftY, xOffset, yOffset);
        //if (!sprites[spriteIndex].hasTransparency()) break;
      }
    }
  }

Stage Map Render Code (Currently unused and rendering directly to the screen as I was previously before trying to implement Parallax)

  void render(Screen screen, Viewport viewport, Sprite sprite, int stageWidth, int stageHeight, int xPos, int yPos, int xOffset, int yOffset) {
     screen.renderSceneTile(viewport, sprite, xPos, yPos, xOffset, yOffset);
  }

Screen Render Code:

  public final void renderSceneTile(Viewport viewport, Sprite sprite, int xPos, int yPos, int xOffset, int yOffset) {
    // Viewport bounds can never be lower than 0 or beyond Screen Width or Height
    final int viewportLeft = (viewport.getLeft() < 0 ? 0 : viewport.getLeft());
    final int viewportTop = (viewport.getTop() < 0 ? 0 : viewport.getTop());
    final int viewportRight = (viewport.getRight() > width ? width : viewport.getRight());
    final int viewportBottom = (viewport.getBottom() > height ? height : viewport.getBottom());

    // xPos Starts at the viewport boundry
    xPos += viewportLeft;
    yPos += viewportTop;
    
    // Offset by Camera
    xPos -= viewport.getCamX(); //(viewport.getWidth()>>1) - (viewport.getCamX() * xOffset);
    yPos -= viewport.getCamY(); 
    
    xPos += xOffset; //(viewport.getWidth()>>1) - (viewport.getCamX() * xOffset);
    //yPos += yOffset; 
    
    // If Position + Width and Height of the Sprite will go outside Viewport then crop it by the difference
    final int spriteWidth = (xPos + sprite.getWidth() > viewportRight ? sprite.getWidth() - ((xPos + sprite.getWidth()) - viewportRight) : sprite.getWidth());
    final int spriteHeight = (yPos + sprite.getHeight() > viewportBottom ? sprite.getHeight() - ((yPos + sprite.getHeight()) - viewportBottom) : sprite.getHeight());
    
    // If Position starts outside of viewport then set a Starting position of the difference
    final int spriteStartPixelX = (xPos < viewportLeft ?  viewportLeft - xPos : 0);
    final int spriteStartPixelY = (yPos < viewportTop ?  viewportTop - yPos : 0);
    
    final int xPixelStart = spriteStartPixelX + xPos;
    final int yPixelStart = spriteStartPixelY + yPos;
    
    // x & y Correspond to Sprite Pixel, xPixel & yPixel Correspont do Screen Pixel
    int x, y, xPixel, yPixel;//, pixel, index;
    
    for (y = spriteStartPixelY, yPixel = yPixelStart; y < spriteHeight; y++, yPixel++) {
      for (x = spriteStartPixelX, xPixel = xPixelStart; x < spriteWidth; x++, xPixel++) {
         pixels[xPixel + yPixel * width] = sprite.getPixel(x, y);
      }
    }
  }

I’ve changed the code for rendering parallax to more match the code in the book.

It works as intended with very little performance drop, I’ll post the code finished code after I clean it up and optimize it for people to refer to it in future.

Here’s the finished code:

Scene variables: (absolute width and height of the stage)

    displacementX = viewport.getWidth() - pixelWidth;
    displacementY = viewport.getHeight() - pixelHeight;

Stage Map variables: (width and height of specific layer)

    displacementX = viewport.getWidth() - pixelWidth;
    displacementY = viewport.getHeight() - pixelHeight;

Stage Class (most peoples Level class)

  @Override
  public final void tick(long tickTimeNow) {
    this.tickTimeNow = tickTimeNow;
    eventChangedTick();
    
    //sound.tick();
    
    updateEntities();
    viewport.setCam(player.getCenterX() - (viewport.getWidth() >> 1), player.getCenterY() - (viewport.getHeight() >> 1));
    
    // Set the Parallax Offsets
    final int xOffset = (viewport.getWidth() >> 1) - Math.round(player.getX()) - scene.getTileWidth() + ((player.getWidth() >> 1) % scene.getTileWidth());
    final int yOffset = (viewport.getHeight() >> 1) - Math.round(player.getY()) - scene.getTileHeight() + ((player.getHeight() >> 1) % scene.getTileHeight());
    viewport.setScroll(xOffset, yOffset);
  }

  @Override
  public final void render(Screen screen) {
    // Render backgrounds with Parallax Scrolling
    scene.renderBackground(screen, viewport);

    final int camX = viewport.getCamX();
    final int camY = viewport.getCamY();

    // The first visible tile on the screen.
    final int xTileStart = camX >> scene.getShiftX();
    final int yTileStart = camY >> scene.getShiftY();

    // The amount of Visible tiles on the screen.
    final int xTileLast = (camX + viewport.getWidth()) >> scene.getShiftX();
    final int yTileLast = (camY + viewport.getHeight()) >> scene.getShiftY();

    // The amount of visible tiles ont he screen plus 2 to account for offsets.
    final int xRange = xTileLast + 2;
    final int yRange = yTileLast + 2;

    int x, y;    
    // Loop through the tile range and render the background Collision tiles that are visible
    for (y = yTileStart; y < yRange; y++) {
      for (x = xTileStart; x < xRange; x++) {
        scene.renderCollisionTile(screen, x, y);
      }
    }

    renderEntities(screen);

    // Loop through the tile range and render the foreground tiles that are visible
    for (y = yTileStart; y < yRange; y++) {
      for (x = xTileStart; x < xRange; x++) {
        scene.renderCollisionForegroundTile(screen, x, y);
      }
    }
    
    // Render foregrounds with Parallax Scrolling
    scene.renderForeground(screen, viewport);

    // Render Stage Name if Recently Changed
    eventChangedRender(screen);
  }

Scene Class:

  public boolean constrained = false;
  public final void renderBackground(Screen screen, Viewport viewport) {
    final int xOffset = (constrained ? Math.max(Math.min(viewport.getScrollX(), 0), displacementX) : viewport.getScrollX());
    final int yOffset = (constrained ? Math.max(Math.min(viewport.getScrollY(), 0), displacementY) : viewport.getScrollY());
    
    for (int i = 0; i < backgroundLayers; i++) {
      mapLayers[i].renderParallax(screen, viewport, sprites, xOffset, yOffset, displacementX, displacementY, i == 0);
    }
  }

MapLayer Code

  void renderParallax(Screen screen, Viewport viewport, Sprite[] sprites, int xOffset, int yOffset, int dx, int dy, boolean renderVoid) {
    final int xOffsetParallax = xOffset * displacementX / dx;
    final int yOffsetParallax = yOffset * displacementY / dy;

    final int shiftX = sprites[voidSpriteIndex].getShiftX();
    final int shiftY = sprites[voidSpriteIndex].getShiftY();

    // The first visible tile on the screen.
    final int xTileStart = (-xOffsetParallax) >> shiftX;
    final int yTileStart = (-yOffsetParallax) >> shiftY;

    // The amount of Visible tiles on the screen.
    final int xTileLast = xTileStart + (viewport.getWidth() >> shiftX);
    final int yTileLast = yTileStart + (viewport.getHeight() >> shiftY);

    // The amount of visible tiles ont he screen plus 2 to account for offsets.
    final int xRange = xTileLast + 2;
    final int yRange = yTileLast + 2;

    int x, y, spriteIndex;
    for (y = yTileStart; y < yRange; y++) {
      for (x = xTileStart; x < xRange; x++) {
        spriteIndex = getSpriteID(x, y);
        if ((!renderVoid && spriteIndex != voidSpriteIndex) || renderVoid) screen.renderSceneParallax(viewport, sprites[spriteIndex], x << shiftX, y << shiftY, xOffsetParallax, yOffsetParallax);
      }
    }
  }

Screen Class

public final void renderSceneParallax(Viewport viewport, Sprite sprite, int xPos, int yPos, int xOffset, int yOffset) {
    // Viewport bounds can never be lower than 0 or beyond Screen Width or Height
    final int viewportLeft = (viewport.getLeft() < 0 ? 0 : viewport.getLeft());
    final int viewportTop = (viewport.getTop() < 0 ? 0 : viewport.getTop());
    final int viewportRight = (viewport.getRight() > width ? width : viewport.getRight());
    final int viewportBottom = (viewport.getBottom() > height ? height : viewport.getBottom());

    // xPos Starts at the viewport boundry
    xPos += viewportLeft;
    yPos += viewportTop;
    
    // Offset by Layer Offset
    xPos += xOffset;
    yPos += yOffset;
    
    // If Position + Width and Height of the Sprite will go outside Viewport then crop it by the difference
    final int spriteWidth = (xPos + sprite.getWidth() > viewportRight ? sprite.getWidth() - ((xPos + sprite.getWidth()) - viewportRight) : sprite.getWidth());
    final int spriteHeight = (yPos + sprite.getHeight() > viewportBottom ? sprite.getHeight() - ((yPos + sprite.getHeight()) - viewportBottom) : sprite.getHeight());
    
    // If Position starts outside of viewport then set a Starting position of the difference to crop
    final int spriteStartPixelX = (xPos < viewportLeft ?  viewportLeft - xPos : 0);
    final int spriteStartPixelY = (yPos < viewportTop ?  viewportTop - yPos : 0);
    
    // Starting x and y Pixels on the Screen
    final int xPixelStart = spriteStartPixelX + xPos;
    final int yPixelStart = spriteStartPixelY + yPos;
    
    // x & y Correspond to Sprite Pixel, xPixel & yPixel Correspont do Screen Pixel
    int x, y, xPixel, yPixel;
    
    for (y = spriteStartPixelY, yPixel = yPixelStart; y < spriteHeight; y++, yPixel++) {
      for (x = spriteStartPixelX, xPixel = xPixelStart; x < spriteWidth; x++, xPixel++) {
         pixels[xPixel + yPixel * width] = sprite.getPixel(x, y);
      }
    }
  }