Skip to content

234. 综合实战:开放世界(三十五)

Published:

现在这些提示不是很友好:

image.png

内容太多了。

我们优化下显示逻辑。

首先之前的提示内容清空:

image.png

<div id="viewTip"></div>
<div id="weatherTip">晴天</div>

后面需要的时候才展示,比如靠近车的时候。

image.png

设置面板左边加一个使用手册的按钮和对应的弹窗 html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>open-world</title>
    <style>
      /* 关键 CSS - 首屏立即生效,避免 FOUC */
      body { margin: 0; }
      #loadingOverlay {
        position: fixed; top: 0; left: 0; width: 100%; height: 100%;
        background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
        display: flex; justify-content: center; align-items: center;
        z-index: 10000; transition: opacity 0.5s ease;
      }
      .loading-content { text-align: center; color: #fff; }
      .loading-content h1 { font-size: 28px; margin-bottom: 24px; font-weight: 300; }
      .loading-bar {
        width: 320px; height: 8px; background: rgba(255,255,255,0.2);
        border-radius: 4px; overflow: hidden; margin: 0 auto 12px;
      }
      .loading-progress {
        height: 100%; width: 0%; background: linear-gradient(90deg,#4ade80,#22c55e);
        border-radius: 4px; transition: width 0.2s ease;
      }
      .loading-content p { font-size: 14px; color: rgba(255,255,255,0.7); }
      #loadingOverlay.hidden { opacity: 0; pointer-events: none; }
    </style>
    <style>
      .dialog {
        line-height: 40px;
        text-align: center;
        font-size: 20px;
        padding: 10px;
        border: 1px solid #000;
        background: #fff;
        border-radius: 4px;
        white-space: nowrap;
      }
    </style>
  </head>
  <body>
    <div id="loadingOverlay">
      <div class="loading-content">
        <h1>加载中...</h1>
        <div class="loading-bar"><div id="loadingProgress" class="loading-progress"></div></div>
        <p id="loadingPercent">0%</p>
      </div>
    </div>
    <div id="app"></div>
    <div id="viewTip"></div>
    <div id="weatherTip">晴天</div>
    <div id="saveLoadToast" class="save-load-toast"></div>
    <div id="personDialog" style="display:none;" class="dialog">
      你好,我是NPC!
    </div>
    <div id="playerDialog" style="display:none;" class="dialog">
      你好!
    </div>
    <div id="desktop" style="display: none;">
      <img class="bg" src="./bg.png"/>
      <div class="app">
        <div class="logo"></div>
        <div class="name">浏览器</div>
      </div>
      <iframe class="browser" style="display: none;" src="/assets/threejs/b5c7c27c462211378dfee368618551a67209cb7c.image"></iframe>
    </div>
    <!-- 小地图 -->
    <div id="miniMap">
      <canvas class="map-canvas" id="miniMapCanvas"></canvas>
      <div class="map-legend">
        <div class="legend-item">
          <span class="legend-icon player-icon"></span>
          <span>玩家</span>
        </div>
        <div class="legend-item">
          <span class="legend-icon car-icon"></span>
          <span>车辆</span>
        </div>
        <div class="legend-item">
          <span class="legend-icon plane-icon"></span>
          <span>飞机</span>
        </div>
        <div class="legend-item">
          <span class="legend-icon npc-icon"></span>
          <span>NPC</span>
        </div>
        <div class="legend-item">
          <span class="legend-icon house-icon"></span>
          <span>房屋</span>
        </div>
      </div>
      <div class="map-hint">按 ? 查看帮助</div>
    </div>
    <!-- 全屏地图 -->
    <div id="fullMap" style="display: none;">
      <div class="map-header">
        <h2>地图</h2>
        <button id="closeMapBtn" class="close-btn">关闭 (M)</button>
      </div>
      <canvas class="map-canvas-full" id="fullMapCanvas"></canvas>
      <div class="map-legend-full">
        <div class="legend-item">
          <span class="legend-icon player-icon"></span>
          <span>玩家</span>
        </div>
        <div class="legend-item">
          <span class="legend-icon car-icon"></span>
          <span>车辆</span>
        </div>
        <div class="legend-item">
          <span class="legend-icon plane-icon"></span>
          <span>飞机</span>
        </div>
        <div class="legend-item">
          <span class="legend-icon npc-icon"></span>
          <span>NPC</span>
        </div>
        <div class="legend-item">
          <span class="legend-icon house-icon"></span>
          <span>房屋</span>
        </div>
      </div>
    </div>
    <!-- 设置面板 -->
    <div id="settingsPanel" style="display: none;">
      <div class="settings-content">
        <div class="settings-header">
          <h2>设置</h2>
          <button id="closeSettingsBtn" class="close-btn">关闭 (ESC)</button>
        </div>
        <div class="settings-body">
          <div class="settings-section">
            <h3>音频设置</h3>
            <div class="settings-item">
              <label>
                <input type="checkbox" id="bgMusicToggle" checked>
                <span>背景音乐</span>
              </label>
            </div>
            <div class="settings-item">
              <label>
                <input type="checkbox" id="soundEffectToggle" checked>
                <span>音效</span>
              </label>
            </div>
          </div>
          <div class="settings-section">
            <h3>界面设置</h3>
            <div class="settings-item">
              <label>
                <input type="checkbox" id="miniMapToggle" checked>
                <span>小地图</span>
              </label>
            </div>
          </div>
          <div class="settings-section">
            <h3>天气设置</h3>
            <div class="weather-buttons">
              <button class="weather-btn" data-weather="clear">晴天</button>
              <button class="weather-btn" data-weather="rain">雨天</button>
              <button class="weather-btn" data-weather="snow">雪天</button>
              <button class="weather-btn" data-weather="fog">雾天</button>
            </div>
          </div>
          <div class="settings-section">
            <h3>存档</h3>
            <div class="save-load-buttons">
              <button id="saveGameBtn" class="save-btn">存档</button>
              <button id="loadGameBtn" class="load-btn">读档</button>
            </div>
            <div id="saveStatus" class="save-status"></div>
          </div>
        </div>
      </div>
    </div>
    <!-- 使用手册 -->
    <div id="manualPanel" style="display: none;">
      <div class="manual-content">
        <div class="manual-header">
          <h2>使用手册</h2>
          <button id="closeManualBtn" class="close-btn">关闭 (?)</button>
        </div>
        <div class="manual-body">
          <section>
            <h3>移动</h3>
            <p>WASD 移动 · 空格 跳跃 · 鼠标 转动视角</p>
          </section>
          <section>
            <h3>载具</h3>
            <p>靠近车辆按 <kbd>X</kbd> 上车/下车</p>
            <p>靠近飞机按 <kbd>C</kbd> 上飞机/下飞机(需先降落)</p>
            <p>飞行时:空格上升 · Shift 下降</p>
          </section>
          <section>
            <h3>交互</h3>
            <p>靠近电脑按 <kbd>E</kbd> 使用</p>
            <p>靠近 NPC 按 <kbd>H</kbd> 对话 · <kbd>K</kbd> 结束</p>
          </section>
          <section>
            <h3>界面</h3>
            <p><kbd>M</kbd> 地图 · <kbd>P</kbd> 设置 · <kbd>?</kbd> 本手册</p>
          </section>
          <section>
            <h3>天气</h3>
            <p>数字键 1-4 切换,或在设置中选择</p>
          </section>
          <section>
            <h3>存档</h3>
            <p>Ctrl+Shift+S 存档 · Ctrl+Shift+L 读档</p>
          </section>
        </div>
      </div>
    </div>
    <!-- 设置按钮 -->
    <button id="settingsBtn" class="settings-btn" title="设置">⚙️</button>
    <button id="manualBtn" class="manual-btn" title="使用手册">?</button>
    <script type="module" src="/src/main.js"></script>
    <script>
      document.addEventListener('DOMContentLoaded', function() {
        const appElement = document.querySelector('#desktop .app');
        const browserElement = document.querySelector('#desktop .browser');
        if (appElement && browserElement) {
          appElement.addEventListener('dblclick', function() {
            browserElement.style.display = browserElement.style.display === 'none' ? 'block' : 'none';
          });
        }
      });
    </script>
  </body>
</html>

同样在 main.js 里加一下打开关闭弹窗的逻辑。

image.png

image.png

image.png

export let isManualOpen = false;
function toggleManual() {
  const manualPanel = document.getElementById('manualPanel');
  if (!manualPanel) return;
  isManualOpen = !isManualOpen;
  manualPanel.style.display = isManualOpen ? 'flex' : 'none';
  if (isManualOpen && document.pointerLockElement) {
    document.exitPointerLock();
  }
}
  // 使用手册
  const manualBtn = document.getElementById('manualBtn');
  const closeManualBtn = document.getElementById('closeManualBtn');
  const manualPanel = document.getElementById('manualPanel');
  if (manualBtn) manualBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleManual(); });
  if (closeManualBtn) closeManualBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleManual(); });
  if (manualPanel) {
    manualPanel.addEventListener('mousedown', (e) => e.stopPropagation());
    manualPanel.addEventListener('click', (e) => e.stopPropagation());
  }

加一下对应样式:

image.png


/* 使用手册按钮 */
.manual-btn {
  position: fixed;
  top: 20px;
  right: 80px;
  width: 50px;
  height: 50px;
  background: rgba(0, 0, 0, 0.6);
  border: 2px solid rgba(255, 255, 255, 0.3);
  border-radius: 50%;
  color: white;
  font-size: 22px;
  font-weight: bold;
  cursor: pointer;
  z-index: 1001;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.3s;
}

.manual-btn:hover {
  background: rgba(0, 0, 0, 0.8);
  border-color: rgba(255, 255, 255, 0.5);
  transform: scale(1.1);
}

/* 使用手册面板 */
#manualPanel {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.85);
  z-index: 3000;
  display: flex;
  justify-content: center;
  align-items: center;
  pointer-events: auto;
}

.manual-content {
  background: rgba(30, 30, 30, 0.98);
  border-radius: 12px;
  width: 90%;
  max-width: 420px;
  max-height: 85vh;
  overflow-y: auto;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
  border: 2px solid #555;
}

.manual-header {
  padding: 20px;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 2px solid #555;
  border-radius: 12px 12px 0 0;
}

.manual-header h2 {
  color: white;
  margin: 0;
  font-size: 22px;
}

.manual-body {
  padding: 24px;
}

.manual-body section {
  margin-bottom: 20px;
}

.manual-body section:last-child {
  margin-bottom: 0;
}

.manual-body h3 {
  color: #4ade80;
  margin: 0 0 8px 0;
  font-size: 16px;
}

.manual-body p {
  color: #e2e8f0;
  margin: 0 0 6px 0;
  font-size: 14px;
  line-height: 1.6;
}

.manual-body kbd {
  display: inline-block;
  padding: 2px 6px;
  background: #4a5568;
  border-radius: 4px;
  font-family: monospace;
  font-size: 13px;
}

然后其余提示的逻辑也改一下:

image.png

tipElement.textContent = weatherNames[this.currentWeather];

并且和设置面板一样,帮助面板也要退出鼠标锁定状态:

image.png

  const manualPanel = document.getElementById('manualPanel');
  const manualBtn = document.getElementById('manualBtn');
  if (isSettingsOpen || isManualOpen ||
      (settingsPanel && (settingsPanel.contains(target) || settingsPanel === target)) ||
      (settingsBtn && (settingsBtn.contains(target) || settingsBtn === target)) ||
      (manualPanel && (manualPanel.contains(target) || manualPanel === target)) ||
      (manualBtn && (manualBtn.contains(target) || manualBtn === target)) ||
      (fullMap && fullMap.style.display !== 'none' && fullMap.contains(target))) {
    return;
  }

不然没鼠标。

2026-03-01 22.50.22.gif

2026-03-01 22.50.38.gif

2026-03-01 22.51.20.gif

2026-03-01 22.51.59.gif

只有在靠近的时候才展示提示信息,其余都收到了帮助面板了。

界面简洁多了。

案例代码上传了小册仓库 部署好的 url:https://quarkgluonplasma.github.io/threejs-open-world/

总结

这节我们优化了提示信息。

加了一个帮助面板,在那里集中展示快捷键等信息。

当时走近 NPC、飞机、汽车等依然有提示。

这样体验就好多了。

评论