六个目标、四种图形 API、一个面向应用的模型。没有 OpenGL 或 WebGL 回退 路径:每个平台得到该平台的当前 API,配上一套自己的手写着色器。 所有真正不同的东西都被推到一个静态服务对象背后,所以你的 场景代码永远不会包含平台条件编译。
| 平台 | 目标框架 | 入口点 | 后端 |
|---|---|---|---|
| Windows | net10.0-windows10.0.19041.0 | WindowsApp.Run(app) | Direct3D 12 |
| Linux | net10.0 | LinuxApp.Run(app) | Vulkan |
| Android | net10.0-android | AndroidApp.Run(app) | Vulkan |
| iOS | net10.0-ios | iOSApp.Run(app) | Metal |
| Mac Catalyst | net10.0-maccatalyst | MacCatalystApp.Run(app) | Metal |
| Web | net10.0-browser | WebApp.Run(app, jsRuntime, httpClient, ...) | WebGPU |
Windows 目标是有条件加入的,仅当构建主机是 Windows 时才有,所以 Linux 或 macOS 机器可以不装 Windows SDK 就构建另外五个目标。Android、iOS 和 Mac Catalyst 用 MAUI 宿主层来做应用壳;Linux 和 Web 不用。
Vulkan 由 Linux 和 Android 共享。 它们住在同一个
Platforms/Shared/LinuxAndroid 后端里,iOS 和 Mac Catalyst
共享 Platforms/Shared/Apple。四种图形实现,
六个平台:共享发生在 API 层面,而这是唯一诚实共享的层面。
Direct3D 12 用 HLSL,Vulkan 用编译为 SPIR-V 的 GLSL,Apple 用 Metal Shading Language, WebGPU 用 WGSL。没有交叉编译器,没有着色器抽象层,没有运行时 翻译。代价是一次光照改动要改四处;收益是当一个 后端看起来不对时,你读到的正是实际运行的那份着色器,而且一个后端可以 使用其他后端没有的特性,而不必发明一个最小公分母。
DeviceServices 是一组静态接口,在你的应用运行之前
由平台入口点初始化一次。所有平台相关的东西都通过它到达,
这就是引擎源码在面向场景的代码里不含任何
#if ANDROID 的原因。
| 服务 | 接口 | 覆盖范围 |
|---|---|---|
Core | IDeviceCore | 平台与渠道、方向、资源加载、深色模式、权限。 |
Media | IMediaPlayer | 带独立音量的音乐与音效播放。 |
Video | IVideoPlayerService | 视频帧以 RGBA8 推入引擎。Windows 上硬件加速。 |
Dialog | IDialogService | 原生消息框和原生键盘。 |
File | IFileService | 选择器和平台文件访问。 |
Image | IImageService | 原生图像解码与编码。 |
Gallery | IGalleryService | 从系统相册读取和保存。 |
Record | IRecordService | 麦克风采集。 |
Recorder | IMediaRecorder | 录制应用自身的渲染输出。未实现处为 null。 |
Download | IDownloadService | 带进度的后台下载。 |
Store | IStoreService | 应用内购买与许可证检查。 |
Ads | IAds | 广告集成,自愿加入。 |
WindowsFeatures | IWindowsFeatures | 仅 Windows 的额外能力,隔离在一个接口背后,而不是散落在引擎各处。 |
平台没有实现的地方,服务就是 null 或一个文档化的空操作。它
不会静默返回一个看起来合理的值。调用方做空检查
——DeviceServices.Recorder 是你最先撞上的那个——
作为交换,你永远不会发布一个开发时看似能用、
在用户设备上却什么都不做的功能。
两个体现这种不对称的具体例子。 屏幕捕获干净地一分为二:
CaptureSelf 把引擎自己的后备缓冲读回为
RGBA8,处处可用,因为它走图形 API。
CaptureWindow 抓取整个屏幕,仅限桌面
——移动端和 Web 沙箱不允许,方法返回 null 而不是
黑图。资源加载也一分为二:每个平台都有同步的
LoadFile,但 Web 用 HTTP 下载覆盖异步路径,
因为 Blazor WebAssembly 没有同步 I/O,那里的任务绝不能被同步等待。
你的应用是一个带面板的 BaseApp 子类,
在所有六个目标上都是同一个类。每个平台头部都是一个小项目,
它的全部工作就是创建窗口或画布、构建平台服务实现,
并把你的应用交给循环。
// Windows head
WindowsApp.Run(new MyApp());
// Web head, inside a Blazor component
await WebApp.Run(new MyApp(), JS, Http, canvasId: "season-canvas");
Web 头部接收更多参数,因为浏览器给它的更少:一个用于互操作的 JavaScript 运行时、
一个 HttpClient(因为资产走网络到达),
以及要渲染进去的画布 id。那一行之上的一切完全一致。
WebGPU 是真的,不是模拟的。 GPU 对象住在 JavaScript 里——
seasonWebGPU.js——C# 层通过
[JSImport] 转发。着色器是 WGSL,由浏览器编译,
JS 侧不携带任何着色器源码。Compute 特效在浏览器里运行;全局
光照、TAA 和大气模型与桌面上是同一条代码路径。
StorageService 从一个
DirectoryBase 名称解析出每个平台的设置与文件位置,
所以应用只需声明一次数据住在哪里,就在每个目标上得到正确答案。
Database 是本地持久化层,
Localization 支撑
Texts.Translate 标志——这就是为什么文本在布局
之前而非之后翻译,一段更长的德语字符串会正确重排,
而不是溢出它的盒子。