我们主要为孩子们打造这个游戏,同时也在学习使用Netcode for Entities。

Steam愿望清单:https://store.steampowered.com/app/2269500/Leap_of_Legends/ (我将soon使用Unity版本更新Steam的新视觉效果)

游戏玩法视频:https://www.youtube.com/watch?v=ncHXY-mI1yE

Leap of Legends — 技术介绍(Unity DOTS/ECS + Netcode for Entities)

堆栈&工具包

*- Unity 6与Entities 1.3.5,Netcode for Entities 1.10.0,Unity Transport 2.6.0,Unity Physics 1.3.5,Character Controller 1.4.2

*- URP 17.3.0与Entities Graphics 1.4.18用于hybrid ECS渲染

*- Burst 1.8.27 + Collections 2.6.5用于HPC#工作

*- Steamworks.NET(git依赖)用于桌面设备;苹果的GameKit用于iOS;谷歌的Google Play Games用于Android设备

*- Unity Relay 1.2.0 + Lobby 1.3.0 + Authentication 3.6.0用于移动设备的多人游戏服务

*-输入系统 1.18.0,具有运行时平台分支(桌面设备:物理键盘/鼠标,移动设备:增强的触摸虚拟控制器)

*- PrimeTween(本地tarball)用于过程化动画

*- Addressables 2.8.1通过传输依赖包拉取,但当前资源加载是使用Resources.Load<>() — 没有远程包

*-自定义轻量级JSON化本地化系统,覆盖了30个语言

单源多平台构建

*-一种代码库,四个平台目标:Windows(x64),macOS(通用),iOS(IL2CPP,min 16.0),Android(ARM64,min SDK 25,.aab输出)

-平台抽象层通过接口实现:IPlatformAuth,IPlatformMatchmaking,IPlatformRelay,IPlatformLeaderboard,IPlatformAchievements,IPlatformStats,IPlatformInventory,IPlatformCloudSave,IPlatformAvatar。游戏逻辑从来不直接调用Steam/GameCenter/GooglePlay — PlatformManager单例在启动时解析正确的后端,使用#if鏈,plus NullPlatform用于离线/编辑器的fallback

*-asmdef版本定义了驱动特征检测:UNITY_PIPELINE_URP,HAS_APPLE_GAMEKIT,HAS_GOOGLE_PLAY_GAMES,HAS_UNITY_RELAY,PRIME_TWEEN_INSTALLED。Steam受#if !DISABLESTEAMWORKS && (UNITY_STANDALONE_WIN || UNITY_STANDALONE_OSX || UNITY_EDITOR_WIN || UNITY_EDITOR_OSX)

*- StripSteamFromAndroid.cs — IPreprocessBuildWithReport回调,禁用libsteam_api.so原生插件以避免在Android构建中使用Steam二进制文件

*- MultiPlatformBuilder.cs — 编辑器菜单项用于一次性构建每个平台;设置脚本定义(STEAMWORKS_NET用于桌面,DISABLESTEAMWORKS用于移动)和输出路径。场景解锁为Menu.unity(0)+ Game.unity(1)

*- GitHub Actions - 无限制(.github/workflows/build.yml)- 触发在主分支推送或手动分发。四个并行作业:Windows(windows-latest),macOS(macos-latest),iOS(macos-latest),Android(ubuntu-latest)使用game-ci/unity-builder@v4与每平台的library缓存和基于密钥的Unity许可激活

*- AutoGameSetup.cs — [InitializeOnLoad]编辑器脚本验证场景在构建设置中注册,自动为Netcode生成playerGhostBase.prefab和验证steam管理器存在(仅此次禁用steamprefabs和其他netcode设置)

架构

*-双世界Netcode模型:包括单人游戏在内的所有会话都运行一个ServeWorld + ClientWorld对。服务器是权威的(物体定位、物理、AI、游戏状态)。客户端根据所有者预测的幽灵进行插值。单人游戏实际上是本地游戏:只有一个客户端。

*-幽灵复制:玩家实体是所有者预测的幽灵。复制组件:LocalTransform,PlatformerCharacterComponent(是否Grounded),PlayerHealth(是否死亡),KinematicCharacterBody(相对速度),PlayerInput(命令缓冲区)。通过快照不复制:PlayerCosmetics和PlayerPlatformId - 在加入或更改cosmetic时只通过RPC广播一次(每个幽灵/帧节省\~84字节)

*- PlayerInfoCache 桥接 RPC 元数据和幽灵物体的创造时间:缓存身份数据以便在 RPC 到达之前survive客户端

*-地图从不复制。WFC 求解器在服务器和客户端上运行使用相同的种子 — 确定输出。MapChecksumSystem 在客户端上计算 CRC32 生成的地图格子缓冲区,并通过 RPC 将其发送到服务器以验证。地图实体是本地 ECS 实体,而不是幽灵

*- ECS 系统通过 RequireForUpdate(GameActive)() gated — 世界存在于场景加载过程中,但仅在激活 GameActive 单例时才会触发。 CleanupPreviousGame()销毁陈旧实体(地图块、游戏状态)而不销毁世界

*-图形是混合的:RuntimeVisualInjectorSystem 根据 ECS 幽灵数据创造伴侣 GameObjec(动画 3D 模型或.SpriteRenderer)。双向模式:3D 模式实例化复杂的Prefab + 皮肤材质 + 手帽附加物;Sprite模式使用预先烘焙的图像集从 SpritesheetCache 中检索

*-只有Socket网络驱动程序:自定义SteamDriverConstructor和UdpOnlyDriverConstructor强制使用UDP Socket — 从来也不使用IPC。IPC在ServerWorld dispose(纯客户端模式)时会导致连接失败,不传递UDP包至127.0.0.1:7979。这是一个不明显的需求,使我在诊断过程中才能意识到

10个我们解决的不明显问题

1。没有在引擎中进行修改的情况下在本地计算机上使用Netcode for Entities

Netcode for Entities假设:一个客户端世界 === 一条网络连接 === 一个玩家。如果有2-4个玩家进行桌游,则客户端世界在runtime上创建(ClientServerBootstrap.CreateClientWorld( "LocalClient {i}")。每个世界都有自己的GameActive单元,连接到同一个本地服务器,并拥有自己的NetworkId。输入路由使用显式GhostOwner.NetworkId映射(不使用GhostOwnerIsLocal,这只有一个世界有用)。只有第一个客户端世界才创建可视GameObjec(isvisualowner=true),其他世界跳过可视激活以避免多个可视器-renderer。 MultiPlayerCameraFollow计算出所有人类玩家的边界盒(extent * 3.0f,限定在 [30, 80]),动态调整FOV(extent * 3.0f,限定在 [30, 80])并使用指数平滑(1 - exp(-))。

2。使用Steam P2P relay作为透明的UDP桥梁

Netcode for.Entities使用Unity Transport Protocol(UTP)通过UDP进行通信。Steam P2P使用SteamNetworkingSockets。这样做的解决方案不是编写自定义INetworkInterface,而是为Netcode添加SteamNetworkRelay:一个用于相互转发的双向UDP ↔ P2P桥梁。主机端:监听Steam P2P连接,通过一个本机UDP socket分配每个远程伙伴,向UTP服务器发送数据到127.0.0.1:7979。客户端:绑定一个本地UDP socket在7979上,Netcode在本地连接,中继服务以便host从steamp2p转发给k_nSteamNetworkingSend_NoNagles(不靠谱)由于UTP自行处理可靠性。桥梁是不可见的 — AutoConnectSystem只是将其连接到本地中

3. 确定性的WFC地图生成与CRC32跨世界验证

地图必须在服务器和客户端上保持一致,然而却从不复制。这意味着WFCSolver应该是确定性的,既可以在本地计算机上也可在外部计算机上进行生成,而不用考虑如何在这两个世界之间的通信过程中共享其结果。WFCSolver的确是确定性的,因为无论在哪里和何时运行它,只要种子相同,所输出的结果也相同。然而,因为地图生成的确定性,只要通过网络传输地图就会被复制两次,所以必须对每个客户端都执行地图生成过程。为了解决这个问题,我们将每个客户端都用作一个WFC solver,因此,每个客户端都会自己生成地图,并将其本地生成的CRC32 hash值通过RPC发送给服务器,并等待服务器告知这一过程是否成功。

4._ AI身份惯例为负数的网络ID

我们需要用来进行AI(人工智能)的客户端实体在同样的世界内作为玩家的客户端实体进行网络交互。那么问题是:我们该如何给予他们自己的ID?这是一个很简单的问题,用一个简单的逻辑。给AI客户端实体一个ID,根据他们的ID位组来区分出是不是AI(负数ID),而给AI客户端实体用负数来代表他们的ID。这样就很显然了,那么这个ID如果是负数,它就不是人工智能了。

5. 预烘焙Voronoi网格片段用于零成本的运行时创伤效应

为了在运行时减少mesh的复杂程度,我们使用Voronoi网格分解来生成 mesh 中的细节,这是在 mesh 的每个片段中包含了 mesh 的细节,这里我们所讲的细节,指的都是一些 mesh 片段的细节。这种网格分解方法是通过取 mesh 中最小的区域块作为网格分解的基本单位,比如 mesh 中最小的区域块是 0.1*0.1 这样的一个矩形块。我们只取最小的区域块来作为最基本的网格分解单位。这种网格分解方法的好处是减少了 mesh 在运行时的复杂度,使得 mesh 处理起来要快很多。

6. 预测振荡冲突消除用于破坏效果

Netcode for Entities 服务端预测功能可能会导致 PlayerHealth.IsDead 在传递给客户端预测时产生轻微的错误。这会导致在预测时触发 DestructionEffectSystem 触发无用的爆炸动画。解决方案是为每个实体记录一个 DestructionDedup 过滤器:当实体生成一组爆炸碎片后,便在未来一段时间内抑制其他 IsDead 变化,以便客户端不再触发无用的爆炸动画。只有在服务器确认了实体死亡后才会恢复可视化。

7. 在 RPC 广播中使用可视化代替幽灵序列化的外观属性

玩家外观(AnimalDefId、SkinDefId、HatDefId — 12字节)可能只更改一次。幽灵序列化意味着每帧 12字节×6个玩家 =\~ 4.3 KB/s 的废 bandwidth。因此,我们将玩家外观 RPC 广播一次,服务器将它们存储在幽灵实体的属性中,然后向所有客户端广播玩家信息 RPC。 PlayerInfoCache(客户端字典)将其缓存,根据网络 ID 寻找。 CosmeticVisualUpdateSystem跟踪上一个外观,并在皮肤或帽子更改时进行轻量级材质交换,仅在动物更改时(重新建构可视效果)。延迟结构更改(收集并应用)避免 EntityCommandBuffer 异常期间的迭代。

8. 为移动设备(3D → 2D管线)预渲染的图像精灵集

移动设备无法维持 6+ 个带有单独描绘的皮肤 mesh。 SpritesheetCache 以 SpriteAtlas 为单位预渲染每个独特的cosmetic组合(动物+皮肤+帽子): 4 行( Idle、Run、Jump、Swim)× N 列(帧)。使用静态相机 + 渲染纹理捕捉各个动画帧(configurable FPS)。每帧的协程(SpritesheetWorker)以 Unity 框架速度呈现一帧以避免卡顿。这次有 atlas key 是所有cosmetic ID 的哈希 — 相同载具的角色分享材质。移动设备分辨率是 192px compare 300px 的桌面分辨率。 runtime(SpriteCharacterRenderer)通过UV偏移采样精灵集(零皮肤成本)。

9. 19 个字符的便捷代码序列与启动调试面板的超时用于调试面板激活

运动_ cheat_ panel 是一个桌面 IMGUI 侧边栏,包含所有 13 个物理参数的滑动条(重力、跳跃力、空中控制等)- 激活调试面板可通过在 2 秒窗口内输入“movementdebug”。每个按键延长超时_; 错误或超时后重置。更改同步通过 RPC 到所有世界并增加一个版本计数,物理系统会轮询它。面板受构建配置控制,从不在发布中发送。这选择了标准调试菜单,而不是简单地通过各种UI占据的空间,因为它在任何现场都将对玩家不可见;它不会因为各种UI的显示问题而触发bug;它可以在任何场景中打开。

10. 过渡服饰与统一骨骼命名和缩放补偿的 Offset 配置

所有动物都来自 Quirky Series 资产包,具有自己的躯干骨骼,但我们强制实施一个“躯干”骨骼命名惯例。 HatOffsetConfig 设定每个帽子对于每种动物的偏移补偿(4级 fallback): [ ①]精确匹配( HatDefId + AnimalDefId)、[ ②]所有动物的帽子默认值(AnimalDefId=0)、[ ③] 蟾蜍的 fallback(参照骨骼)、[ ④]通用默认值。将所有动物骨骼缩放除以 Frog 的躯干骨骼失去缩放(1.0)以标准化帽子尺寸。新添加的动物中只需没有 hat 配置即可,因为 fallback 链段处理它。

我很乐意在任何部分中为您添加更多内容或为特定实现添加代码片段。