Skip to content

Repository files navigation

GhostWorld

Raycasting 3D engine + metaverse server + map editor + AI Agent platform.

从 2D 网格地图渲染第一人称视角。提供引擎库、地图编辑器、多人元宇宙、AI Agent 平台。

┌─────────────────────────────────────────────────────────────────────────┐
│                        GhostEngine Metaverse v2                         │
│                    同进程架构 · 无网络 · 共享 WorldState                  │
└─────────────────────────────────────────────────────────────────────────┘

                              ┌───────────────┐
                              │  地图编辑器    │
                              │  (PySide6)    │
                              │               │
                              │ 传送门全地图扫描│
                              │ 跨图即时双向配对│
                              │ 墙壁-实体互斥  │
                              │ 定时自动保存   │
                              │ 越界幽灵清理   │
                              └──────┬────────┘
                                     │ save/load + validate
                                     ▼
                           ┌─────────────────┐
                           │   .json 地图文件 │
                           │ examples/*.json │
                           └────────┬────────┘
                                    │
              ┌─────────────────────┼─────────────────────┐
              ▼                     ▼                     ▼
   ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐
   │   launch.py      │  │   runner.pyw     │  │ headless_agent.py│
   │   launcher.py    │  │ 单机预览(Ctrl+R)  │  │   无头调试模式    │
   └────────┬─────────┘  └────────┬─────────┘  └────────┬─────────┘
            │                     │                     │
            └─────────────────────��─────────────────────┘
                                  │
                                  ▼
   ┌──────────────────────────────────────────────────────────────────────┐
   │                         metaverse 核心                               │
   │  ┌─────────────┐  ┌─────────────┐  ┌──────────────────────────────┐  │
   │  │local_client │  │local_agent  │  │          server.py           │  │
   │  │  (pygame)   │  │  (omp)      │  │   say/move/goto/turn/pos     │  │
   │  │ 跨图深拷贝   │  │ 0.3s 轮询   │  │   pickup/place/give/look     │  │
   │  └──────┬──────┘  └──────┬──────┘  │  set_entity/edit_map/set_cell│  │
   │         └────────┬───────┘         │   传送门坐标越界保护           │  │
   │                  ▼                 └──────────────┬───────────────┘  │
   │           ┌──────────────────┐                    │                  │
   │           │    WorldState    │ ← 唯一权威数据源    │                  │
   │           │ grid/items/maps  │   碰撞检测 · A*寻路 │                  │
   │           │ avatars/inv      │   传送门 · 拾取     │                  │
   │           │ 深拷贝隔离Item    │   越界幽灵自动清理  │                  │
   │           └──────────────────┘                    │                  │
   └───────────────────────────────────────────────────┼──────────────────┘
                                                       │
   ┌───────────────────────────────────────────────────┼──────────────────┐
   │                       ghostengine 渲染引擎         │                  │
   │  ┌────────────┐  ┌────────────┐  ┌────────────┐   │  ┌────────────┐  │
   │  │ renderer   │  │ entity     │  │ animation  │   │  │ minimap    │  │
   │  │ 7阶段管线   │  │ 投影数学   │  │ 悬浮/脉动   │   │  │ 小地图渲染  │  │
   │  │ 射线投射    │  │ 遮挡裁剪   │  │ 旋转/GIF    │   │  │            │  │
   │  └────────────┘  └────────────┘  └────────────┘   │  └────────────┘  │
   └──────────────────────────────────────────────────────────────────────┘

完整架构图见 docs/ARCHITECTURE.txt

文档 内容
docs/HANDOFF.md 现状与待办(接手先看这份)
docs/ARCHITECTURE.txt 完整架构图、数据流、命令通道
docs/SPEC.md 需求对照(v2 完成项)
docs/PROTOCOL-agent-channel.md Agent 通道协议(给别的程序接入用)
docs/DESIGN-agent-channel.md Agent 通道重写设计(已确认)
docs/PLAN-agent-channel.md Agent 通道重写实施计划(14 个任务已完成)

安装

环境要求:Python 3.10 – 3.13。Python 3.14 暂不支持(pygame 尚无预编译包)。

pip install git+https://github.com/Offblink/GhostWorld.git

依赖:pygame, numpy——上面那条命令会自动装(pip install -e . --no-deps 那种才要自己补)。 编辑器额外需要 PySide6:pip install "ghostworld[editor] @ git+https://github.com/Offblink/GhostWorld.git", 或在克隆目录里 pip install -e ".[editor]"([full] 再加 Pillow)。

装完先跑一次它:会打印装在哪儿、运行时往哪儿写(装在 Program Files 这类只读位置会起不来,这里一眼看得出), 以及该填进 Fungi 的 ghostworld_dir 那一行:

ghostworld --where          # 等价:python -m metaverse.launch --where

常见安装问题

症状 原因 解决
ModuleNotFoundError: No module named 'distutils.msvccompiler' Python 3.14 移除了 distutils,pygame 尚未适配 降级到 Python 3.12 或 3.13
SSL: UNEXPECTED_EOF_WHILE_READING 中国大陆网络无法直连 GitHub / SDL 下载源 设置代理 set HTTPS_PROXY=http://127.0.0.1:7890 后重试
pygame.error: No available video device 无头环境(SSH / Docker / WSL 无桌面) 确认有图形环境;纯命令行使用需装 pygame 后调用引擎 API 不创建窗口
pygame 源码编译失败 缺少 Visual Studio Build Tools 先 pip install pygame 安装预编译 wheel,或安装 VS Build Tools

中国大陆用户建议:如 pip install git+https:// 速度极慢或超时,先设置代理再执行安装命令。

打包成 Windows exe(目标机器不用装 Python)

python tools/build_exe.py --zip     # -> dist/GhostWorld/ 和 dist/GhostWorld-<版本>-win64.zip

出一个文件夹、三个 exe(共用同一个 _internal/):

exe 控制台 用途
GhostWorld.exe 无(窗口版) 双击 = GUI 启动器;--play [地图.json] = 直接进游戏;--editor [目录] 也还能用
GhostWorldEditor.exe 无(窗口版) 地图编辑器:双击就开,图标和任务栏按钮都是它自己的那枚
GhostWorldCLI.exe 有 send '<json>'、wait [--timeout N] [--follow]、where、play

通道 CLI 必须是单独一个控制台 exe:Fungi 靠子进程运行它并读它的 stdout 来驱动角色, 窗口版进程没有 stdout,那条线会静默失效。三个 exe 共用一个 _internal/,所以第三个只多出自己的 启动器和字节码包,不会多带一份 Qt。

事实 说明
运行时写哪儿 打包版写 %LOCALAPPDATA%\GhostWorld\(地图、states/、.channel.json、两个 jsonl、snapshots/);源码检出仍写在代码旁边。exe 所在目录可以只读,装在 Program Files 也没事
首次启动 把内置的两张 demo 图拷进用户目录,之后只补不改(你改过的地图不会被覆盖)
版本号 exe 里带着 pyproject.toml,所以 --where 报的是它打包时的版本,不是残留的 pip 元数据
更新 打包版不 pip install 自己:_update_check 会提示去下载新 exe
接 Fungi ghostworld_dir 指向 exe 所在目录,通道命令换成 GhostWorldCLI.exe send/wait;GhostWorldCLI.exe where 会把这一行直接打出来

图标两枚,故意不一样(���个窗口会在任务栏挨着):

文件 用在哪 长什么样
assets/ghostworld.ico 启动器 / 游戏 / 两个 exe 的内嵌图标 👻 在夜雾色圆角方块上(emoji 字形,Qt6 才有彩色)
assets/ghostworld-editor.ico 编辑器窗口 画出来的:编辑器自己那套墙壁调色板摆成 3×3,中心格空着、套上选中框 #0af

两枚都是 16/24/32/48/64/128/256 逐档渲染(不是把大图缩小),小尺寸另有简化画法(编辑器 16/24 px 用 2×2 四块)。 重新生成:

python tools/make_icon.py --app launcher        # assets/ghostworld.ico + .png
python tools/make_icon.py --app editor          # assets/ghostworld-editor.ico + .png

更新

pip cache purge && pip install --upgrade git+https://github.com/Offblink/GhostWorld.git

(打包版不适用——它没有 pip;_update_check 对 exe 会改成提示下载新版 exe。)

⛔ 开发禁令:禁止 WebSocket 协议与 asyncio 网络 I/O;允许 127.0.0.1 上的同步 socket + 线程向队列投递。 Windows 上 WebSocket 存在未修复的严重 bug,曾导致项目崩溃、数据丢失;MCP 同样禁用。 病因是「Windows WS + 异步网络 + 跨线程改世界」,所以还有一条硬约束:WorldState 只在帧循环 线程里被修改,通道线程只能把命令投进 cmd_queue、把事件投进 EventBus。

缓存策略

启动时 launch.py 设置 sys.dont_write_bytecode = True,全程不产生 .pyc 文件。 每次运行从 .py 源码直接编译,彻底杜绝 __pycache__ 导致的旧代码污染。 如需手动清理历史残留,运行 pyclean(项目附带)。

一键启动元宇宙

python -m metaverse.launch                        # 默认地图
python -m metaverse.launch my_map.json             # 指定地图
python launcher.pyw                                # GUI 启动器(需 PySide6)
python headless_player.py                          # 无头联调:服务 + 一个会说活的玩家
python headless_player.py my_map.json --say 你好 --after 3

examples/ 里有两张互相配对的 demo 图(demo_metaverse.json ↔ demo_metaverse2.json):走到门口就会被送到对面那张的门口, 再走回去也能回来。

其中 demo_metaverse.json 是一张元素样板图:16×16,三进的室内(每进之间一道墙、各留一个窄门),一进摆 8 种 墙型(各一块,交错成两排)、二进是内柱与物品、三进深处才到那扇门。元素覆盖:可拾取物品与 pickup_label、三种动画 (float / pulse / rotation)、两种遮挡(center / per_column)、capture_for、metadata、隐形实体、 带 dialogue 的 NPC。配色是它自己那套夜雾幽绿(天空/地板/八种墙色都与隔壁那张不同)。没有贴图资源,实体按引擎的 几何画法渲染;那扇门通 demo_metaverse2.json——对面是一间 10×10 的明亮小房间(亮天蓝 + 亮草绿), 里面只有一扇回程门,走回去就回到这里。

Agent 控制(通道)

Agent(omp、别的 harness、任何 bot)通过本机通道驱动自己的角色:游戏进程内起一个 127.0.0.1 的 socket 服务,端口与 token 落在 metaverse/.channel.json。命令不用轮询, 发一条拿一个 ack;等唤醒是阻塞的:

ghostworld-send '{"cmd":"pos"}'                    # 打印 ack JSON,退出码 0
ghostworld-send '{"cmd":"say","message":"来了"}'
ghostworld-wait --timeout 25                       # 阻塞:玩家一发言就打印一行事件 JSON
ghostworld-wait --all --follow                     # 常驻观察者(含 see/goto_done 等非唤醒事件)

两个 CLI 的 stdin/stdout/stderr 一律 UTF-8(与 Windows 控制台代码页无关),事件行一出就落地—— 逐行读、直接按 UTF-8 解码即可,中文不会变乱码。

唤醒语义:Agent 常驻 ghostworld-wait,玩家发言即被唤醒;不发言时零开销(阻塞在 socket 上, 不轮询、不烧 CPU、不烧 token)。思考期间玩家说的第二句会在通道里排队,下次 wait 一次性拿到。玩家按下空格暂停、或正在聊天输入框里打字时,客户端不跑帧循环,命令会被当场回绝({"type":"error","reason":…},退出码仍是 0)——那是暂停,不是通道坏了。

退出码 ghostworld-send ghostworld-wait
0 收到 ack 打印了事件
1 连上了但没有 ack(帧循环没在跑) —
2 连不上(游戏没在跑)/用法错误 同左
3 — 超时:期限内没有事件

命令的 JSON 与下面的文件通道完全一致(cmd + 参数)。旧文件通道仍可用:向 metaverse/agent_commands.jsonl 写一行 JSON,agent 每 0.3s 读一次并交给帧循环执行(兼容层, 写进去的命令不再有读后即删的竞态)。

{"cmd":"say","message":"hello"}
{"cmd":"move","x":10,"y":3}
{"cmd":"goto","x":5,"y":5}
{"cmd":"turn","x":10,"y":5}
{"cmd":"pos"}
{"cmd":"inv"}
{"cmd":"look"}
{"cmd":"pickup"}
{"cmd":"place","item_id":"gem_B","x":3.5,"y":3.5}
{"cmd":"give","target":"player","item_id":"token"}
{"cmd":"edit_map","operations":[
  {"op":"set_cell","x":5,"y":5,"wall":1},
  {"op":"set_grid","x":0,"y":0,"grid":[[0,0],[0,0]]},
  {"op":"set_color","key":"sky_top","rgb":[60,60,130]},
  {"op":"set_entity","id":"portal_north","prop":"portal_target","value":{"x":10,"y":7}},
  {"op":"delete_entity","id":"old_portal"},
  {"op":"reload_maps"},
]}
{"cmd":"post_issue","caption":"screenshot","filepath":"snapshots/omp_1234567890.png"}
{"cmd":"snapshot","caption":"view"}
{"cmd":"dump_map"}

dump_map 输出当前地图的完整状态:网格矩阵、items、avatars、预加载地图列表。适合调试跨地图传送。

监听玩家消息

python metaverse/tools/listen.py --once       # 单次扫描,打印玩家新消息
python metaverse/tools/listen.py              # 持续 tail(默认每 5 秒)

玩家在游戏里说的话 → agent_output.jsonl 的 heard 事件(kind=wake)。 推荐直接用 ghostworld-wait 阻塞等唤醒——那是零轮询的,listen.py 留给 grep/看日志的场景。 listen.py 的进度按事件 seq 记在 tools/listen_cursor.json(不再用行号), 日志被轮转或被新进程重启都不会丢事件、也不会重放。

按键(人类客户端)

按键 功能
W S 前进 / 后退
A D 左 / 右平移
← → 左右转向
鼠标 转动视角
M 小地图开关
F 全屏切换
Enter 打开聊天输入框(打字期间世界与命令通道一起暂停)
Space 暂停 / 继续(屏幕中间显示 PAUSED;暂停期间 Agent 的命令会被回绝)
L Agent 手电筒开关(小地图绿色锥形光照,默认开)
Esc 退出

引擎 API

from ghostengine import Frame, PlayerView, EntityView, ColorConfig, render
import numpy as np

frame = Frame(
    player=PlayerView(x=5, y=5, angle=0, pitch=0),
    walls=np.zeros((10, 10), dtype=int),
    entities=[],
    colors=ColorConfig(),
)
surface = pygame.display.set_mode((800, 600))
render(frame, surface)

主要导出:

符号 说明
render(frame, dst) 核心渲染函数,无状态,每帧调用一次
Frame / PlayerView / EntityView 一帧的完整世界描述
ColorConfig / WallDef / FogConfig 天空、地板、墙壁颜色和雾效配置
FirstPersonController 第一人称控制器,含碰撞检测
AnimState / compute_animation() 动画引擎(悬浮 / 脉动 / 旋转 / GIF 帧)
TextureLoader 纹理加载器,带 LRU 缓存
load_raw() / save_raw() 地图 JSON 读写
draw_minimap() 小地图渲染

地图编辑器

python editor.pyw [项目目录]

左侧栏

  • 地图列表:列出项目 .json 地图,双击加载,Delete 删除
  • 场景颜色:天空顶部/底部、地板取色器

右侧栏(互斥显示)

  • 墙壁属性:墙壁类型(1–8)+ 颜色/贴图
  • 物品属性:可拾取、拾取标签、动画(悬浮/脉动/旋转)、贴图、遮挡模式
  • 精灵属性:名称、归属、朝向贴图(四向选择)
  • 传送门属性:ID 显示 + 目标传送门下拉框(扫描项目全部地图的全部传送门,格式 [地图名] ID);已配对灰显标注;跨图点击即双向配对;换目标自动断旧配对

画布图例

颜色 实体类型
🟣 紫色 传送门
🟡 金色 物品
🔵 蓝色 NPC / 精灵
⚪ 青色圆点 + 白线 玩家出生点朝向

操作

  • 左键 = 当前工具主操作;右键 = 擦除墙壁+实体;拖拽 = 连续放置/擦除
  • 墙壁与实体互斥:有墙处不能放实体,有实体处不覆盖(提示"��置已被占用")
  • 加载时自动检测越界幽灵实体并清理
  • Ctrl+Z/Y 撤销/重做;Delete 删除选中;Ctrl+S 保存(未命名弹出对话框);Ctrl+Shift+S 另存为
  • Ctrl+R 用预览器打开;编辑器启动时恢复上次地图
  • 每 3 秒自动保存已命名地图

地图 JSON 格式 (v3)

{
  "version": 3,
  "grid": [[0,1,0,1,0], …],
  "player_spawn": {"x": 7.5, "y": 7.5, "angle": 0.0},
  "entities": [
    {
      "x": 10.0, "y": 7.0,
      "kind": "avatar", "name": "", "owner": "",
      "facing": 0.0,
      "use_facing": false, "textures": {},
      "size_3d": 800, "width_3d": 0.8,
      "anim": {"float": {"speed": 0.003, "amp": 0.05}},
      "occlusion": "per_column", "texture": "",
      "capture_for": "", "portal_target": null,
      "metadata": {}
    },
    {
      "x": 7.5, "y": 7.5,
      "kind": "item", "pickup": true, "pickup_label": "卷轴",
      "size_3d": 140, "width_3d": 0.1,
      "occlusion": "center", "texture": "",
      "capture_for": ""
    },
    {
      "x": 14.5, "y": 7.5,
      "kind": "portal", "id": "portal_0",
      "portal_target": {"portal_id": "portal_1", "map": "other.json"},
      "size_3d": 150, "width_3d": 0.2,
      "occlusion": "center"
    }
  ],

元宇宙模块

文件

文件 作用
launch.py 一键启动。同时启动服务器、人类客户端、agent
local_client.py 人类客户端。pygame 渲染第一人称视角,WASD 移动,Enter 聊天,Space 暂停,M 小地图
channel.py 事件总线:单调 seq + 有界缓冲 + Condition;零 IO,跨线程唯一入口
channel_server.py 通道服务:127.0.0.1 行 JSON socket;线程只碰 cmd_queue 与 EventBus
channel_client.py 通道客户端:纯 stdlib、可整文件拷走给别的项目用
cli_channel.py 两个 CLI 入口(ghostworld-send / ghostworld-wait)与退出码
local_agent.py Agent。同进程运行:读 agent_commands.jsonl(兼容层)投队列;事件走 EventBus,由 FileSink 落 agent_output.jsonl
headless_player.py 联调夹具:无头起服务,并连一个人类玩家(可用 --say 让它说一句话)——给外部 Agent 试通道用
launch_config.json 启动配置:玩家名、agent 名、贴图路径

命令

命令 说明
say 发言(global channel)
move 移动到指定坐标,服务器校验碰撞
goto A* 自动导航到目标
turn 转向面对指定坐标(一次性)
track 持续面向目标 avatar/item
pos 查询当前位置和所在地图
pickup 远程拾取:x,y(必填),可选 item_id
place 从背包取出物品放到指定坐标
give 从背包取出物品丢脚下,设 capture_for
snapshot 从角色自己的位置与朝向前视渲染一张 PNG(���空 / 地面 / 建筑 / 视野里的人),存 snapshots/;ack 回 {"type":"snapshot_done","local":"<路径>"}
post_issue 拍照并发 GitHub Issue(需设 GHOSTENGINE_REPO 环境变量)
dump_map 调试:矩阵格式输出完整地图状态
edit_map 批量编辑:set_cell / set_grid / set_color / reload_maps
set_entity 创建/修改/删除实体:id,x,y,kind,pickup,pickup_label,visible,delete
set_cell 修改单个墙壁:x,y,wall

set_entity 示例:

{"cmd":"set_entity","id":"gem_1","x":5.5,"y":5.5,"kind":"item","pickup":true,"pickup_label":"宝石"}
{"cmd":"set_entity","id":"gem_1","prop":"pickup_label","value":"新名字"}
{"cmd":"set_entity","id":"gem_1","delete":true}

pickup 远程拾取:

{"cmd":"pickup","x":7.0,"y":5.5}
{"cmd":"pickup","x":7.0,"y":5.5,"item_id":"test_gem"}

已知限制

问题 说明
不要发表情 pygame 字体不支持 emoji,显示乱码

测试

pytest tests/ -q --ignore=tests/scratch

154 个测试,覆盖引擎渲染、实体投影、碰撞检测、地图 I/O、WorldState、Server 协议、跨地图传送、传送门配对/取消/重配对、编辑器验证(越界清理/墙壁重叠)、Item 深拷贝隔离,以及 Agent 通道(事件总线游标/容量/并发、服务端线程边界、端到端唤醒与排队、 暂停或打字时的诚实回绝、跟随子进程的 flush 与 UTF-8 编码、旧文件通道兼容、listen 游标轮转/重启、goto 到脚下坐标不再除零崩帧循环、安装布局与单实例锁的辨识)。

ruff check .                    # 代码门禁(配置在 pyproject.toml)

About

Only 0.56MB, 3D Engine + Editor + Metaworld

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages