CD Code Digest Hub /GTA V · v2

内景与门户:CInteriorProxy、CInteriorInst、CPortalTracker

45 分钟阅读 13 个章节 2 张图

本页解释 GTAV 的内景(MLO,Multi Level Object)系统:内景如何由 IMAP 中的 CMloInstanceDef 产生一个常驻的轻量代理 CInteriorProxy 和一个按需实例化的重量级 CInteriorInst;内景按“无 → 外壳 → 全部内容 → 带碰撞”分级实例化的状态机;房间/门户如何映射到框架场景图供可见性遍历;动态实体如何用 CPortalTracker 追踪自己在哪个内景的哪个房间;以及实体集(entity set)、保留列表(retain list)、内景组、LayoutManager 等配套机制。

背景与相关页面#

相关源码文件#

文件内容
game/scene/portals/InteriorProxy.h/.cpp常驻代理、实例化状态机、实体集、保留列表、cacheloader
game/scene/portals/InteriorInst.h/.cpp实例:房间数据、子场景图、外壳/内容填充、对象增删
game/scene/portals/PortalInst.h/.cpp外部门户实体 CPortalInst(连接外景或链接两个内景)
game/scene/portals/Portal.h/.cppCPortal:每帧内景激活、活动内景列表、内景 FX 级别、环境光
game/scene/portals/PortalTracker.h/.cppCPortalTracker、CPortalVisTracker
game/scene/portals/InteriorGroupManager.*内景组(地铁/隧道等)的出口门户登记
game/scene/portals/LayoutManager.*、LayoutManagerParser.psc脚本驱动的内景布置(家具布局)
game/modelinfo/MloModelInfo.*、game/scene/loader/MapData_Interiors.pscMLO archetype:房间、门户、实体、实体集、时间周期修改器
game/scene/portals/FrustumDebug.*、portalDebugHelper.*调试

1. 核心概念与对象关系#

证据:CMapDataContents::ConstructInteriorProxies 为 CMloInstanceDef 调用 CInteriorProxy::Add/Overwrite src/dev_ng/game/scene/loader/MapData.cpp:534-568;代理持有 CInteriorInst* src/dev_ng/game/scene/portals/InteriorProxy.h:140-141;CInteriorInst : CBuilding、持有代理与 CMloModelInfo src/dev_ng/game/scene/portals/InteriorInst.h:96-108、src/dev_ng/game/scene/portals/InteriorInst.h:341-342;CPortalInst : CBuilding src/dev_ng/game/scene/portals/PortalInst.h:18;外部门户实例数组 src/dev_ng/game/scene/portals/InteriorInst.h:383-384;追踪器字段 src/dev_ng/game/scene/portals/PortalTracker.h:78-80;CMloModelInfo::CreateEntity 返回 CInteriorInst src/dev_ng/game/modelinfo/MloModelInfo.cpp:226-233。

为什么分代理与实例:地图上有成百上千个内景,CInteriorProxy 只保存位置、包围体、组 ID、出口门户数、静态包围盒槽位等少量数据并常驻;完整的房间/门户/内容实体只在玩家靠近时才实例化。代理的池索引就是 fwInteriorLocation 中的内景索引,所以代理顺序必须在所有机器上一致(见 1.2 节)。

1.1 fwInteriorLocation#

fwInteriorLocation 是 4 字节联合体:s16 m_interiorIndex(代理池索引,上限 INTLOC_MAX_INTERIOR_INDEX=1200)+ 1 位“挂在门户上” + 1 位“门户通向外景” + 14 位房间或门户索引;房间 0 是“limbo”(内景外壳以外的部分),每个内景最多 32 个房间、256 个门户,房间 0 最多 12 个实体 src/dev_ng/rage/framework/src/fwscene/world/InteriorLocation.h:6-68。游戏侧 MAX_ROOMS_PER_INTERIOR=32、MAX_SHELL_ENTITIES=64 src/dev_ng/game/scene/portals/InteriorInst.h:26-29。实体用 CInteriorProxy::CreateLocation(proxy, room) 生成位置,CInteriorInst::GetInteriorForLocation 反查实例 src/dev_ng/game/scene/portals/InteriorProxy.h:212-218、src/dev_ng/game/scene/portals/InteriorInst.h:421-423。

1.2 代理的创建与稳定排序#

CInteriorProxy::Add 先查 m_orderData(来自 INTERIOR_PROXY_ORDER_FILE 的 SInteriorOrderData{startFrom, filePathHash, proxies[]}):若该 IMAP 在顺序表中,就在池的固定下标处 placement-new 代理,并记录当前执行的 DLC 变更集;否则普通分配——DLC 执行期间出现未排序代理会断言“将导致代理顺序不一致” src/dev_ng/game/scene/portals/InteriorProxy.h:25-46、src/dev_ng/game/scene/portals/InteriorProxy.cpp:220-300。关卡装载末尾 CInteriorProxy::Sort() + AddAllDummyBounds(0) src/dev_ng/game/scene/FileLoader.cpp:2391-2392。这表明稳定的代理索引是网络同步内景位置的前提(推断:联机时 fwInteriorLocation 以代理索引传输,网络侧细节见 ../17-network-sync/sync-trees-and-serialisation.md)。

InitFromMLOInstanceDef 从实例定义读取出口门户数、组 ID(断言 <255)、名称、位置与旋转;包围球由 IMAP 物理范围膨胀 10m;还会定位音频遮挡元数据文件槽位。若干隧道内景被硬编码为 0 个出口门户,ch_dlc_arcade 设置“添加实体时重建灯光” src/dev_ng/game/scene/portals/InteriorProxy.cpp:423-499。代理也可写入/读取 cacheloader 缓存(CInteriorProxyCacheEntry 存组、楼层、出口数、名字哈希、位置、四元数、包围盒)src/dev_ng/game/scene/portals/InteriorProxy.h:56-74。

2. 实例化状态机#

2.1 状态与请求模块#

CInteriorProxy 有 4 个实例化状态与 8 个请求来源 src/dev_ng/game/scene/portals/InteriorProxy.h:96-119:

状态含义
PS_NONE未实例化
PS_PARTIAL外壳:房间 0 与外部门户上的实体
PS_FULL全部内容
PS_FULL_WITH_COLLISIONS全部内容且碰撞已就绪

请求模块:RM_VISIBLE(主视锥)、RM_PT_PROXIMITY(追踪器接近)、RM_GROUP(组策略)、RM_SCRIPT、RM_CUTSCENE、RM_LOADSCENE、RM_SHORT_TUNNEL;STATE_COOLDOWN=15 帧。

SetRequestedState(module, state, cooldown) 用位掩码记录每个模块的请求,把代理加入 ms_immediateStateUpdateList;请求升级时更新 IMAP 固定(pin)状态;禁用的代理忽略请求 src/dev_ng/game/scene/portals/InteriorProxy.cpp:1204-1232。

ImmediateStateUpdate 对列表中的每个代理(冷却中的跳过)取所有模块请求的最高状态作为目标,但每次只向目标移动一级 src/dev_ng/game/scene/portals/InteriorProxy.cpp:1422-1473:

证据:src/dev_ng/game/scene/portals/InteriorProxy.cpp:1476-1609。PS_PARTIAL→PS_FULL 若内景已填充(经历过 FULL→PARTIAL→FULL)则只改状态 src/dev_ng/game/scene/portals/InteriorProxy.cpp:1549-1552。非法转换断言。CleanupInteriorImmediately 清空所有请求并循环推进直到 PS_NONE src/dev_ng/game/scene/portals/InteriorProxy.cpp:1235-1251。

IMAP 固定:UpdateImapPinState 综合脚本请求、过场请求、LoadScene 需要以及玩家/活动组/观战相机等条件(局部变量 bPinnedByScript/Cutscene/LoadScene/Player/PlayerInGroup/ActiveGroup/SpectatorCam),把内景所在 IMAP 在 box streamer 中设为用户固定(pinned),内景关闭时解除 src/dev_ng/game/scene/portals/InteriorProxy.cpp:1265-1300。这防止玩家所在内景的宿主 IMAP 被流出。

2.2 谁在请求:CPortal::Update#

CScene::Update 每帧首先调用 CPortal::Update src/dev_ng/game/scene/scene.cpp:793-794,其流程 src/dev_ng/game/scene/portals/Portal.cpp:442-665:

  1. 渲染锁定时直接返回;确保玩家追踪器能激活内景;CInteriorProxy::ImmediateStateUpdate() 处理状态变化 src/dev_ng/game/scene/portals/Portal.cpp:446-455。
  2. 收集“激活中的追踪器”(CPortalTracker::GetActivatingTrackerList(),最多 1024 个;带 owner 的需 IsActivatingCollisions)src/dev_ng/game/scene/portals/Portal.cpp:489-506。
  3. 遍历代理池(PORTAL_UPDATE_NUM_FRAMES_TO_PROCESS=1,即每帧全量):
    • 组 0(普通内景):任一追踪器的视锥检查通过 → RM_VISIBLE 请求 PS_FULL;任一追踪器在 5m 球内接近代理包围球 → RM_PT_PROXIMITY 请求 PS_FULL_WITH_COLLISIONS;否则对应模块请求 PS_NONE src/dev_ng/game/scene/portals/Portal.cpp:555-588、src/dev_ng/game/scene/portals/Portal.cpp:279-291。视锥检查只用 vis 追踪器(相机),条件是距离 < 人口距离+15m 且代理包围球与视口相交,或者距离 < 包围半径 + PORTAL_INSTANCE_DIST/3 src/dev_ng/game/scene/portals/Portal.cpp:229-277。
    • 组 1:组检查 → RM_GROUP;其他组:短隧道检查 → RM_SHORT_TUNNEL;两者退出时带冷却 src/dev_ng/game/scene/portals/Portal.cpp:589-621。
    • 激活了“HeistIsland”岛屿时,5 个航母内景被硬编码跳过(其组 ID 异常为 100)src/dev_ng/game/scene/portals/Portal.cpp:517-544。
  4. 遍历实例池:m_bInUse 的实例调用 CInteriorInst::Update,并记录到相机距离;最后计算内景 FX 级别 src/dev_ng/game/scene/portals/Portal.cpp:634-656。

此外 CLoadScene(RM_LOADSCENE)、脚本命令(RM_SCRIPT)、过场(RM_CUTSCENE)直接调用 SetRequestedState,例如 LoadScene 结束时以冷却请求 PS_NONE src/dev_ng/game/scene/LoadScene.cpp:189-195。

3. CInteriorInst:实例化与子场景图#

3.1 填充外壳#

PopulateInteriorShell(PS_NONE→PS_PARTIAL)依次 src/dev_ng/game/scene/portals/InteriorInst.cpp:817-848:

  1. GenerateInstanceData:为每个房间分配 CRoomData——活动追踪器列表、门户索引、主/次时间周期修改器索引、6×6 烟雾网格(武器烟雾在室内滞留)、本地水位、包围盒 src/dev_ng/game/scene/portals/InteriorInst.h:50-76、src/dev_ng/game/scene/portals/InteriorInst.cpp:1556-1636。
  2. InitializeSubSceneGraph:创建 fwInteriorSceneGraphNode(内景索引 = CInteriorInst 池索引)挂到世界场景图根;每个房间一个 fwRoomSceneGraphNode(外景可见深度、强制方向光、不渲染外景等房间标志);每个门户一个 fwPortalSceneGraphNode:角点变换到世界、不透明度、单向/链接/仅低 LOD/可关闭、镜面/镜面地板/水面类型;门户负端是房间,正端是另一房间、外景根节点,或(链接门户)暂空待另一内景实例化后填入 src/dev_ng/game/scene/portals/InteriorInst.cpp:3604-3750。
  3. PopulateMloShell:若内景有 LOD 父实体,复制一份“内部 LOD”实体放入房间 0(并在反射中隐藏,个别内景有 hack),再 CreateInteriorEntities(pMI, bSelectShell=true, …) 创建房间 0 与外部门户上的实体,外壳资产记入 strRequestArray<MAX_SHELL_ENTITIES> src/dev_ng/game/scene/portals/InteriorInst.cpp:504-594、src/dev_ng/game/scene/portals/InteriorInst.cpp:701-720。
  4. 未封顶时 CreateInterfacePortalInsts(创建房间 0 的 CPortalInst),ProcessCloseablePortals src/dev_ng/game/scene/portals/InteriorInst.cpp:841-845。

HasShellLoaded() 判定外壳请求数组是否全部加载,是进入 PS_FULL 的前提(除非 SkipShellAssetBlocking)src/dev_ng/game/scene/portals/InteriorInst.cpp:803-814。CanReceiveObjects() 仅在外壳填充后为真 src/dev_ng/game/scene/portals/InteriorInst.h:180。

3.2 填充内容#

PopulateInteriorContents(PS_PARTIAL→PS_FULL)调用 PopulateMloContents:创建除外壳外的所有默认实体;校验并创建激活的实体集中的实体(位置编码:最高位为 1 表示挂在门户上,否则为房间号;带实体集色调);把每个 CMloTimeCycleModifier(球形)以 AABB 形式加入 g_timeCycle 并标记来自内景 src/dev_ng/game/scene/portals/InteriorInst.cpp:596-699。随后补建门户实例、在反射中隐藏外部 LOD 父实体、设置音频内景数据 src/dev_ng/game/scene/portals/InteriorInst.cpp:851-891。DepopulateInterior 执行反向清理 src/dev_ng/game/scene/portals/InteriorInst.cpp:1024。

内景内的实体创建也考虑 g_MapChangeMgr 的模型替换(包围球与活动变更相交时)src/dev_ng/game/scene/portals/InteriorInst.cpp:611-615。

3.3 每帧 CInteriorInst::Update#

衰减各房间烟雾;计算相机到外部门户的最近距离;当状态 ≥PS_FULL 且碰撞可用(物理实例或静态包围盒已加载)时,首次调用 CapturePedsAndVehicles(让附近 ped/车/拾取物/投射物/脚本对象的追踪器重新探测以进入内景)并请求重置渲染相位追踪器;同条件下把保留列表中的实体释放回内景;确保内部 LOD 有 drawable src/dev_ng/game/scene/portals/InteriorInst.cpp:1294-1363、src/dev_ng/game/scene/portals/InteriorInst.cpp:1365-1422。

3.4 对象进出内景#

操作实现说明
静态实体加入AppendObjectInstance(CEntity*, loc)由 CEntity::AddToInterior 调用 src/dev_ng/game/scene/Entity.cpp:1591-1609、src/dev_ng/game/scene/portals/InteriorInst.cpp:1654
动态实体加入AddObjectInstance(CDynamicEntity*, loc)房间位置:设置追踪器的内景/房间,AppendEntity 到房间场景节点容器,置 bInMloRoom;门户位置:放入门户节点容器并让追踪器不再处理更新;最后加入房间的活动追踪器列表 src/dev_ng/game/scene/portals/InteriorInst.cpp:1786-1870
位置更新UpdateObjectInstance由 CEntity::UpdateWorldFromEntity 调用 src/dev_ng/game/scene/Entity.cpp:1694-1704
移出RemoveObjectInstance动态实体移出时重置追踪器的房间与代理 src/dev_ng/game/scene/DynamicEntity.cpp:196-214

内景本身不能被加入另一个内景(AddToInterior 断言)src/dev_ng/game/scene/portals/InteriorInst.h:110-111。

3.5 门户实体 CPortalInst#

CPortalInst 代表房间 0 上通向外部(或链接另一内景)的门户,作为普通实体进入外景世界表示,从而在外景扫描/搜索中可被找到。它记录拥有者内景与门户索引(m_pMLO1/m_portalIdx1)、可选的链接内景(m_pMLO2/m_portalIdx2),以及单向、链接、忽略修改器、光渗等标志;FindIntersectingPortals 供追踪器查找附近门户 src/dev_ng/game/scene/portals/PortalInst.h:18-110。门户标志的数据源是 CPortalFlags(INTINFO_PORTAL_ONE_WAY/LINK/MIRROR/MIRROR_FLOOR/WATER_SURFACE/LOWLODONLY/ALLOWCLOSING/USE_LIGHT_BLEED 等)src/dev_ng/game/modelinfo/MloModelInfo.h:42-87。

4. 房间/门户可见性遍历的入口#

可见性扫描本身是框架的 fwScan(沿门户场景图节点递归,按门户角点裁剪视锥);游戏侧决定从哪个节点开始:CGameScan::FindStartingSceneNode 取渲染相位的 CPortalVisTracker 所在内景与房间,有效时从对应 fwRoomSceneGraphNode 开始(内景必须能接收对象),否则从世界场景图根开始 src/dev_ng/game/scene/world/GameScan.cpp:89-120。因此:

  • 相机在内景房间中 → 扫描从房间出发,经门户进入相邻房间或外景(门户正端为外景根节点);
  • 相机在外景 → 从根出发,经外部门户节点(负端为房间)看进内景;
  • 链接门户连接两个内景,正端在另一内景实例化后才被填入 src/dev_ng/game/scene/portals/InteriorInst.cpp:3724-3742。

房间标志 ROOM_DONT_RENDER_EXTERIOR 与“外景可见深度”限制从房间能看多远的外景 src/dev_ng/game/scene/portals/InteriorInst.cpp:3625-3629。内景流式的球形剔除形状(SetupInteriorStreamCullShape)见流式页。

5. CPortalTracker:动态实体的房间追踪#

每个 CDynamicEntity 内嵌一个 CPortalTracker(见实体页);车辆、ped、对象在 CGameWorld::ProcessAfterAllMovement 中调用 UpdatePortalTracker() → m_portalTracker.Update(当前位置) src/dev_ng/game/scene/DynamicEntity.cpp:913-918、src/dev_ng/game/scene/world/GameWorld.cpp:2243。

探测距离常量:CAPTURE_PROBE_DIST=1、SHORT=2、NEAR=4、FAR=16、SUPER_FAR=32、FAR_VIS=64、STADIUM=150,MP 中脚本对象额外 +2m src/dev_ng/game/scene/portals/PortalTracker.h:56-76。

CPortalTracker::Update(pos) 的核心分支 src/dev_ng/game/scene/portals/PortalTracker.cpp:1745-2110:

  1. 是否必须重扫:ScanUntilProbeTrue、RequestRescanNextUpdate、全局强制、捕获请求;等待碰撞时直到 mover 静态包围盒在位置处加载后再重扫;m_bIsExteriorOnly 的对象永不入内景 src/dev_ng/game/scene/portals/PortalTracker.cpp:1749-1770。
  2. 被保留的实体不做增量追踪(除非要重扫);首次更新只建立初始位置;挂在门户上的对象不更新;位移平方 <0.0001 时提前返回 src/dev_ng/game/scene/portals/PortalTracker.cpp:1777-1830。
  3. 重扫路径(过场/回放控制、必须重扫、或单帧位移超过 SPEED_LIMIT=25m):向下发射探测线,命中的静态碰撞材质中编码了房间 ID(PGTAMATERIALMGR->UnpackRoomId),再由静态包围盒槽位反查内景代理(只信任 ENTITY_OWNEDBY_STATICBOUNDS 的命中)src/dev_ng/game/scene/portals/PortalTracker.cpp:970、src/dev_ng/game/scene/portals/PortalTracker.cpp:1875-1879、src/dev_ng/game/scene/portals/PortalTracker.cpp:456-540。探测不确定时,保留中的实体继续留在保留列表 src/dev_ng/game/scene/portals/PortalTracker.cpp:2025-2035。
  4. 增量路径:以新旧位置中点为球心、半径 = 位移 + 0.5m(上限 25m;可跨门户的实体用 AABB 半径)构造测试球,在当前房间的门户中检测穿越(EntryExitPortalCheck),得到目标房间/内景/外景,并处理“跨门户”状态 src/dev_ng/game/scene/portals/PortalTracker.cpp:2077-2110、src/dev_ng/game/scene/portals/PortalTracker.cpp:750。
  5. 位置变化最终由 MoveToNewLocation(destLoc) 落实到世界(从外景移除并加入房间,或反之)src/dev_ng/game/scene/portals/PortalTracker.cpp:858。

CPortalVisTracker 是相机/渲染相位使用的特化:没有 owner,持有 grcViewport,静态记录“主内景代理与房间”(ms_pPrimaryIntProxy/ms_primaryRoomIdx,CGameWorld::SetFreezeFlags 等使用);若所在内景已不能接收对象则先移到外景;UpdateUsingTarget 先把追踪器放到相机目标实体的位置/房间,再从那里更新到相机位置,以穿过中间的门户(允许多个链接门户)src/dev_ng/game/scene/portals/PortalTracker.h:287-346、src/dev_ng/game/scene/portals/PortalTracker.cpp:2750-2805。

网络克隆的内景更新使用一组诊断码 eNetInteriorUpdateCloneCode src/dev_ng/game/scene/portals/PortalTracker.h:30-43。PC 上追踪器另存一份混淆的当前位置,属于安全措施,不展开 src/dev_ng/game/scene/portals/PortalTracker.h:84-86。

6. 保留列表(retain list)#

当内景被降级而其中仍有不能随意删除的动态实体时,这些实体被移到代理的保留列表:MoveToRetainList 把实体移出世界(CGameWorld::Remove(pEntity, true) 保留物理)、标记 IsRetainedByInteriorProxy 与代理索引,并保证需要处理的物理实体仍在 scene update 中 src/dev_ng/game/scene/portals/InteriorProxy.cpp:979-1010。ShouldEntityFreezeAndRetain 依据实体类型、网络克隆/迁移能力等判断(例如网络克隆车辆不保留、无法移交控制权的车辆保留)src/dev_ng/game/scene/portals/InteriorProxy.cpp:1037-1060。内景回到 ≥PS_FULL 且碰撞可用后,ExtractRetainedEntities 让每个实体的追踪器“探测直到成功”并更新,从而回到正确房间 src/dev_ng/game/scene/portals/InteriorProxy.cpp:1013-1034。CGameWorld::Remove 对保留中的实体调用 CleanupRetainedEntity src/dev_ng/game/scene/world/GameWorld.cpp:931-932。

7. 实体集、禁用与封顶#

  • 实体集:MAX_ENTITY_SETS=150,SEntitySet{name, limit, tintIdx};脚本 ActivateEntitySet/DeactivateEntitySet/SetEntitySetTintIndex,RefreshInterior 使改动生效 src/dev_ng/game/scene/portals/InteriorProxy.h:49、src/dev_ng/game/scene/portals/InteriorProxy.h:76-90、src/dev_ng/game/scene/portals/InteriorProxy.h:253-265。实体集定义(CMloEntitySet)在 MLO archetype 中 src/dev_ng/game/modelinfo/MloModelInfo.h:135-139。
  • 禁用:SetIsDisabled(脚本)、SetIsDisabledByCode、SetIsDisabledDLC(DLC 变更集按 IMAP 槽位禁用代理);禁用代理忽略状态请求 src/dev_ng/game/scene/portals/InteriorProxy.h:236-247。
  • 封顶在 PARTIAL:SetIsCappedAtPartial 让内景只实例化外壳(不建外部门户实例,不进入 FULL)src/dev_ng/game/scene/portals/InteriorProxy.cpp:1547、src/dev_ng/game/scene/portals/InteriorInst.cpp:841-844。

8. 内景组与 LayoutManager#

  • 内景组:组 ID 非 0 的内景(地铁、长隧道等)不由视锥/接近检查实例化,而由组策略或短隧道策略决定;CInteriorGroupManager 记录每组的出口门户(RegisterGroupExit/UnregisterGroupExit)及门户包围盒,提供 RayIntersectsExitPortals src/dev_ng/game/scene/portals/InteriorGroupManager.h:13-44。注册在代理进入/离开 PS_FULL 时发生 src/dev_ng/game/scene/portals/InteriorProxy.cpp:1490-1493、src/dev_ng/game/scene/portals/InteriorProxy.cpp:1560-1563。
  • LayoutManager:CLayoutManager 解析布局文件(CMiloInterior → CMiloRoom → CLayoutNode → CGroup → CRsRef,CRsRef 带平移/旋转与商店数据),按组 ID 的激活状态把 CRsRef 引用的模型作为实体(ENTITY_OWNEDBY_LAYOUT_MGR)放入内景;节点句柄由指针数据哈希生成 src/dev_ng/game/scene/portals/LayoutManager.h:24-140、src/dev_ng/game/scene/portals/LayoutManagerParser.psc:5-58、src/dev_ng/game/scene/portals/LayoutManager.cpp:184、src/dev_ng/game/scene/portals/LayoutManager.cpp:269-293。它由脚本命令驱动(commands_interiors.cpp 中的 LoadLayout/GetInteriorFromHandle/...)src/dev_ng/game/script/commands_interiors.cpp:91-144。

9. 内景流式#

内景的资产流式由几条路径组成:

  1. 宿主 IMAP 与静态包围盒:代理通过 RequestContainingImapFile、RequestStaticBoundsFile/ReleaseStaticBoundsFile 控制宿主 IMAP 与内景静态碰撞(.bvh)src/dev_ng/game/scene/portals/InteriorProxy.cpp:1400-1420;内景 mover/weapon 包围盒在静态包围盒 box streamer 中被设为忽略,改由内景自行请求(见 map-data-and-lod.md 第 2 节)。
  2. 外壳请求:strRequestArray<MAX_SHELL_ENTITIES> 阻塞 PARTIAL→FULL 转换 src/dev_ng/game/scene/portals/InteriorInst.h:414。
  3. 房间 0 模型:静态 ms_roomZeroRequests(容量 INTLOC_MAX_ENTITIES_IN_ROOM_0=12)与 HoldRoomZeroModels,以及 RequestObjects/RequestRoom src/dev_ng/game/scene/portals/InteriorInst.h:416-417、src/dev_ng/game/scene/portals/InteriorInst.cpp:2545、src/dev_ng/game/scene/portals/InteriorInst.cpp:2626。
  4. 场景流式:内景实体本身在 PVS 中时按可见性请求;另外 CStreamingLists::AddFromRequiredInteriors/AddFromInteriorRoomZero 把玩家所在内景的实体加入需要列表,内景 HD 流式半径 20m(在内景外 12m)(见流式页)。
  5. LoadScene:先用 CInteriorInst::RequestInteriorAtLocation 探测目标位置是否在内景,若是则等代理达到 PS_FULL_WITH_COLLISIONS 再注册流式体积 src/dev_ng/game/scene/LoadScene.cpp:211-260。

约束与边界行为#

  • 一次只升/降一级:即使请求从 NONE 直接到 FULL_WITH_COLLISIONS,也要经过多帧(且受外壳资产加载与 15 帧冷却限制)src/dev_ng/game/scene/portals/InteriorProxy.cpp:1454-1458。
  • 代理索引稳定性:DLC 内景必须在顺序文件中有条目,否则断言;这是保证多机一致的内景索引的约束 src/dev_ng/game/scene/portals/InteriorProxy.cpp:265-270。
  • 房间 ID 来自碰撞材质:探测只信任静态包围盒(ENTITY_OWNEDBY_STATICBOUNDS);若静态包围盒有房间 ID 却找不到对应内景则断言并报错 src/dev_ng/game/scene/portals/PortalTracker.cpp:507-528。因此碰撞未加载时追踪器可能需要 m_bWaitForAllCollisionsBeforeProbe。
  • 位掩码与枚举的比较:m_statesRequestedByModules[m] 存的是 1<<state 位掩码 src/dev_ng/game/scene/portals/InteriorProxy.cpp:1220,而 UpdateImapPinState 用 >= PS_FULL(枚举值 2)判断脚本/过场是否要求固定 src/dev_ng/game/scene/portals/InteriorProxy.cpp:1280-1290;按字面计算,请求 PS_PARTIAL(掩码 2)也会满足条件。这是代码层面的观察,是否为有意行为无法从源码判断。
  • 大量 GTAV 专用 hack:隧道出口数、arcade 灯光、vbca 隧道反射、v_trailer 镜面、航母内景、heist island 等按模型哈希硬编码(见上文各处引用),修改资产时需留意。

未决问题 / 证据不足#

  • EntryExitPortalCheck、MoveToNewLocation、DepopulateInterior、CreateInterfacePortalInsts 的完整实现未逐行展开;“跨门户实体”(straddling portal)容器的维护只引用入口。
  • CPortal::CalcAmbientScale/FindModifiersForInteriors(内景对环境光与时间周期修改器的递归影响)属于渲染,未展开。
  • 组 1 的 InteriorGroupCheck 与短隧道检查 InteriorShortTunnelCheck 的具体条件未展开 src/dev_ng/game/scene/portals/Portal.cpp:293-425。