Minimap overlay for maze game

i created this code for a 3d endless maze explorer with a minimap, but the minimap appears on one of the walls of the maze instead of being a 2d overlay. everytime i try to embed it as a 2d overlay on top of the 3d game things start get really visually glitchy i see the map overall of the walls and some really strange bugs.

my question is what would be the best way to add the minimap as a 2d overlay on the top right of the screen?

// mySketch.pde

import java.util.HashMap;
import java.util.Random;
import java.util.ArrayList;
import java.util.Stack;

// Player position and viewing angles
float x, y, z;
float angleX, angleY;
float targetAngleX, targetAngleY;
float angleSmoothing = 0.1f;

// Movement settings
float speed = 5;
float friction = 0.85f;
PVector velocity;

// Maze settings
int cellSize = 200;
int wallHeight = 300;
float playerRadius = 30;
float playerHeight = 150;

// Textures
PImage wallTexture;
PImage floorTexture;

// Maze chunks
HashMap<String, MazeChunk> mazeChunks;
int chunkSize = 20;
int renderDistance = 2;

// Movement keys
boolean moveForward = false;
boolean moveBackward = false;
boolean moveLeft = false;
boolean moveRight = false;
boolean running = false;

// Minimap settings
PGraphics minimap;
int minimapSize;

void setup() {
  fullScreen(P3D);
  noCursor();
  smooth(4);

  // Initialize textures
  generateTextures();

  // Initialize mouse positions
  pmouseX = mouseX;
  pmouseY = mouseY;

  // Set the perspective
  float fov = PI / 3;
  float aspect = (float) width / height;
  float near = 0.1f;
  float far = 10000;
  perspective(fov, aspect, near, far);

  x = cellSize * 1.5f;
  y = playerHeight;
  z = cellSize * 1.5f;
  angleX = 0;
  angleY = 0;
  targetAngleX = angleX;
  targetAngleY = angleY;
  velocity = new PVector(0, 0, 0);

  mazeChunks = new HashMap<String, MazeChunk>();

  // Initialize minimap
  minimapSize = 300; // Adjust as needed
  minimap = createGraphics(minimapSize, minimapSize, P2D); // Use P2D renderer for 2D drawing
}

void draw() {
  background(0);

  // 3D Drawing
  lights();
  ambientLight(100, 100, 100);
  directionalLight(150, 150, 150, -0.5f, -1, -0.5f);

  // Smoothly interpolate camera angles
  angleX = lerp(angleX, targetAngleX, angleSmoothing);
  angleY = lerp(angleY, targetAngleY, angleSmoothing);

  // Adjust speed based on running
  float currentSpeed = speed;
  if (running) {
    currentSpeed *= 1.5;
  }

  // Update player velocity and position based on input
  PVector inputDir = new PVector(0, 0, 0);

  // Get the forward and right directions
  PVector forwardDir = new PVector(cos(angleY), 0, sin(angleY));
  PVector rightDir = new PVector(-sin(angleY), 0, cos(angleY));

  if (moveForward) {
    inputDir.add(forwardDir);
  }
  if (moveBackward) {
    inputDir.sub(forwardDir);
  }
  if (moveLeft) {
    inputDir.sub(rightDir);
  }
  if (moveRight) {
    inputDir.add(rightDir);
  }

  if (inputDir.mag() > 0) {
    inputDir.normalize();
    inputDir.mult(currentSpeed);
    velocity.lerp(inputDir, 0.2);
  } else {
    velocity.mult(friction);
  }

  // Update player position with collision detection
  float newX = x + velocity.x;
  float newZ = z + velocity.z;

  if (!checkCollision(newX, newZ)) {
    x = newX;
    z = newZ;
  } else {
    velocity.set(0, 0, 0);
  }

  // Set the camera
  camera(x, y, z,
         x + cos(angleY) * cos(angleX), y + sin(angleX),
         z + sin(angleY) * cos(angleX), 0, 1, 0);

  // Update maze chunks
  updateMazeChunks();

  // Draw the maze
  drawMaze();

  // Update and draw the minimap last
  updateMinimap();
  drawMinimap();
}

void generateTextures() {
  // Wall texture
  wallTexture = createImage(64, 64, RGB);
  wallTexture.loadPixels();
  for (int i = 0; i < wallTexture.width; i++) {
    for (int j = 0; j < wallTexture.height; j++) {
      float shade = random(100, 150);
      wallTexture.pixels[j * wallTexture.width + i] = color(shade);
    }
  }
  wallTexture.updatePixels();

  // Floor texture
  floorTexture = createImage(64, 64, RGB);
  floorTexture.loadPixels();
  for (int i = 0; i < floorTexture.width; i++) {
    for (int j = 0; j < floorTexture.height; j++) {
      float shade = random(80, 130);
      floorTexture.pixels[j * floorTexture.width + i] = color(shade);
    }
  }
  floorTexture.updatePixels();
}

void updateMazeChunks() {
  int playerChunkX = floor(x / (cellSize * chunkSize));
  int playerChunkZ = floor(z / (cellSize * chunkSize));

  // Remove chunks out of render distance
  ArrayList<String> keysToRemove = new ArrayList<String>();
  for (String key : mazeChunks.keySet()) {
    String[] coords = key.split(",");
    int chunkX = Integer.parseInt(coords[0]);
    int chunkZ = Integer.parseInt(coords[1]);
    if (abs(chunkX - playerChunkX) > renderDistance ||
        abs(chunkZ - playerChunkZ) > renderDistance) {
      keysToRemove.add(key);
    }
  }
  for (String key : keysToRemove) {
    mazeChunks.remove(key);
  }

  // Add new chunks
  for (int i = -renderDistance; i <= renderDistance; i++) {
    for (int j = -renderDistance; j <= renderDistance; j++) {
      int chunkX = playerChunkX + i;
      int chunkZ = playerChunkZ + j;
      String key = chunkX + "," + chunkZ;
      if (!mazeChunks.containsKey(key)) {
        // Determine openings
        boolean openLeft = false;
        boolean openRight = false;
        boolean openTop = false;
        boolean openBottom = false;

        String leftKey = (chunkX - 1) + "," + chunkZ;
        if (mazeChunks.containsKey(leftKey)) {
          openLeft = mazeChunks.get(leftKey).openRight;
        }

        String rightKey = (chunkX + 1) + "," + chunkZ;
        if (mazeChunks.containsKey(rightKey)) {
          openRight = mazeChunks.get(rightKey).openLeft;
        }

        String topKey = chunkX + "," + (chunkZ - 1);
        if (mazeChunks.containsKey(topKey)) {
          openTop = mazeChunks.get(topKey).openBottom;
        }

        String bottomKey = chunkX + "," + (chunkZ + 1);
        if (mazeChunks.containsKey(bottomKey)) {
          openBottom = mazeChunks.get(bottomKey).openTop;
        }

        // Generate the maze chunk
        MazeChunkData chunkData = generateMazeChunk(chunkSize, chunkSize,
          chunkX, chunkZ, openLeft, openRight, openTop, openBottom);
        PShape newChunkShape = createMazeChunkShape(chunkData.maze,
          chunkX, chunkZ);
        MazeChunk newChunk = new MazeChunk(chunkData.maze, newChunkShape,
          chunkX, chunkZ, chunkData.openLeft, chunkData.openRight,
          chunkData.openTop, chunkData.openBottom);
        mazeChunks.put(key, newChunk);
      }
    }
  }
}

void drawMaze() {
  // Draw all chunks
  for (MazeChunk chunk : mazeChunks.values()) {
    shape(chunk.shape);
  }
}

void updateMinimap() {
  minimap.beginDraw();
  minimap.background(50, 50, 50, 200); // Semi-transparent background

  // Set fill and stroke for maze cells
  minimap.stroke(255); // White outline
  minimap.fill(100, 100, 100); // Gray fill

  // Adjusted scale factor
  float mapWidth = (renderDistance * 2 + 1) * chunkSize * cellSize;
  float scale = minimapSize / mapWidth * 4; // Adjust as needed

  // Center the minimap on the player
  minimap.pushMatrix();
  minimap.translate(minimapSize / 2, minimapSize / 2);
  minimap.rotate(-angleY);

  // Draw maze walls
  for (MazeChunk chunk : mazeChunks.values()) {
    int chunkX = chunk.chunkX;
    int chunkZ = chunk.chunkZ;
    int[][] maze = chunk.maze;

    int baseX = chunkX * chunkSize * cellSize;
    int baseZ = chunkZ * chunkSize * cellSize;

    for (int i = 0; i < chunkSize; i++) {
      for (int j = 0; j < chunkSize; j++) {
        if (maze[i][j] == 1) {
          float cellX = baseX + i * cellSize - x;
          float cellZ = baseZ + j * cellSize - z;

          float drawX = cellX * scale;
          float drawY = cellZ * scale;

          minimap.rect(drawX, drawY, cellSize * scale, cellSize * scale);
        }
      }
    }
  }

  // Draw the player position
  minimap.fill(255, 0, 0); // Red color
  minimap.noStroke();
  minimap.ellipse(0, 0, 15, 15);

  minimap.popMatrix();
  minimap.endDraw();
}

void drawMinimap() {
  // Save the current transformation and style
  pushStyle();
  pushMatrix();

  // Disable depth test
  hint(DISABLE_DEPTH_TEST);

  // Draw the minimap image
  image(minimap, width - minimapSize - 10, 10);

  // Draw the minimap border
  noFill();
  stroke(255);
  rect(width - minimapSize - 10, 10, minimapSize, minimapSize);

  // Re-enable depth test
  hint(ENABLE_DEPTH_TEST);

  // Restore the style and transformations
  popMatrix();
  popStyle();
}

// MazeChunk class
class MazeChunk {
  int[][] maze;
  PShape shape;
  int chunkX, chunkZ;
  boolean openLeft, openRight, openTop, openBottom;

  MazeChunk(int[][] maze, PShape shape, int chunkX, int chunkZ,
            boolean openLeft, boolean openRight,
            boolean openTop, boolean openBottom) {
    this.maze = maze;
    this.shape = shape;
    this.chunkX = chunkX;
    this.chunkZ = chunkZ;
    this.openLeft = openLeft;
    this.openRight = openRight;
    this.openTop = openTop;
    this.openBottom = openBottom;
  }
}

// MazeChunkData class
class MazeChunkData {
  int[][] maze;
  boolean openLeft, openRight, openTop, openBottom;

  MazeChunkData(int[][] maze, boolean openLeft, boolean openRight,
                boolean openTop, boolean openBottom) {
    this.maze = maze;
    this.openLeft = openLeft;
    this.openRight = openRight;
    this.openTop = openTop;
    this.openBottom = openBottom;
  }
}

PShape createMazeChunkShape(int[][] maze, int chunkX, int chunkZ) {
  PShape chunkShape = createShape();
  chunkShape.beginShape(TRIANGLES);
  chunkShape.noStroke();
  chunkShape.textureMode(NORMAL);

  int baseX = chunkX * chunkSize;
  int baseZ = chunkZ * chunkSize;

  chunkShape.texture(floorTexture);

  // Draw floor and ceiling
  for (int i = 0; i < chunkSize; i++) {
    for (int j = 0; j < chunkSize; j++) {
      float x0 = (baseX + i) * cellSize;
      float z0 = (baseZ + j) * cellSize;
      float x1 = x0 + cellSize;
      float z1 = z0 + cellSize;

      // Floor
      chunkShape.normal(0, 1, 0);
      chunkShape.vertex(x0, 0, z0, 0, 0);
      chunkShape.vertex(x1, 0, z0, 1, 0);
      chunkShape.vertex(x1, 0, z1, 1, 1);

      chunkShape.vertex(x0, 0, z0, 0, 0);
      chunkShape.vertex(x1, 0, z1, 1, 1);
      chunkShape.vertex(x0, 0, z1, 0, 1);

      // Ceiling
      chunkShape.normal(0, -1, 0);
      chunkShape.vertex(x0, wallHeight, z0, 0, 0);
      chunkShape.vertex(x1, wallHeight, z1, 1, 1);
      chunkShape.vertex(x1, wallHeight, z0, 1, 0);

      chunkShape.vertex(x0, wallHeight, z0, 0, 0);
      chunkShape.vertex(x0, wallHeight, z1, 0, 1);
      chunkShape.vertex(x1, wallHeight, z1, 1, 1);
    }
  }

  chunkShape.texture(wallTexture);

  // Draw walls
  for (int i = 0; i < chunkSize; i++) {
    for (int j = 0; j < chunkSize; j++) {
      if (maze[i][j] == 1) {
        float x0 = (baseX + i) * cellSize;
        float z0 = (baseZ + j) * cellSize;
        float x1 = x0 + cellSize;
        float z1 = z0 + cellSize;

        // Front wall (+Z)
        chunkShape.normal(0, 0, 1);
        chunkShape.vertex(x0, 0, z1, 0, 1);
        chunkShape.vertex(x1, 0, z1, 1, 1);
        chunkShape.vertex(x1, wallHeight, z1, 1, 0);

        chunkShape.vertex(x0, 0, z1, 0, 1);
        chunkShape.vertex(x1, wallHeight, z1, 1, 0);
        chunkShape.vertex(x0, wallHeight, z1, 0, 0);

        // Back wall (-Z)
        chunkShape.normal(0, 0, -1);
        chunkShape.vertex(x1, 0, z0, 0, 1);
        chunkShape.vertex(x0, 0, z0, 1, 1);
        chunkShape.vertex(x0, wallHeight, z0, 1, 0);

        chunkShape.vertex(x1, 0, z0, 0, 1);
        chunkShape.vertex(x0, wallHeight, z0, 1, 0);
        chunkShape.vertex(x1, wallHeight, z0, 0, 0);

        // Left wall (-X)
        chunkShape.normal(-1, 0, 0);
        chunkShape.vertex(x0, 0, z0, 0, 1);
        chunkShape.vertex(x0, 0, z1, 1, 1);
        chunkShape.vertex(x0, wallHeight, z1, 1, 0);

        chunkShape.vertex(x0, 0, z0, 0, 1);
        chunkShape.vertex(x0, wallHeight, z1, 1, 0);
        chunkShape.vertex(x0, wallHeight, z0, 0, 0);

        // Right wall (+X)
        chunkShape.normal(1, 0, 0);
        chunkShape.vertex(x1, 0, z1, 0, 1);
        chunkShape.vertex(x1, 0, z0, 1, 1);
        chunkShape.vertex(x1, wallHeight, z0, 1, 0);

        chunkShape.vertex(x1, 0, z1, 0, 1);
        chunkShape.vertex(x1, wallHeight, z0, 1, 0);
        chunkShape.vertex(x1, wallHeight, z1, 0, 0);
      }
    }
  }

  chunkShape.endShape();
  return chunkShape;
}

MazeChunkData generateMazeChunk(int width, int height, int chunkX,
                                int chunkZ, boolean openLeft,
                                boolean openRight, boolean openTop,
                                boolean openBottom) {
  int[][] maze = new int[width][height];

  // Initialize all cells as walls
  for (int i = 0; i < width; i++) {
    for (int j = 0; j < height; j++) {
      maze[i][j] = 1;
    }
  }

  // Random number generator with seed
  Random rng = new Random(computeHash(chunkX, chunkZ));

  // Use a stack for backtracking
  Stack<int[]> stack = new Stack<>();
  int startX = rng.nextInt(width / 2) * 2 + 1;
  int startY = rng.nextInt(height / 2) * 2 + 1;
  maze[startX][startY] = 0;
  stack.push(new int[]{startX, startY});

  // Directions: right, down, left, up
  int[][] directions = {{2, 0}, {0, 2}, {-2, 0}, {0, -2}};

  while (!stack.isEmpty()) {
    int[] current = stack.peek();
    int cx = current[0];
    int cy = current[1];

    ArrayList<int[]> neighbors = new ArrayList<>();
    for (int[] dir : directions) {
      int nx = cx + dir[0];
      int ny = cy + dir[1];
      if (nx >= 1 && nx < width - 1 && ny >= 1 && ny < height - 1 &&
          maze[nx][ny] == 1) {
        neighbors.add(new int[]{nx, ny});
      }
    }

    if (!neighbors.isEmpty()) {
      int[] next = neighbors.get(rng.nextInt(neighbors.size()));
      int nx = next[0];
      int ny = next[1];
      // Open the wall
      maze[(cx + nx) / 2][(cy + ny) / 2] = 0;
      maze[nx][ny] = 0;
      stack.push(next);
    } else {
      stack.pop();
    }
  }

  // Ensure connections
  if (openLeft) {
    maze[0][height / 2] = 0;
  }
  if (openRight) {
    maze[width - 1][height / 2] = 0;
  }
  if (openTop) {
    maze[width / 2][0] = 0;
  }
  if (openBottom) {
    maze[width / 2][height - 1] = 0;
  }

  // Randomly create additional openings
  if (!openLeft && rng.nextBoolean()) {
    maze[0][rng.nextInt(height / 2) * 2 + 1] = 0;
    openLeft = true;
  }
  if (!openRight && rng.nextBoolean()) {
    maze[width - 1][rng.nextInt(height / 2) * 2 + 1] = 0;
    openRight = true;
  }
  if (!openTop && rng.nextBoolean()) {
    maze[rng.nextInt(width / 2) * 2 + 1][0] = 0;
    openTop = true;
  }
  if (!openBottom && rng.nextBoolean()) {
    maze[rng.nextInt(width / 2) * 2 + 1][height - 1] = 0;
    openBottom = true;
  }

  return new MazeChunkData(maze, openLeft, openRight, openTop, openBottom);
}

boolean checkCollision(float newX, float newZ) {
  int cellX = floor(newX / cellSize);
  int cellZ = floor(newZ / cellSize);

  // Get chunk coordinates
  int chunkX = floor((float) cellX / chunkSize);
  int chunkZ = floor((float) cellZ / chunkSize);
  String chunkKey = chunkX + "," + chunkZ;

  MazeChunk chunk = mazeChunks.get(chunkKey);

  if (chunk != null) {
    // Local cell coordinates
    int localX = ((cellX % chunkSize) + chunkSize) % chunkSize;
    int localZ = ((cellZ % chunkSize) + chunkSize) % chunkSize;

    // Check for collision with walls
    for (int i = -1; i <= 1; i++) {
      for (int j = -1; j <= 1; j++) {
        int globalCellX = cellX + i;
        int globalCellZ = cellZ + j;

        int neighborChunkX = floor((float) globalCellX / chunkSize);
        int neighborChunkZ = floor((float) globalCellZ / chunkSize);
        String neighborChunkKey = neighborChunkX + "," + neighborChunkZ;
        MazeChunk neighborChunk = mazeChunks.get(neighborChunkKey);

        if (neighborChunk != null) {
          int neighborLocalX = ((globalCellX % chunkSize) + chunkSize) % chunkSize;
          int neighborLocalZ = ((globalCellZ % chunkSize) + chunkSize) % chunkSize;

          if (neighborLocalX >= 0 && neighborLocalX < chunkSize &&
              neighborLocalZ >= 0 && neighborLocalZ < chunkSize) {
            if (neighborChunk.maze[neighborLocalX][neighborLocalZ] == 1) {
              // Calculate the distance to the wall
              float wallX = (globalCellX + 0.5f) * cellSize;
              float wallZ = (globalCellZ + 0.5f) * cellSize;
              float dx = newX - wallX;
              float dz = newZ - wallZ;
              float distanceX = abs(dx) - (cellSize / 2 + playerRadius);
              float distanceZ = abs(dz) - (cellSize / 2 + playerRadius);
              if (distanceX < 0 && distanceZ < 0) {
                return true; // Collision with wall
              }
            }
          }
        }
      }
    }
  }
  return false; // No collision
}

void mouseMoved() {
  targetAngleY += (mouseX - pmouseX) * 0.005f;
  targetAngleX += (mouseY - pmouseY) * 0.005f;
  targetAngleX = constrain(targetAngleX, -PI / 2 + 0.1f,
                           PI / 2 - 0.1f);

  pmouseX = mouseX;
  pmouseY = mouseY;
}

void keyPressed() {
  if (key == 'w' || key == 'W' || keyCode == UP) moveForward = true;
  if (key == 's' || key == 'S' || keyCode == DOWN) moveBackward = true;
  if (key == 'a' || key == 'A' || keyCode == LEFT) moveLeft = true;
  if (key == 'd' || key == 'D' || keyCode == RIGHT) moveRight = true;
  if (keyCode == SHIFT) running = true;
}

void keyReleased() {
  if (key == 'w' || key == 'W' || keyCode == UP) moveForward = false;
  if (key == 's' || key == 'S' || keyCode == DOWN) moveBackward = false;
  if (key == 'a' || key == 'A' || keyCode == LEFT) moveLeft = false;
  if (key == 'd' || key == 'D' || keyCode == RIGHT) moveRight = false;
  if (keyCode == SHIFT) running = false;
}

int computeHash(int x, int z) {
  // Combine x and z into a single hash value
  int h = 31;
  h = h * 17 + x;
  h = h * 17 + z;
  return h;
}
1 Like

Hello @winterchan,

Try this:

void draw() {
  background(0);

  pushMatrix(); //Added

  //Your code...

  popMatrix(); //Added

  // Update and draw the minimap last
  updateMinimap();
  drawMinimap();
}

The above seemed to work in this case:

This was an initial attempt that worked in your case; there may be other ways to do this.

Reference:

Hints:

  • push() / Reference / Processing.org
  • minimap.rotate(-angleY-TAU/4);
  • You may no longer require these and may impact performance (test this):
    hint(DISABLE_DEPTH_TEST);
    hint(ENABLE_DEPTH_TEST);

:)

2 Likes