SDK 开发文档
按模块说明接入:框架、管控、大空间、内容、网络、交互。 API 见 API 文档,变更见 更新说明。
1. 概述
UPM 安装:com.slhx.sdk(必装)、com.slhx.samples(推荐)。
对照示例 Packages/com.slhx.samples/Content/Entry.unity。
| 模块 | 范围 |
|---|---|
| 框架 | 入口初始化、Boot、设备 SN、地图路径 |
| 管控 | 收发管控指令、内容状态上报(无特殊指令时内容一般不用处理) |
| 大空间 | 体验区加载/切换/预览;行进式须编辑器配置并提供 config.json |
| 内容 | 步骤、场景加载、进度;超时由内容自行处理 |
| 网络 | 房间连接、事件、自定义消息、物体同步、成员/位姿、Avatar |
| 交互 | XR 头手、手势 |
2. 环境要求
| 项 | 要求 |
|---|---|
| Unity | 2022.3.x LTS |
| 设备 | PICO 设备 |
| 渲染 | URP |
| PICO SDK | 工程自行安装 |
| OpenUPM | LiteNetLib、YooAsset(版本以 package.json 为准) |
3. 安装 SDK
- 工程使用 URP,并在 Graphics / Quality 中指定 URP Asset。
- 解压 SDK zip,将
com.slhx.sdk与com.slhx.samples放入Packages/。 - 配置 OpenUPM(LiteNetLib、YooAsset):
"scopedRegistries": [{
"name": "package.openupm.com",
"url": "https://package.openupm.com",
"scopes": ["com.revenantx.litenetlib", "com.tuyoogame.yooasset"]
}]
- 导入 PICO Unity Integration SDK,XR Plug-in Management 启用 PICO。
- Package Manager 对 XR Interaction Toolkit、XR Hands 导入 Samples(Starter Assets、Hands Interaction Demo、XR Device Simulator、Gestures、HandVisualizer)。
- 执行 Tools → Slhx → 安装/修复 Layers 与 Tags。
约定 Layer 26–30:Overlay / Underlay / Boundary / OutBoundary / Avatar。
4. 快速接入
打开 Packages/com.slhx.samples/Content/Entry.unity。
平台节点可沿用已拖好的 Inspector 引用;内容推进脚本是 Samples 示例,按自己的内容改。
4.1 Entry 组件
节点名是示例命名,以组件类型为准。
| 模块 | 场景节点(示例) | 组件 | 作用 |
|---|---|---|---|
| 框架 | FrameworkBootstrap | FrameworkEntry |
Awake 初始化框架。nextScene 可空 |
| BootSystem | BootSystem |
设备 SN、ContentMode、Editor 地图路径 | |
| 管控 | AndroidBridge | AndroidBridge |
接收管控指令 |
| Network / ContentSync | ContentStateSync |
内容状态 / 进度上报 | |
| 大空间 | ContentExperienceHandler | ExperienceAreaHandler |
体验区模板、加载、切换、预览 |
| 同上 | ExperienceAreaSafetyPresenter |
出区相机 mask | |
| ToBService | DetectingSystem、IPDAutoCalibrator |
出区检测、瞳距校准 | |
| 内容 | ContentSceneDirector | ContentSceneDirectorSample、RoomContentSceneFlow、ContentProgressReporter |
示例:切场景、切体验区、进度分段 |
| 网络 | Network / RoomMgr | RoomMgr + RoomNetClient |
进房;引用 Member / Transform / Event / Object Handler |
| Network / AvatarMgr | AvatarMgr |
房间 Avatar | |
| Network / UDPMgr | 大厅在线 / 位姿上报 | 大厅设备列表 | |
| 交互 | XR Interaction Hands Setup | XRI 官方 | 头手交互(须已 Import XR Samples) |
| InteractorMgr | HandGesturesMgr |
手势 |
各模块怎么用见后文对应章节。
5. 框架
FrameworkEntry 在入口 Awake 初始化。ServiceLocator 可取场景、定时器、事件、协程、资源、日志(见 API · 框架)。
BootSystem:
ContentMode:LargeSpace 实例化场地;Origin 不实例化Test Device Sn:Editor 设备号Editor Map Config Url:默认可落到 samples 的Map/config.jsonDeviceSn/Root:设备号与已解析的 boot 配置
if (!ServiceLocator.TryGet<ITimerService>(out var timer)) return;
var handle = timer.Wait(30f, () => { /* 超时 */ });
if (!ServiceLocator.TryGet<IEventBus>(out var bus)) return;
bus.Subscribe(100, _ => { });
bus.Publish(100);
6. 管控
Entry 已挂 ContentStateSync、CalibrationHandler。
内容不用自己接管控 JSON。
两种开播用 Inspector autoSync 区分;都不会自动切内容场景。
进度 / 结束两种情况都由内容上报:
contentStateSync.SetContentProgressAndReport(42, "内容1");
contentStateSync.NotifyGameFinished();
6.1 自动开播
勾选 autoSync(默认)。校准完成后 SDK 自动:
StartSync():握手、上报、连房间NotifyAgreementFinished():打开体验计时- 本地把
ContentStatus置为Running
这时的 Running 不是管控指令。内容不必等开播,校准完即可自己推进场景。体验时长从这时开始往管控报。
Offline / Editor 同样走这条;也可再调 NotifyExperienceStarted()。
6.2 管控开播
取消勾选 autoSync。校准和场地就绪后内容自己调 StartSync(),否则不会握手、不上报。
contentStateSync.StartSync();
StartSync() 之后就会握手、上报、连房间,还不是开播。
内容先停在自己的等待场景,等 ContentStatus == Running(管控下发 start)。
NotifyAgreementFinished():已 Running 且调过它,才开始计时。不调则时长为 0。
中途加入看 IsLateJoiner,不要再等开播。接入写法见 §8.1。
7. 大空间
行进式内容必须在编辑器中配置体验区,并提供配置好的 config.json。
未配置则无法正确加载场地与切区。
| 环境 | config.json 路径 |
|---|---|
| 设备(真机测试) | /sdcard/Download/lycj/boot/config.json |
| Editor | BootSystem 的 Editor Map Config Url;留空则用 samples 的 Map/config.json |
设备内测试:将编辑器导出的 config.json 拷到上表设备路径后再运行内容。
7.1 体验区
组件在 Entry 的 ContentExperienceHandler 上。
切区参数是 Boot placeItems[].id(如 area_01),不是 Unity 场景名。
模板 areaId 须与 placeItems.id 一致。
Inspector:
Areas:每项 areaId + 体验区 PrefabBase Object/Boundary Area:场地根与对齐边界Content Root:可选,内容根随当前区对齐
h.LoadBoundaryPlace();
h.SwitchExperienceArea("area_01");
h.ShowExperienceAreaPreview("area_02");
h.ClearExperienceAreaPreview();
h.SwitchToNextExperienceArea();
string cur = h.ActiveAreaId;
h.OnAreaChanged += e => { };
7.2 边界系统
出区线、障碍提示、相机 mask 由 SDK 自动处理。Entry 已挂 DetectingSystem、ExperienceAreaSafetyPresenter,无特殊需求时内容一般不用自己开关表现。
检测靠 Tag 和 Marker,不要靠节点名字:
| 类型 | 说明 |
|---|---|
Tag ContentArea + ExperienceAreaItem | 内容区碰撞体 |
ExperienceHexRoadMarker | Hex 通道;在通道内不算出来区 |
ExperienceObstacleMarker | 障碍 |
ExperienceNearAreaLineMarker | 近区线 |
ExperienceOutAreaLineMarker | 出区线 |
ExperienceObstacleAreaMarker | 障碍区视觉 |
ExperienceBoundaryVisualMarker | 子视觉;Kind:Boundary / Arrow / GuideLine |
内容要响应出区或障碍时,用 DetectingSystem;要自己改出区线 / 相机 mask 时,用当前区 ExperienceAreaItem 或 ExperienceAreaHandler。
DetectingSystem.Instance.EnableDetecting();
DetectingSystem.Instance.RegisterHeadDetecting(data => {
bool outArea = data.isOutContentArea == 1; // 1 出区,0 在区内
string areas = data.currentAreas;
});
bool inObstacle = DetectingSystem.Instance.IsInObstacle();
var h = GetComponent<ExperienceAreaHandler>();
var area = h.GetArea(h.ActiveAreaId);
area.SetOutAreaVisible(true);
h.SetCameraLayerMask(true);
h.LockCameraMask(true);
7.3 编辑器:体验区设置与预览
行进式内容用菜单 Tools → Slhx → 地图体验区配置工具 配置体验区,并产出 / 提供 config.json。
- 导入服务端
placeOperationJSON - 在 Scene 中预览体验区线框与位姿
- 导出配置好的
config.json,供 Boot 读取
导出后拷到设备 /sdcard/Download/lycj/boot/config.json 即可在头显内测场地与切区。
7.4 内容场景怎么配、位置如何对齐
切体验区时,SDK 会把内容根节点的位置、朝向(Y 轴)对齐到当前体验区。 因此内容场景必须有一个可被移动的根,且不要勾选 Static。
- 根节点:内容场景里放一个根物体,把场景内容都挂在它下面。给根物体打 Tag
ContentScene(安装 Layers/Tags 后可选到)。 - 取消静态:根节点及其子物体 Inspector 上取消 Static。勾选 Static 后运行时无法跟着体验区移动,会出现内容还在世界原点、和场地对不齐。
- 如何对齐:加载内容场景后找到 Tag 为
ContentScene的根(没有 Tag 时用场景里非灯光的第一个根),调用SetContentRoot,再SwitchExperienceArea。根节点的position与体验区一致,eulerAngles.y与体验区偏航一致。
// 加载内容场景后
handler.SetContentRoot(contentRootGo); // Tag=ContentScene 的根
handler.SwitchExperienceArea("area_01"); // 根对齐到该体验区位姿
Entry 示例导演会在加载场景后自动按 Tag 找根并对齐。内容自己写流程时,加载完场景也要 SetContentRoot 再切区。
8. 内容
内容自己决定步骤、是否加载场景、以及何时切体验区。 场景配置与对齐见 §7.4。 超时由内容自行处理(等待、卡关、单步过久等)。
Samples 推荐对照:等待开播 → 步骤表 → 导演执行 → 可选房间推进 → 进度上报。正式玩法按自己的表改。
8.1 等待开播
对应 §6.2 管控开播:取消 autoSync,关掉导演 Auto Cycle。
校准和场地就绪后 StartSync():这时已经在握手和上报,还没开播。
先停在内容自己的等待场景。用 IsLateJoiner 区分下面两种。
stateSync.StartSync();
director.GoTo(ContentSceneSearcher.佩戴区);
NotifyAgreementFinished():已 Running 且调过它,才开始计时。不调则时长为 0。
8.1.1 正常进房
IsLateJoiner == false。等管控开播后再推进。
yield return new WaitUntil(() =>
stateSync.ContentStatus == ContentStatus.Running);
stateSync.NotifyAgreementFinished();
director.GoTo(ContentSceneSearcher.内容1);
Offline / Editor 无管控开播:NotifyExperienceStarted() 本地进入 Running。
8.1.2 中途加入
IsLateJoiner == true:进房时房间已经 Running。不要再等开播。
IsLateJoiner 不带当前步骤;201 是当时全房确认才发,晚进不一定收得到。
需要和房间对齐时:进房后发自定义事件向房内要当前 Content 和时长(不要用 101 / 104 / 201 / 204)。
房内已有进度的端回一条;晚进的人按回复 GoTo 并对齐时长。
const ushort Req = 1101;
const ushort Ack = 1102;
var eb = RoomMgr.Instance.EventBroadcastHandler;
eb.RegisterJsonEvent<RoomSnap>(Ack, (senderId, snap) => {
director.GoTo((ContentSceneSearcher)snap.step);
// snap.time:房间已进行时长,内容自己对齐
});
eb.RegisterSimpleEvent(Req, senderId => {
if (stateSync.IsLateJoiner) return;
if (senderId == RoomNetClient.Instance.LocalPlayerId) return;
eb.TriggerEventJson(Ack, new RoomSnap {
step = (int)RoomContentSceneFlow.Instance.SceneStep,
time = myElapsed
});
});
if (stateSync.IsLateJoiner)
eb.TriggerEventSimple(Req);
stateSync.NotifyAgreementFinished();
RoomSnap.step / time 由内容自己定义。自定义事件见 §9.3。
8.2 步骤表
示例枚举 ContentSceneSearcher:[Description] 是 Unity 场景名(空 = 本步不加载场景),
[ExperienceAreaId] 是体验区 id(如 area_01),不要把场景名当 areaId。
string scene = ContentSceneStepMap.GetSceneName(ContentSceneSearcher.内容1); // Level_B
string area = ContentSceneStepMap.GetAreaId(ContentSceneSearcher.内容1); // area_02
8.3 导演
ContentSceneDirectorSample 本地执行:有场景则加载,再 SwitchExperienceArea;
无场景(如归还区)只切体验区、开透视。
director.GoTo(ContentSceneSearcher.内容1);
director.NextContent();
director.OnContentStepChanged += step => { };
8.4 房间推进
RoomContentSceneFlow 只负责请求与确认步骤 / 体验区,不加载场景。
导演订阅确认事件后再 GoTo。事件协议见 §9.3。
flow.OnSceneStepChanged += step => director.GoTo(step);
flow.ClientRequestNextContent(ContentSceneSearcher.内容1);
flow.ClientRequestNextExperienceArea("area_02");
8.5 进度
ContentProgressReporter 按段权重自动归一成 0~100,每升 1% 上报;到 100% 发结束。
归还区默认不计入。Segments 权重按自己内容改。
reporter.OnContentStepChanged(ContentSceneSearcher.内容1);
int pct = reporter.GlobalProgressPct;
9. 网络
房间连接、事件、自定义消息、物体同步、成员/位姿、媒体同步、Avatar 都在本模块。
Entry 中 RoomMgr 与 RoomNetClient 同物体,Handler 引用已拖好。
9.1 连接
RoomNetClient.Instance.Configure(host, port);
RoomNetClient.Instance.Connect();
// ConnectStatus == RoomNetStatus.Connected 后再发业务
RoomNetClient.Instance.Disconnect();
| API | 说明 |
|---|---|
Configure(ip, port, roomKey?) | 写入地址,不自动连接 |
Connect() / Disconnect() | 连接 / 断开 |
ConnectStatus / IsConnected | 状态 |
LocalPlayerId | 进房后由服务分配;未就绪为 -1 |
全房同步状态用事件广播;自定义字节流用频道;道具位姿用 NetworkedObject。
9.2 编辑器多人调试
ParrelSync 开两个 Editor,连接本机 房间服务(端口 11001)。
RoomNetClient.Instance.Configure("192.168.x.x", 11001);
RoomNetClient.Instance.Connect();
- 安装并启动房间服务。
- 主工程 Configure 后 Play。
- Clone 同样 Configure,并把
BootSystem的 Test Device Sn 改成与主工程不同后再 Play。 - 两端 Connected,成员为两个不同 DeviceSn。
每次测试前建议重启房间服务。
9.3 事件广播
通过 RoomMgr.EventBroadcastHandler,进房后使用。全房用同一个 eventId(ushort)。
自定义事件
自己选一个 eventId(不要用下面的 101 / 104 / 201 / 204)。
各端先 Register,再 Trigger。
服务器只转发,收到即发给全房,不做人数或 payload 校验。
var eb = RoomMgr.Instance.EventBroadcastHandler;
// 各端都要注册(含发送端,若本端也要执行)
eb.RegisterEvent(1001, (senderId, payload) => {
// senderId:触发方玩家 Id;payload 可为 null
});
eb.RegisterStringEvent(1002, (senderId, text) => { });
eb.RegisterJsonEvent<MyDto>(1003, (senderId, dto) => { });
eb.RegisterSimpleEvent(1004, senderId => { });
// 已进房后发送
eb.TriggerEvent(1001, myBytes);
eb.TriggerEventString(1002, "hello");
eb.TriggerEventJson(1003, new MyDto { step = 2 });
eb.TriggerEventSimple(1004);
eb.UnregisterEvent(1001, myListener);
回调里的 senderId 是触发方的房间玩家 Id,由服务器在转发时带上,内容不用自己传。
本端 Id:RoomNetClient.Instance.LocalPlayerId(进房后由服务器分配,未就绪为 -1)。
eb.RegisterEvent(1001, (senderId, payload) => {
int self = RoomNetClient.Instance.LocalPlayerId;
if (senderId == self) {
// 自己发的(本端若也 Register 了同样会收到)
} else {
// 其他玩家发的
}
});
- 适合离散玩法事件(过关、开关、同步阶段),不适合每帧高频。
- payload 编解码全房必须一致(字符串 / JSON / 字节)。
- 未连接时 Trigger 会丢弃。未 Register 的端收不到。
约定推进协议(101 / 104)
这两条由服务器做校验:客户端上报 101 / 104; 服务器收到同等人数、相同 payload 后,才向全房回 201 / 204。 内容端监听 201 / 204 再执行切步骤、切体验区。
| 事件 | Id | 说明 |
|---|---|---|
| CArrivedNextContent | 101 | 客户端请求切内容步骤 |
| SConfirmNextContent | 201 | 人齐且 payload 一致后,服务器确认全房执行 |
| CArrivedNextExperienceArea | 104 | 客户端请求切体验区 |
| SConfirmNextExperienceArea | 204 | 人齐且 payload 一致后,服务器确认全房切区 |
9.4 自定义消息
customMessageHandler.RegisterChannel(10, (senderId, data) => { });
customMessageHandler.SendReliable(10, bytes);
customMessageHandler.SendUnreliable(10, bytes);
未注册的频道不会处理。同一 channelId 重复注册会覆盖。
9.5 网络物体
挂 NetworkedObject。场景物体 id ≥ 1000(编辑器自动分配);运行时 Spawn ≥ 1_000_000。
仅 Owner 同步 Transform。Owner = -1 时本端移动不会同步。
netObj.RequestOwnership(force: false);
if (netObj.IsOwner()) netObj.SyncNow();
RoomMgr.Instance.RegisterPrefab("MyProp", prefab);
uint id = RoomMgr.Instance.SpawnObject("MyProp", pos, rot, scale, "{}");
RoomMgr.Instance.ObjectSyncHandler.OnObjectSpawned += (oid, go) => { };
9.6 成员与位姿
MemberHandler:成员列表(PlayerId、DeviceSn),并为其他成员创建房间 Avatar。
TransformHandler:头手位姿。
双开时两端 DeviceSn 必须不同。
RoomMgr.Instance.MemberHandler.RegisterHandler(list => {
foreach (var m in list) { /* m.PlayerId, m.DeviceSn */ }
});
RoomMgr.Instance.TransformHandler.RegisterHandler(msg => { });
9.7 内容播放同步
视频 / 音频播放状态对齐。实现 IContentController 后:
cs.RegisterContent("intro_video", myVideo);
cs.Play("intro_video", seekTime: 0f);
cs.Pause("intro_video");
cs.Stop("intro_video");
cs.Seek("intro_video", 12.5f);
9.8 Avatar
默认 Catalog:Resources/AvatarTemplate/DefaultAvatarCatalog。
AvatarMgr.Instance.SetCatalog(myCatalog);
var av = AvatarMgr.Instance.GetAvatarRoom(playerId);
av.SetNickName("P1");
av.Calibrate();
默认只同步头手。全身 IK 实现 IAvatarIkDriver 赋给 AvatarMono 的 vrIk。Samples AvatarVRIK 需自行准备 FinalIK。
大厅在线:LobbyDeviceOnlineMonitor.OnOnlineChanged。
10. 交互
头手与手势在 Entry 的 XR Interaction Hands Setup、InteractorMgr(组件 HandGesturesMgr)上。须已 Import XR Samples。
抓取、点击、手势由内容在自己的物体上接 XRI / HandGestureHold;平台只提供场景里的交互原点与手势管理。
HandGesturesMgr.Instance.RegisterLeftHandTrackingEvent(
() => { /* acquired */ },
() => { /* lost */ });
hold.Register();
hold.gesturePerformed.AddListener(() => { });
11. 配置路径
行进式场地配置说明见 §7。路径速查:
# 设备(真机测试必须放到此路径)
/sdcard/Download/lycj/boot/config.json
# Editor
# BootSystem.editorMapConfigUrl,留空则 samples:
Packages/com.slhx.samples/Map/config.json
12. 常见问题
等开播但一校准就进游戏?
勾选 autoSync 会在校准后本地进入 Running。等管控开播须取消 autoSync,关导演 Auto Cycle。见 §8.1。
事件或自定义消息无反应?
是否已 Connected;是否注册了同一 eventId / channelId;房间服务是否在运行。
物体不同步?
Owner 是否为本端(-1 需先申请所有权)。两端 objectId 须相同且已进房。
体验区 area not found?
是否已 LoadBoundaryPlace;placeItems.id 与模板 areaId 是否一致。设备上确认 /sdcard/Download/lycj/boot/config.json 已放好。
设备上场地不对 / 切不了区?
行进式必须用编辑器配体验区并提供 config.json,拷到设备上述路径。内容场景根节点 Tag ContentScene,并取消 Static。
Editor 双开成员 / Avatar 异常?
Clone 的 BootSystem.testDeviceSn 必须与主工程不同。
场景粉红或 Missing Script?
确认已指定 URP Asset,并已 Import XR Samples。
找不到 LiteNetLib / YooAsset?
检查 scopedRegistries;版本与 SDK package.json 对齐。
Entry 里物体怎么对应?
按 §4.1 六模块对照。内容推进脚本是 Samples 示例。