fenomas / noa

Experimental voxel game engine.
MIT License
611 stars 87 forks source link

Any tips for how to generate a minimap client-side? #117

Closed Jarred-Sumner closed 4 years ago

Jarred-Sumner commented 4 years ago

I've been trying to add a minimap to the game for the past few days. My current setup is as follows:

Is this...a reasonable way to do it?

The problems I'm running into are:

Here's a more visual explanation. You can click on it to make the image bigger

Frame 92

This is what the code for actually drawing the chunk images looks like:

if (!this.position) {
  return;
}
this.ctx.fillStyle = "#ccc";
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);

const noa = this.loader.noa;
const {
  _renderPosition: renderPosition,
  position,
} = noa.entities.getPositionData(noa.playerEntity);

const cs = 32.0;

const globalX = position[0]; //renderPosition[0] + noa.worldOriginOffset[0];
const globalY = position[1]; //renderPosition[1] + noa.worldOriginOffset[1];
const globalZ = position[2]; //renderPosition[2] + noa.worldOriginOffset[2];

const currentChunkZ = globalZ / cs;
const currentChunkX = globalX / cs;

this.minChunkX = Math.floor(currentChunkX - MinimapRender.squareLength);
this.minChunkZ = Math.floor(currentChunkZ - MinimapRender.squareLength);
this.maxChunkX = Math.ceil(currentChunkX + MinimapRender.squareLength);
this.maxChunkZ = Math.ceil(currentChunkZ + MinimapRender.squareLength);

const translateX = ((globalX % cs) + cs) % cs;
const translateZ = ((globalZ % cs) + cs) % cs;

const { minChunkX, maxChunkX, minChunkZ, maxChunkZ } = this;

let chunkXOffset = -1;
let chunkZOffset = -1;

const drawXOffset = 0;
const drawYOffset = 0;

for (let chunkX = minChunkX; chunkX < maxChunkX; chunkX++) {
  chunkXOffset++;
  chunkZOffset = -1;

  for (let chunkZ = minChunkZ; chunkZ < maxChunkZ; chunkZ++) {
    chunkZOffset++;

    const imageData = this.worldMap.imageAt(
      Math.round(chunkX),
      Math.round(chunkZ)
    );

    if (!imageData) {
      continue;
    }

    let x = chunkXOffset * imageData.width - drawXOffset;
    let y = chunkZOffset * imageData.height - drawYOffset;

    this.ctx.putImageData(imageData, x, y);

    if (
      Math.floor(currentChunkZ) === Math.floor(chunkX) &&
      Math.floor(currentChunkZ) === Math.floor(chunkZ)
    ) {
      this.ctx.fillStyle = "pink";
      this.ctx.strokeStyle = "red";
      const r = 4;
      this.ctx.beginPath();
      this.ctx.arc(x, y, r, 0, 359);
      this.ctx.font = "10px Arial";
      this.ctx.strokeText(
        `${Math.floor(globalX)},${Math.floor(globalY)},${Math.floor(
          globalZ
        )}`,
        x,
        y
      );
      this.ctx.fill();
      this.ctx.closePath();
    }

  }

}

this.ctx.canvas.style["transform"] = `translateX(${
  (MINIMAP_SIZE - MINIMAP_WIDTH) / -2
}px) translateX(${
  (MINIMAP_SIZE - MINIMAP_HEIGHT) / -2
}px) translateX(-${translateX}px) translateY(-${translateZ}px)`;

The pink dot / text is just for debugging

fenomas commented 4 years ago

This is a cool idea! The noa-related code (e.g. how you're getting the player position) looks right to me. It's a bit hard to grok the rest, but I'm guessing there's just a math bug somewhere in those offsets and modulos. (and maybe the fact that in Canvas the Y coord runs from top to bottom?)

That said:

      Math.floor(currentChunkZ) === Math.floor(chunkX) &&
      //                     ^ X?
      Math.floor(currentChunkZ) === Math.floor(chunkZ)
Jarred-Sumner commented 4 years ago

Thanks for taking a look – it was a few math things.

Plus, the axis was wrong for getting the colors. I was setting all the x to z and all the z to x.

Framerate is a little too low now on Safari though and I optimized a lot of this code already. I hate Safari.

This is what it looks like now. I'm still going to add a few things (location name) and a full-screen map when you press M, but I'm happy with it so far. I'd rather a more Fortnite/World of Warcraft-style map design than a pixel art map design....but that seems much more time consuming to code

minimap

It'd be slightly better if I fix the occasional missing frame at the bottom but that's a thing I can fix later

fenomas commented 4 years ago

Hey, this looks really cool. Are you constructing the whole image each tick? The low-hanging fruit optimization would be for each chunk to render its own 32x32 image, cached and only periodically recreated, and the main loop would just take those N images and composite them to the right place on the canvas. This would also make it relatively easy to make the map rotate with the player if you want (just rotate them before drawing them into the canvas). Anyway cool stuff!

Jarred-Sumner commented 4 years ago

Thanks.

Yeah, on Firefox & Chrome it:

On Safari, it’s pretty much the same except there’s no ImageBitmap support. It just uses the ImageData object directly, but draws it twice - once at the smaller size and again to scale it correctly. I tried using Image with blobs but it didn’t make much of a difference

Theoretically, I could move all of this except the transform part to an OffscreenCanvas in a web worker but that won’t help Safari.

From profiling, it looks like three issues:

On Wed, Jun 24, 2020 at 1:16 AM Andy Hall notifications@github.com wrote:

Hey, this looks really cool. Are you constructing the whole image each tick? The low-hanging fruit optimization would be for each chunk to render its own 32x32 image, cached and only periodically recreated, and the main loop would just take those N images and composite them to the right place on the canvas. This would also make it relatively easy to make the map rotate with the player if you want (just rotate them before drawing them into the canvas). Anyway cool stuff!

— You are receiving this because you modified the open/close state.

Reply to this email directly, view it on GitHub https://github.com/andyhall/noa/issues/117#issuecomment-648671018, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAFNGS5F2L77JILEZ3QZBN3RYGY6RANCNFSM4OEJ3CKA .