Skip to content

227. 综合实战:开放世界(二十八)

Published:

这节来修一个问题:

2026-02-15 09.37.04.gif

注意右上角的小地图。

我们上车之后,移动的是蓝点,但是箭头还在红点上。

这时候应该红点消失,下车后才出现。

并且箭头应该在车上。

开飞机也是这样。

改一下 MapSystem:

image.png

增加这两种状态和对应的处理逻辑。

image.png

在车上、飞机上不显示玩家标记,并且绘制方向线。

更新的时候也是:

image.png

因为要绘制箭头,所以方向也要更新下:

image.png

import * as THREE from 'three';

// 地图配置
const MAP_CONFIG = {
  worldSize: 100, // 世界大小(从mesh.js中的groundSize)
  miniMapSize: 200, // 小地图画布大小(像素)
  fullMapSize: 800, // 全屏地图画布大小(像素)
  updateInterval: 100 // 更新间隔(毫秒)
};

// 地图系统类
class MapSystem {
  constructor() {
    this.miniMapCanvas = document.getElementById('miniMapCanvas');
    this.fullMapCanvas = document.getElementById('fullMapCanvas');
    this.fullMap = document.getElementById('fullMap');
    this.isFullMapOpen = false;
    this.isCarView = false;
    this.isPlaneView = false;
    
    // 创建Canvas上下文
    this.miniCtx = this.miniMapCanvas.getContext('2d');
    this.fullCtx = this.fullMapCanvas.getContext('2d');
    
    // 设置Canvas尺寸
    this.miniMapCanvas.width = MAP_CONFIG.miniMapSize;
    this.miniMapCanvas.height = MAP_CONFIG.miniMapSize;
    this.fullMapCanvas.width = MAP_CONFIG.fullMapSize;
    this.fullMapCanvas.height = MAP_CONFIG.fullMapSize;
    
    // 标记点数据
    this.markers = {
      player: { x: 0, z: 0, color: '#ff0000', label: '玩家' },
      car: { x: 0, z: 10, color: '#0066ff', label: '车辆' },
      plane: { x: -10, z: 10, color: '#00ff00', label: '飞机' },
      npc: { x: 5, z: 5, color: '#ffaa00', label: 'NPC' },
      house: { x: -20, z: -20, color: '#8b4513', label: '房屋' }
    };
    
    // 绑定关闭按钮事件
    const closeBtn = document.getElementById('closeMapBtn');
    if (closeBtn) {
      closeBtn.addEventListener('click', () => this.toggleFullMap());
    }
    
    // 监听窗口大小变化,调整全屏地图尺寸
    window.addEventListener('resize', () => this.updateFullMapSize());
  }
  
  // 将世界坐标转换为地图坐标
  worldToMap(worldX, worldZ, canvasSize) {
    // 世界坐标范围:-50 到 50(groundSize / 2)
    const worldMin = -MAP_CONFIG.worldSize / 2;
    const worldMax = MAP_CONFIG.worldSize / 2;
    const worldRange = worldMax - worldMin;
    
    // 归一化到 0-1
    const normalizedX = (worldX - worldMin) / worldRange;
    const normalizedZ = (worldZ - worldMin) / worldRange;
    
    // 转换为画布坐标(注意Z轴需要翻转,因为画布的Y轴向下)
    const mapX = normalizedX * canvasSize;
    const mapZ = (1 - normalizedZ) * canvasSize; // 翻转Z轴
    
    return { x: mapX, z: mapZ };
  }
  
  // 绘制地图背景
  drawBackground(ctx, size) {
    // 绘制地面
    ctx.fillStyle = '#90a955';
    ctx.fillRect(0, 0, size, size);
    
    // 绘制网格线
    ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
    ctx.lineWidth = 1;
    const gridCount = 10;
    const gridStep = size / gridCount;
    
    for (let i = 0; i <= gridCount; i++) {
      const pos = i * gridStep;
      // 垂直线
      ctx.beginPath();
      ctx.moveTo(pos, 0);
      ctx.lineTo(pos, size);
      ctx.stroke();
      // 水平线
      ctx.beginPath();
      ctx.moveTo(0, pos);
      ctx.lineTo(size, pos);
      ctx.stroke();
    }
    
    // 绘制中心点
    ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
    ctx.beginPath();
    ctx.arc(size / 2, size / 2, 3, 0, Math.PI * 2);
    ctx.fill();
  }
  
  // 绘制标记点
  drawMarker(ctx, mapX, mapZ, color, label, isPlayer = false) {
    const radius = isPlayer ? 6 : 4;
    
    // 绘制外圈(发光效果)
    if (isPlayer) {
      ctx.shadowBlur = 10;
      ctx.shadowColor = color;
    }
    
    // 绘制标记点
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.arc(mapX, mapZ, radius, 0, Math.PI * 2);
    ctx.fill();
    
    // 绘制白色边框
    ctx.strokeStyle = '#ffffff';
    ctx.lineWidth = 2;
    ctx.stroke();
    
    // 重置阴影
    ctx.shadowBlur = 0;
    
    // 在全屏地图上绘制标签
    if (this.isFullMapOpen && label) {
      ctx.fillStyle = '#ffffff';
      ctx.font = '12px Arial';
      ctx.textAlign = 'center';
      ctx.fillText(label, mapX, mapZ - radius - 5);
    }
  }
  
  // 绘制玩家方向指示
  drawPlayerDirection(ctx, mapX, mapZ, rotationY) {
    const length = 15;

    // 计算箭头终点位置
    const endX = mapX + Math.sin(rotationY) * length;
    const endZ = mapZ - Math.cos(rotationY) * length;
    
    // 绘制箭头线
    ctx.strokeStyle = '#ff0000';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(mapX, mapZ);
    ctx.lineTo(endX, endZ);
    ctx.stroke();
    
    // 绘制箭头头部
    const arrowSize = 5;
    const angle = Math.atan2(endZ - mapZ, endX - mapX);
    ctx.beginPath();
    ctx.moveTo(endX, endZ);
    ctx.lineTo(
      endX - arrowSize * Math.cos(angle - Math.PI / 6),
      endZ - arrowSize * Math.sin(angle - Math.PI / 6)
    );
    ctx.lineTo(
      endX - arrowSize * Math.cos(angle + Math.PI / 6),
      endZ - arrowSize * Math.sin(angle + Math.PI / 6)
    );
    ctx.closePath();
    ctx.fillStyle = '#ff0000';
    ctx.fill();
  }
  
  // 更新标记位置
  updateMarker(type, worldX, worldZ, rotationY = null) {
    if (this.markers[type]) {
      this.markers[type].x = worldX;
      this.markers[type].z = worldZ;
      if (rotationY !== null) {
        this.markers[type].rotationY = rotationY;
      }
    }
  }
  
  // 绘制小地图
  drawMiniMap() {
    const ctx = this.miniCtx;
    const size = MAP_CONFIG.miniMapSize;
    
    // 清空画布
    ctx.clearRect(0, 0, size, size);
    
    // 绘制背景
    this.drawBackground(ctx, size);
    
    // 绘制所有标记点
    Object.keys(this.markers).forEach(key => {
      const marker = this.markers[key];
      const { x, z } = this.worldToMap(marker.x, marker.z, size);
      const isPlayer = key === 'player';
      
      // 如果在上车或上飞机状态,不显示玩家标记
      if (isPlayer && (this.isCarView || this.isPlaneView)) {
        return;
      }
      
      this.drawMarker(ctx, x, z, marker.color, null, isPlayer);
      
      // 如果是玩家,绘制方向指示
      if (isPlayer && marker.rotationY !== undefined) {
        this.drawPlayerDirection(ctx, x, z, marker.rotationY);
      }
      
      // 如果在上车状态,在车辆上绘制方向指示
      if (key === 'car' && this.isCarView && marker.rotationY !== undefined) {
        this.drawPlayerDirection(ctx, x, z, marker.rotationY);
      }
      
      // 如果在上飞机状态,在飞机上绘制方向指示
      if (key === 'plane' && this.isPlaneView && marker.rotationY !== undefined) {
        this.drawPlayerDirection(ctx, x, z, marker.rotationY);
      }
    });
  }
  
  // 绘制全屏地图
  drawFullMap() {
    const ctx = this.fullCtx;
    const size = MAP_CONFIG.fullMapSize;
    
    // 清空画布
    ctx.clearRect(0, 0, size, size);
    
    // 绘制背景
    this.drawBackground(ctx, size);
    
    // 绘制所有标记点
    Object.keys(this.markers).forEach(key => {
      const marker = this.markers[key];
      const { x, z } = this.worldToMap(marker.x, marker.z, size);
      const isPlayer = key === 'player';
      
      // 如果在上车或上飞机状态,不显示玩家标记
      if (isPlayer && (this.isCarView || this.isPlaneView)) {
        return;
      }
      
      this.drawMarker(ctx, x, z, marker.color, marker.label, isPlayer);
      
      // 如果是玩家,绘制方向指示
      if (isPlayer && marker.rotationY !== undefined) {
        this.drawPlayerDirection(ctx, x, z, marker.rotationY);
      }
      
      // 如果在上车状态,在车辆上绘制方向指示
      if (key === 'car' && this.isCarView && marker.rotationY !== undefined) {
        this.drawPlayerDirection(ctx, x, z, marker.rotationY);
      }
      
      // 如果在上飞机状态,在飞机上绘制方向指示
      if (key === 'plane' && this.isPlaneView && marker.rotationY !== undefined) {
        this.drawPlayerDirection(ctx, x, z, marker.rotationY);
      }
    });
  }
  
  // 切换全屏地图
  toggleFullMap() {
    this.isFullMapOpen = !this.isFullMapOpen;
    if (this.isFullMapOpen) {
      this.fullMap.style.display = 'flex';
      this.updateFullMapSize();
      this.drawFullMap();
    } else {
      this.fullMap.style.display = 'none';
    }
  }
  
  // 更新全屏地图尺寸
  updateFullMapSize() {
    if (!this.isFullMapOpen) return;
    
    const container = this.fullMapCanvas.parentElement;
    if (!container) return;
    
    const containerRect = container.getBoundingClientRect();
    const headerHeight = 80; // 头部高度
    const legendHeight = 60; // 图例高度
    const margin = 40; // 边距
    
    const availableHeight = containerRect.height - headerHeight - legendHeight - margin;
    const availableWidth = containerRect.width - margin;
    
    // 保持正方形,取较小的尺寸,但不超过配置的最大尺寸
    const newSize = Math.min(availableHeight, availableWidth, MAP_CONFIG.fullMapSize);
    
    this.fullMapCanvas.width = newSize;
    this.fullMapCanvas.height = newSize;
    
    // 更新CSS样式以保持正方形
    this.fullMapCanvas.style.width = newSize + 'px';
    this.fullMapCanvas.style.height = newSize + 'px';
    
    this.drawFullMap();
  }
  
  // 更新地图(在渲染循环中调用)
  update() {
    this.drawMiniMap();
    if (this.isFullMapOpen) {
      this.drawFullMap();
    }
  }
}

// 创建并导出地图系统实例
export const mapSystem = new MapSystem();

// 导出更新标记的函数
export function updateMapMarkers(characterModel, carModel, planeModel, personModel, camera, isCarView = false, isPlaneView = false) {
  // 更新地图系统的状态
  mapSystem.isCarView = isCarView;
  mapSystem.isPlaneView = isPlaneView;
  
  // 更新玩家位置(只有在非上车/上飞机状态时才更新)
  if (characterModel && !isCarView && !isPlaneView) {
    const worldPos = new THREE.Vector3();
    characterModel.getWorldPosition(worldPos);
    
    // 使用相机的实际朝向来计算方向,因为玩家移动是基于相机方向的
    let rot = characterModel.rotation.y;
    if (camera) {
      const forward = new THREE.Vector3();
      camera.getWorldDirection(forward);
      forward.y = 0;
      forward.normalize();
      // 计算相机朝向的角度(相对于+Z方向)
      rot = Math.atan2(forward.x, forward.z);
    }
    
    mapSystem.updateMarker('player', worldPos.x, worldPos.z, rot);
  }
  
  // 更新车辆位置和方向
  if (carModel) {
    const worldPos = new THREE.Vector3();
    carModel.getWorldPosition(worldPos);
    // 如果在上车状态,更新车辆的方向
    let carRotation = null;
    if (isCarView && carModel) {
      carRotation = carModel.rotation.y;
    }
    mapSystem.updateMarker('car', worldPos.x, worldPos.z, carRotation);
  }
  
  // 更新飞机位置和方向
  if (planeModel) {
    const worldPos = new THREE.Vector3();
    planeModel.getWorldPosition(worldPos);
    // 如果在上飞机状态,更新飞机的方向
    let planeRotation = null;
    if (isPlaneView && planeModel) {
      planeRotation = planeModel.rotation.y;
    }
    mapSystem.updateMarker('plane', worldPos.x, worldPos.z, planeRotation);
  }
  
  // 更新NPC位置
  if (personModel) {
    const worldPos = new THREE.Vector3();
    personModel.getWorldPosition(worldPos);
    mapSystem.updateMarker('npc', worldPos.x, worldPos.z);
  }
}

// 导出切换全屏地图的函数
export function toggleFullMap() {
  mapSystem.toggleFullMap();
}

更新小地图的时候传入这俩状态:

image.png

updateMapMarkers(characterModel, carModel, planeModel, personModel, camera, isCarView, isPlaneView);

试一下:

2026-02-15 09.37.04.gif

2026-02-15 09.46.41.gif

2026-02-15 09.47.24.gif

上下车和飞机都没问题。

这样小地图的问题就修复了。

案例代码上传了小册仓库

总结

这节我们修复了小地图的问题。

上车、上飞机后红点消失,箭头加到交通工具上。

下车、下飞机后恢复。

这样,小地图功能就比较完整了。

评论