npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@overworld-engine/scene

v1.5.0

Published

3D world layer: scene shell, player controller, collision, proximity, model loading

Readme

@overworld-engine/scene

Overworld 框架的 3D 世界层:场景外壳、玩家控制器、跟随相机、圆形碰撞、邻近检测、 GLTF 模型加载、场景主题与传送门。基于 React + three.js + @react-three/fiber + drei + zustand。

定位

本包只负责"可探索的 3D 世界"这一层,零游戏内容:没有内置模型路径、NPC 名字、 配色预设或世界边界,全部通过 props / 配置传入。跨系统通信一律走 @overworld-engine/core 的事件总线(gameEvents),因此对话、任务、音频等系统无需 import 本包即可响应玩家移动、场景切换与交互。

发出的事件:

| 事件 | 时机 | | --- | --- | | player:moved | 玩家累计移动约 0.5 米(可配)时,携带位置与移动距离 | | scene:changed | useSceneStore.setScene(id) 切换场景时 | | proximity:enter / proximity:leave | 玩家进入 / 离开 NPC 或建筑的交互半径时 | | entity:interact | 附近有实体且按下交互键(useInteractKey / interact())时 | | interact(已弃用) | 与 entity:interact 同载荷双发,仅为过渡期兼容,2.0 移除 |

核心组件 / API

  • <SceneShell> — 场景样板组合:碰撞注册 + 邻近检测 + NPC / 建筑循环 + 选中光环 + 玩家。场景专属内容(灯光、地面、传送门、装饰)作为 children 传入。 通过 npcIndicators(任务角标)与 interactHint(自定义交互提示)替代原游戏的 store 读取;player prop 默认渲染 <Player />,传 null 可关闭。
  • <Player> — WASD / 方向键移动,Shift 奔跑;圆形碰撞解算、可选世界边界钳制、 动画 crossfade(idle/walk/run)。modelUrl 省略时渲染胶囊体占位。 isInputBlocked?: () => boolean 用于接入你的输入优先级系统(如 @overworld-engine/input)。 externalInput?: MovementInputRef 接受外部移动源(虚拟摇杆/手柄等,形如 { current: { x, z, running } },模长 ≤ 1),每帧与键盘输入合并:方向相加后归一化, running = Shift 或 externalInput.current.running;模拟量模长 < 1 时速度按比例缩放 (纯键盘保持全速),同样受 isInputBlocked 约束。与 @overworld-engine/inputcreateMovementInput() / <VirtualJoystick> 结构兼容——两个包互不 import。
  • <FollowCamera targetRef offset lerp> — 平滑跟随相机,可独立使用。
  • <BaseNPC> / <BaseBuilding> — 模型加载 + 名牌 + 发光 + 交互气泡, 颜色全部来自 theme;"是否在附近"读取本包的 useSceneStore
  • <SelectionRing> / <CollisionRegistration> / <Portal> — 地面选中环、 声明式碰撞注册、场景传送门(默认走 setScene,可用 onEnter 覆盖)。
  • useSceneStorecurrentScene / nearbyNpcId / nearbyBuildingId; setScene(id) 会发出 scene:changed
  • useCollisionStore — 圆形碰撞注册表:registerCollider / checkCollision / resolveCollision(推出式解算)。
  • playerPositionRef / playerRotationRef / teleportPlayer(pos) — 模块级可变引用,每帧由 Player 写入,供逐帧系统(小地图、邻近检测)读取而不触发 React 重渲染;teleportPlayer 用于场景切换后落点。
  • useProximityDetection({ npcs, buildings, npcRadius, buildingRadius }) — 每帧找出最近的在半径内实体,写入 sceneStore 并发出 proximity 事件 (SceneShell 已内置调用)。
  • useModelLoader / preloadSceneModels — GLTF 加载(克隆 + 阴影配置)与预加载。 加载中会 Suspense 挂起(挂起的 promise 会重新抛出,不会被吞),必须在 <Suspense> 边界之下调用——BaseNPC / BaseBuilding / Player 已内置 <Suspense> + ModelErrorBoundary(按模型 URL 作 key,改路径即重试); 加载中与加载失败都显示主题化占位体,失败只打一条 console.errorpreloadSceneModels 因此只是性能优化(缓存命中时同步解析),不再是正确性前提。
  • interact() / useInteractKey(key = 'e', { isInputBlocked }) — 把"按 E 交互"翻译成总线上的 entity:interact 事件(过渡期同载荷双发已弃用的 interact,2.0 移除)。
  • defaultSceneTheme / createSceneTheme(partial) — 中性默认主题与深合并辅助。

最小使用示例

import { Canvas } from '@react-three/fiber'
import { gameEvents } from '@overworld-engine/core'
import {
  SceneShell,
  Player,
  Portal,
  useInteractKey,
  useSceneStore,
  createSceneTheme,
  type NPCConfig,
} from '@overworld-engine/scene'

const theme = createSceneTheme({ npc: { primaryColor: '#ff9f43' } })

const npcs: NPCConfig[] = [
  { id: 'guide', name: '向导', modelPath: '/models/guide.glb',
    position: [4, 0, 2], rotation: [0, Math.PI, 0] },
]

// 任意系统都可以订阅交互事件,无需 import 场景组件
gameEvents.on('entity:interact', ({ kind, id }) => {
  if (kind === 'npc') console.log('开始对话:', id)
})

function World() {
  return (
    <SceneShell
      theme={theme}
      npcs={npcs}
      npcIndicators={{ guide: 'quest-available' }}
      player={<Player bounds={{ minX: -24, maxX: 24, minZ: -24, maxZ: 24 }} />}
    >
      {/* 场景专属内容 */}
      <ambientLight intensity={0.6} />
      <mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
        <planeGeometry args={[50, 50]} />
        <meshStandardMaterial color="#2d3436" />
      </mesh>
      <Portal position={[0, 0, -20]} targetScene="downtown" label="市中心" />
    </SceneShell>
  )
}

export function Game() {
  useInteractKey('e')
  const scene = useSceneStore((s) => s.currentScene)
  return (
    <Canvas shadows camera={{ position: [0, 10, 30], fov: 50 }}>
      {scene !== 'downtown' ? <World /> : null /* 其他场景 */}
    </Canvas>
  )
}

模型加载语义

无美术资产也能跑:省略 modelUrl / modelPath 时,玩家与 NPC 回退为胶囊体、 建筑回退为盒体、传送门回退为发光圆环。有模型路径时的完整语义:

  • 加载中 — 显示同一个主题化占位体(BaseNPC / BaseBuilding / Player 内置 <Suspense>,useModelLoader 会正常挂起而不是吞掉 promise), 加载完成后模型自动出现,无需预加载。
  • 加载失败(404 / 解析错误)— 打印一条 console.error 并永久显示占位体; 组件内部的 ModelErrorBoundary 按模型 URL 作 key,修改路径即可重试。
  • preloadSceneModels — 纯性能优化:预加载后 useGLTF 从缓存同步解析, 跳过占位体闪现;不预加载也完全正确。

NPC 回退胶囊与名牌 / 角标 / 交互气泡高度随 scale 等比缩放(基准:NPC 默认 scale = 2.5,建筑基准 scale = 1,默认值下与旧版完全一致); labelHeight prop 可覆盖名牌高度(角标与气泡保持其上方的比例间距)。 纯数学部分以 npcVisualHeights(scale, labelHeight?) / buildingVisualHeights(scale, labelHeight?) 导出。

性能预设

面向移动端 / 低端设备的渲染质量分档:一个 zustand 单例存当前 QualitySettings,<ApplyQuality /> 挂在 Canvas 内负责把 GL 相关的部分 (DPR、阴影开关)应用到渲染器,其余数值由游戏自己消费。

  • QUALITY_PRESETS — 三档内置预设:

    | 档位 | dpr | 阴影 | shadowMapSize | 粒子倍率 | | --- | --- | --- | --- | --- | | high | [1, 2] | 开 | 2048 | ×1 | | medium | [1, 1.5] | 开 | 1024 | ×0.6 | | low | [0.75, 1] | 关 | 512 | ×0.3 |

  • useQualityStore{ preset, settings, setPreset(name), setSettings(partial) }。 默认 high;setSettings 合并部分覆盖并把 preset 置为 'custom'不持久化——玩家的画质选择由游戏自己存(localStorage / 存档槽)。

  • detectQualityPreset() — 设备启发式:无 navigator(SSR / 测试)返回 'high';"弱设备" = hardwareConcurrency ≤ 4 或 deviceMemory ≤ 4GB (两者都有守卫,缺失不计);"移动端" = 粗指针(pointer: coarse)或移动 UA。 移动 + 弱 → low,移动 → medium,桌面 + 弱 → medium,其余 → high。 只是起点,把结果喂给 setPreset 并允许玩家覆盖。

  • <ApplyQuality /> — 挂在 <Canvas> 内:把 settings.dpr 作为 [min, max] 区间交给 R3F 的 setDpr(真实 devicePixelRatio 被钳制进该 区间);切换 gl.shadowMap.enabled 并置 gl.shadowMap.needsUpdate = true。 注意:shadowMapSize 不会被自动应用——投影灯光归游戏所有,自己在创建 灯光处读值(shadow-mapSize={[size, size]});运行中开关阴影后,阴影关闭期间 创建的材质可能需要 needsUpdate / 重挂载,尽量在场景挂载前定档。

  • useParticleMultiplier() — 读当前粒子倍率的便捷 selector。

import { ApplyQuality, detectQualityPreset, useQualityStore, useParticleMultiplier } from '@overworld-engine/scene'

useQualityStore.getState().setPreset(detectQualityPreset()) // 启动时定档

function World() {
  const shadowMapSize = useQualityStore((s) => s.settings.shadowMapSize)
  const particles = Math.round(200 * useParticleMultiplier())
  return (
    <Canvas shadows>
      <ApplyQuality />
      <directionalLight castShadow shadow-mapSize={[shadowMapSize, shadowMapSize]} />
      {/* 用 particles 决定粒子数量 */}
    </Canvas>
  )
}