How to make something like google searchplayground?

This is my attempt: https://mini-game-frontend-psi.vercel.app/ My photo is 8000x4000 pixels, and my problem is that I don’t understand how to adapt it to different screen sizes.

I thought setting the initial zoom above 1 was a bad idea, but even with that, on a large screen, the photo can’t fill the entire screen space, and the background will be visible.

Ideally, the camera should be able to scroll horizontally but not vertically.

So my question is: How do I properly scale the image so that the zoom level looks approximately the same across all devices?

this is game code:

import Phaser, { Scene } from 'phaser';
import Pinch from 'phaser3-rex-plugins/plugins/input/gestures/pinch/Pinch';

import { EventBus } from '../event-bus';
import { setupAudioToggleListener } from '../events/events-toggle-audio';
import { handlePointerMove } from '../handlers/handle-pointer-move';
import { handleWheel, setupZoomControlsListener } from '../handlers/handle-wheel';

export class Game extends Scene {
  chunkWidth: number;
  chunkHeight: number;
  chunksInRow: number;
  chunksInCol: number;
  loadedChunks: Record<string, boolean>;
  previousCameraState: { scrollX: number; scrollY: number; zoom: number };
  rexGestures: unknown;
  backgroundMusic: Phaser.Sound.BaseSound | undefined;
  promoClickSound: Phaser.Sound.BaseSound | undefined;
  promoMissClickSound: Phaser.Sound.BaseSound | undefined;

  constructor() {
    super('Game');

    const aspectRatio = 1;

    this.chunkHeight = window.innerHeight / 5;

    this.chunkWidth = this.chunkHeight * aspectRatio;

    this.chunksInRow = 10;
    this.chunksInCol = 5;
    this.loadedChunks = {};
    this.previousCameraState = { scrollX: 0, scrollY: 0, zoom: 1 };
    this.backgroundMusic = undefined;
    this.promoClickSound = undefined;
    this.promoMissClickSound = undefined;

    console.log(this.chunkHeight);
    console.log(this.chunkWidth);
  }

  preload() {
    this.load.audio('overture', ['assets/game/audio/Overture.ogg']);
    this.load.audio('promo-clicked', ['assets/game/audio/promo-click.mp3']);
    this.load.audio('promo-miss-clicked', ['assets/game/audio/promo-miss.mp3']);
  }

  create() {
    const camera = this.cameras.main;
    camera.setZoom(1.4);
    camera.setBounds(0, 0, this.chunkWidth * this.chunksInRow, this.chunkHeight * this.chunksInCol);
    camera.setZoom(Phaser.Math.Clamp(camera.zoom, 0.2, 5));
    this.input.on(
      'pointermove',
      (pointer: Phaser.Input.Pointer) => {
        console.log(pointer.event);

        handlePointerMove(camera, pointer);
      },
      this
    );

    this.input.on('wheel', (pointer, gameObject, deltaX: number, deltaY: number) => {
      handleWheel(camera, deltaX, deltaY);
    });

    this.loadVisibleChunks();

    this.backgroundMusic = this.sound.add('overture', { loop: true, volume: 0.1 });
    this.promoClickSound = this.sound.add('promo-clicked', { volume: 1 });
    this.promoMissClickSound = this.sound.add('promo-miss-clicked', { volume: 1 });
    this.backgroundMusic.play();

    setupAudioToggleListener(this);
    setupZoomControlsListener(camera);

    const pinch = this.rexGestures.add.pinch();

    pinch.on(
      'pinch',
      (pinch: Pinch) => {
        const scaleFactor = pinch.scaleFactor;
        camera.zoom *= scaleFactor;
      },
      this
    );

    this.createPromoCodes();

    EventBus.emit('current-scene-ready', this);
  }

  update() {
    const camera = this.cameras.main;

    if (
      camera.scrollX !== this.previousCameraState.scrollX ||
      camera.scrollY !== this.previousCameraState.scrollY ||
      camera.zoom !== this.previousCameraState.zoom
    ) {
      this.previousCameraState.scrollX = camera.scrollX;
      this.previousCameraState.scrollY = camera.scrollY;
      this.previousCameraState.zoom = camera.zoom;

      this.loadVisibleChunks();
    }
  }

  loadVisibleChunks() {
    this.load.setPath('assets/game/map-chunks');

    for (let y = 0; y < this.chunksInCol; y++) {
      for (let x = 0; x < this.chunksInRow; x++) {
        const chunkKey = `chunk_${y}_${x}`;
        this.load.image(chunkKey, `${chunkKey}.png`);
      }
    }

    this.load.once('complete', () => {
      for (let y = 0; y < this.chunksInCol; y++) {
        for (let x = 0; x < this.chunksInRow; x++) {
          const chunkKey = `chunk_${y}_${x}`;

          const image = this.add
            .image(x * this.chunkWidth, y * this.chunkHeight, chunkKey)
            .setOrigin(0);

          image.setScale(this.chunkWidth / image.width, this.chunkHeight / image.height);
        }
      }
    });

    this.load.start();
  }

  createPromoCodes() {
    this.promocodes = this.add.group();

    const promoLayer = this.add.layer();

    const background = this.add.rectangle(
      0,
      0,
      this.chunkWidth * this.chunksInRow,
      this.chunkHeight * this.chunksInCol,
      0x000000
    );

    background.setOrigin(0, 0);
    background.setAlpha(0.5);
    background.setInteractive();

    background.on('pointerdown', (pointer) => {
      console.log(pointer.event);

      pointer.event.stopPropagation();
      this.promoMissClickSound?.play();
    });

    for (let i = 0; i < 512; i++) {
      const x = Phaser.Math.Between(0, this.chunkWidth * this.chunksInRow);
      const y = Phaser.Math.Between(0, this.chunkHeight * this.chunksInCol);

      const width = Phaser.Math.Between(10, 50);
      const height = Phaser.Math.Between(5, 25);

      const promoCode = this.add.circle(x, y, width / 10, 0x00ff00);
      promoLayer.add(promoCode);

      promoCode.setInteractive();
      promoCode.on('pointerdown', () => {
        this.promoClickSound?.play();
        const code = 'PROMO' + Phaser.Math.Between(1000, 9999);
        EventBus.emit('promo-code-clicked', code);
      });
    }

    promoLayer.setDepth(1);
  }
}

:wave:

I would try doing it all with camera zoom and scroll. No varying chunk sizes, no image scaling.

You can check camera.dirty to see if zoom or scroll have changed.

Is loadVisibleChunks() via update() duplicating chunk images?

Thanks for the answer, but I meant something a little different.
I have one big picture and so that the user does not have to load the entire image at once, it was decided to split this big image into several small ones and load them depending on the camera position.

Your solution with zoom will be useful to me. But still, in your example, you load one big picture at once.
I see that there is a grid overlay, but in fact it does nothing.

What problem are you facing?

Calculating the correct size of one chunk.
For example, I can divide an 8000x4000 image into 12 columns and 5 rows, then the size of one chunk will be 600x800.
But then on a screen > 8000px, the chunks simply won’t fit, which means they need to be scaled somehow depending on the size of the device. + you need to take into account that mobile devices and PCs should have the same zoom and the ability to see the entire picture if you zoom out to max zoom.
And also on this map there will be objects that are tied to certain objects on the map (not physical)
Maybe you have an example of such an implementation?

I don’t think you need to scale the image chunks. You only need to zoom the camera to fit. That’s what the example is doing.

Also, I think 8 2000x2000 textures will work better than 60 smaller ones.

1 Like