Skip to main content

multiplatform-conversion

Converting a single-target FlatRedBall2 desktop sample into a dual-target desktop + KNI BlazorGL (Blazor WebAssembly / browser) project. Use when the user mentions web deployment, browser/WASM/itch.io targets, KNI, or asks to add web support to an existing game. Assumes you already have a working desktop sample — see sample-project-setup for the desktop bootstrap.

الانتقال إلى التثبيت

معلومات المصدر

المستودع
vchelaru/FlatRedBall2
آخر نشاط في المصدر
٢٣ أغسطس ٢٠٢٦ في ١٠:٥٥
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
١٤
التفرعات
٦

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
multiplatform-conversion
description
Converting a single-target FlatRedBall2 desktop sample into a dual-target desktop + KNI BlazorGL (Blazor WebAssembly / browser) project. Use when the user mentions web deployment, browser/WASM/itch.io targets, KNI, or asks to add web support to an existing game. Assumes you already have a working desktop sample — see sample-project-setup for the desktop bootstrap.
# Multi-Platform Conversion (Desktop + KNI BlazorGL) > Reference samples: `samples/auto/AutoEvalKniBlazorSample/` (minimal — one XNB, no real content) and `samples/PlatformKing/` (content-rich — TMX, JSON, PNG animations). Read PlatformKing first when porting any non-trivial game; AutoEval only proves the wiring, not the content story. ## Backend selection is by which project file you reference `src/FlatRedBall2.csproj` (MonoGame/desktop) and `src/Kni/FlatRedBall2.Kni.csproj` (KNI/browser) are single-TFM (`net10.0`) project files, not one multi-targeted project — see the comment atop either file for why. Consumers pick a backend by which file they `ProjectReference`/which NuGet package (`FlatRedBall2.MonoGame` vs `FlatRedBall2.Kni`) they install, not by TFM. This is the linchpin — every gotcha below comes from wiring the wrong file/package to the wrong head. The KNI file lives in its own `Kni\` subfolder, not next to the MonoGame one: two SDK projects globbing the same physical directory hit a real MSBuild parallel-build race (confirmed — `-m:1` always succeeds, default parallel builds fail intermittently depending on the solution's project-graph shape). Physical separation removes the race at its root; see the comment atop `FlatRedBall2.Kni.csproj` for the full story. ## Project layout Three projects, mirroring `AutoEvalKniBlazorSample`: ``` GameName/ GameName.Common/ net10.0 game code + Content/ — MonoGame build (see below) GameName.Common/Kni/ net10.0 KNI build, same source, own subfolder GameName.Desktop/ net10.0 Program.cs, MonoGame GameName.BlazorGL/ net10.0 Blazor WASM host GameName.slnx ``` The `Game` subclass (Game1) lives in `Common` so both heads instantiate the same type. Heads own only their entry points and platform-specific csproj wiring. ## Asset placement Assets belong in `Common/Content/` by default. Only move to platform-specific folders if they won't work elsewhere (e.g., platform-specific UI sizes, backend-incompatible shader variants). Most art, audio, and data stay in `Common/` so both Desktop and BlazorGL draw from a single source — no duplication. ## Common — two single-TFM csproj files, KNI in its own subfolder Desktop and BlazorGL both now target `net10.0`, so a single Common project can no longer pick the backend by `Condition="'$(TargetFramework)' == ..."` — both heads would match the same condition. Splitting into two files that share a directory isn't safe either (the MSBuild parallel-build race above). Instead: - `GameName.Common.csproj` (MonoGame) stays where it is, default compile items, references `FlatRedBall2.csproj`/`FlatRedBall2.MonoGame`. Add `<DefaultItemExcludes>$(DefaultItemExcludes);Kni\**</DefaultItemExcludes>` so its own glob never reaches into the subfolder below. - `GameName.Common\Kni\GameName.Common.Kni.csproj` (KNI) sets `<EnableDefaultCompileItems>false</EnableDefaultCompileItems>` and explicitly compiles the parent's source: `<Compile Include="..\**\*.cs" Exclude="..\obj\**;..\bin\**;..\Kni\**" />`. References `..\..\..\src\Kni\FlatRedBall2.Kni.csproj`/`FlatRedBall2.Kni`. - `GameName.Desktop.csproj` references the plain file; `GameName.BlazorGL.csproj` references the one in `Kni\`. See `samples/Solitaire/Solitaire.Common/` for a working example, including how its Gum content-copy `<Content Include>` items pick up `Link` metadata once the source path (`..\Content\...`) no longer matches the project's own directory. No `Version` attribute — the repo uses NuGet Central Package Management, so versions are pinned once in `Directory.Packages.props`, not per `PackageReference`. Common must NOT add `MonoGame.Content.Builder.Task` — that belongs on the heads that drive content compilation. `Apos.Shapes` needs no content-pipeline wiring at all — its shader is embedded in the assembly. ## Backend-conditional code Each file sets its own `KNI` / `MONOGAME` define unconditionally (no `Condition` needed — the file itself is the condition): ```xml <DefineConstants>$(DefineConstants);KNI</DefineConstants> ``` The two known places `#if KNI` is needed today: - **`GraphicsProfile`** — Apos.Shapes ships SM 4.0+ shaders. MonoGame's top profile is `HiDef`. KNI's equivalent is `FL10_0`, which doesn't exist on MonoGame. The `Reach` default (SM 2.0) crashes at runtime with "Shader model 4.0 is not supported." - **Anywhere a backend exposes a type the other doesn't.** Stay vigilant; most XNA surface is shared. ## Two canvas patterns — pick one before writing Game1 Stretch-to-viewport (canvas fills the browser) and fixed-size canvas (matches the desktop window) need opposite engine settings. Each pattern is a coordinated set across Game1, holder CSS, body CSS, and the JS host script — mixing them produces the squashing / shifting bugs the engine gates were added to prevent. > **Pattern A is the recommended default.** With `DisplaySettings.AspectPolicy = AspectPolicy.Locked` (the engine default), the canvas can fill the browser viewport and the engine pillarboxes/letterboxes the gameplay area to the design ratio internally — no playfield reshaping. Pattern B is only needed for legacy fixed-canvas embeds. `DisplaySettings.AllowUserResizing` is the source-of-truth signal — it propagates to `Game.Window.AllowUserResizing` at init and gates two engine behaviors: the `externallyManaged` check that skips `ApplyWindowSettings`, and `HandleClientSizeChanged` short-circuiting browser resize echoes. ### Pattern A — Stretch-to-viewport (recommended) Canvas fills the browser; the engine pillarbox/letterboxes the design world to its locked aspect ratio inside the canvas. Reference: `samples/ShmupSpace`, `samples/PlatformKing`. - **Game1**: identical code on Desktop and KNI — set `ResolutionWidth/Height`, `PreferredWindowWidth/Height`, `AllowUserResizing = true`, all on `DisplaySettings`. The engine ignores `PreferredWindowWidth/Height` on KNI (the canvas DOM owns sizing) so no `#if KNI` is needed. - **Holder + canvas markup**: ships from the `FlatRedBall2.BlazorGL` package's `Pages/Index.razor` — fills viewport via `position: fixed; top: 0; left: 0; right: 0; bottom: 0`. No per-sample Razor needed. - **Body CSS** (`wwwroot/index.html`): `margin: 0; overflow: hidden;` — no flex centering needed. - **JS**: ships from the package as `_content/FlatRedBall2.BlazorGL/frb-host.js` (referenced via one `<script>` tag in `index.html`). Defines `initRenderJS` and `tickJS`; sets canvas buffer once from holder size. ### Pattern B — Fixed-size canvas (legacy) Canvas locked at exact pixel dimensions; nothing scales. Use only for embeds with a strict pixel budget; otherwise prefer Pattern A + locked aspect. - **Game1**: set `PreferredWindowWidth/Height` on **both** backends, plus `ds.AllowUserResizing = false`. Don't set `Window.AllowUserResizing = true`. Per-screen `PreferredDisplaySettings` left unset (same as Pattern A). - **Index.razor override**: ship a per-sample `Pages/Index.razor` that overrides the package's route (`@page "/"`) with explicit-dimension CSS: - **Holder CSS**: `width: NNNpx; height: MMMpx; flex-shrink: 0;` — explicit dims, won't shrink in flex centering. - **Canvas CSS**: `width: NNNpx; height: MMMpx; display: block;` — explicit dims, not `100%` (defense in depth if holder is overridden). - **Body CSS**: `display: flex; align-items: center; justify-content: center; min-height: 100vh; overflow: auto; background: #222;` — centers the canvas; scrolls when viewport is smaller than the canvas. - **JS hooks**: still load `frb-host.js` from the package; add an inline override script that uses the `frbBeforeTick` hook to re-pin canvas dimensions each frame. KNI BlazorGL auto-resizes the drawing buffer when the browser resizes; the per-frame lock undoes that: ```html <script src="_content/FlatRedBall2.BlazorGL/frb-host.js"></script> <script> var lockW = 0, lockH = 0; window.frbAfterInit = function (canvas, holder) { lockW = holder.clientWidth; lockH = holder.clientHeight; }; window.frbBeforeTick = function () { var c = document.getElementById('theCanvas'); if (c) { if (c.width !== lockW) c.width = lockW; if (c.height !== lockH) c.height = lockH; } }; </script> ``` All parts must be present together — omit any one and the buffer or viewport drifts on browser resize. ## Game code must avoid `System.IO.File` for content Browsers have no filesystem. Any `File.ReadAllText` / `File.OpenRead` / `File.Exists` / `Path.GetFullPath` call against a content path crashes on WASM with `Could not find a part of the path '/Content/...'` (the leading `/` is `Path.GetFullPath` resolving against the WASM working directory `/`). Route every content read through `Microsoft.Xna.Framework.TitleContainer.OpenStream(path)` instead — it dispatches to File IO on Desktop and HTTP fetch in the browser, single code path: ```csharp using var stream = TitleContainer.OpenStream(path); using var reader = new StreamReader(stream); var json = reader.ReadToEnd(); ``` This applies to game-specific config loaders (the engine's own `PlatformerConfig.FromJson`, `TopDownConfig.FromJson`, `TileMap`, animation/atlas loaders, and `ContentLoader` already do the right thing internally). Save data and user settings legitimately need `File` and stay desktop-only — gate them with `#if !KNI`. ## Content pipeline — single source of truth in Common Keep `Content/` in `Common`. Both heads consume it without duplication. **Desktop** uses MGCB. Link Common's raw runtime-loaded assets (TMX, JSON, animation PNGs — anything not built to XNB) into the Desktop output's `Content/` folder: ```xml <Content Include="..\GameName.Common\Content\Tiled\**" Link="Content\Tiled\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" /> ``` `MonoGameContentReference` is project-local; Desktop needs its own minimal `Content/Content.mgcb` (see `sample-project-setup`). **BlazorGL** is the trap. `<Content Link="wwwroot\…">` copies to `bin/.../wwwroot/` but the file is **not** registered as a static web asset, so the dev server returns 404. Files must land in the project's **physical** `wwwroot/Content/` directory before the static-web-asset manifest is gathered. Use a `<Copy>` target: ```xml <Target Name="CopyCommonRawAssetsToWwwroot" BeforeTargets="GenerateStaticWebAssetsManifest;AssignTargetPaths" Inputs="@(_CommonRawAssets)" Outputs="@(_CommonRawAssets -> '$(MSBuildProjectDirectory)\wwwroot\Content\%(RecursiveDir)%(Filename)%(Extension)')"> <Copy SourceFiles="@(_CommonRawAssets)" DestinationFiles="@(_CommonRawAssets -> '$(MSBuildProjectDirectory)\wwwroot\Content\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="true" /> </Target> ``` Same pattern as the `RedirectKniContentToWwwroot` target, which writes XNBs to the physical wwwroot. Gitignore the destination tree (`wwwroot/Content/.gitignore` excluding `*` except itself) so the copies aren't committed. ## BlazorGL head — minimum setup Reference: `AutoEvalKniBlazorSample.BlazorGL`. Each sample's `.BlazorGL` head owns only: - **`.csproj`** — SDK = `Microsoft.NET.Sdk.BlazorWebAssembly`, `<KniPlatform>BlazorGL</KniPlatform>`, the nkast.Xna / nkast.Kni.Platform.Blazor.GL package list, the `RedirectKniContentToWwwroot` target. **`<ProjectReference>` to `src/FlatRedBall2.BlazorGL/FlatRedBall2.BlazorGL.csproj`** (the host package, not the engine itself). - **`Program.cs`** — standard Blazor WASM bootstrap. Two FRB-specific lines: ```csharp builder.RootComponents.Add<FlatRedBall2.BlazorGL.App>("#app"); builder.Services.AddSingleton<Func<Game>>(_ => () => new MyNamespace.Game1()); ``` - **`wwwroot/index.html`** — the standard Blazor scaffold + script tags: ```html <script src="_framework/blazor.webassembly.js"></script> <script src="_content/FlatRedBall2.BlazorGL/frb-host.js"></script> ``` The package's `frb-host.js` defines `tickJS`, `initRenderJS`, optional hooks (`window.frbBeforeTick`, `window.frbAfterInit`), content prefetching via `content-manifest.json`, and keyboard/mouse scroll prevention for itch.io iframes. - **`Properties/launchSettings.json`** — pick a unique launch port. AutoEvalKniBlazorSample uses 50470/50471; pick something else. Concurrent debugging across samples breaks if ports collide. **Do not duplicate** `App.razor`, `MainLayout.razor`, `_Imports.razor`, `Pages/Index.razor`, or the `tickJS`/`initRenderJS` JS block. They ship from `FlatRedBall2.BlazorGL` and are wired by the `RootComponents.Add<App>` and `frb-host.js` reference above. The package's Index resolves `Func<Game>` from DI on the first tick — that's why `Program.cs` must register it. ## Content prefetching (automatic) The `FlatRedBall2.BlazorGL` NuGet package includes a `.targets` file that auto-imports into consuming projects. On every build it enumerates all files under `wwwroot/Content/` and writes `wwwroot/content-manifest.json` — a JSON array of paths relative to `wwwroot`, each prefixed `Content/`. At runtime, `frb-host.js` fetches `content-manifest.json` and fires background `fetch()` for every listed file during `initRenderJS`. Both `fetch()` and the synchronous `XMLHttpRequest` used by `TitleContainer.OpenStream` share the browser HTTP cache, so by the time game content loads, the files are served from local cache instead of the network. Errors are silently swallowed (fire-and-forget). **No game author action required.** The manifest is generated automatically from whatever files exist in `wwwroot/Content/` at build time. Add or remove content files and the manifest stays in sync on the next build. **Landmine:** `build/*.targets` auto-import only happens for NuGet `PackageReference` consumers — a `ProjectReference` to the host project does **not** import them. That's why repo samples carry an explicit `<Import Project="...\FlatRedBall2.BlazorGL\build\FlatRedBall2.BlazorGL.targets" />`; without it, no manifest is ever generated. NuGet consumers need nothing. ## Verification 1. `dotnet build GameName.Desktop/` clean. 2. `dotnet build GameName.BlazorGL/` clean. 3. `dotnet run --project GameName.Desktop/` plays the original game unchanged. 4. `dotnet run --project GameName.BlazorGL/` serves the dev URL; canvas fills viewport; resizing the window keeps rendering correct (proves `AllowUserResizing` is set). ## Known limitations (as of 2026-08-20) - **No gamepad polling guarantee on web.** Browser gamepad APIs require a connected-device gesture before reporting state. - **Audio gated by user gesture.** Browsers block audio playback until the user interacts with the page once. Have a "click to start" affordance if music plays on screen entry. - **`DynamicSoundEffectInstance` sample rate must match the browser's `AudioContext` rate on Blazor.GL, or `SubmitBuffer` throws** (`Sample rate 44100 does not match AudioContext sample rate 48000`). Desktop OpenAL resamples any source rate for free; Blazor.GL does not, and Kni exposes no public way to read the actual `AudioContext` rate (feature-requested: kniEngine/kni#2690). Until that lands, fall back to a couple of common candidate rates (48000, then 44100) and retry on failure. - **`Pitch` on `SoundEffectInstance`/`DynamicSoundEffectInstance` throws on Blazor.GL** with the currently-published Kni NuGet packages. Fixed upstream (kniEngine/kni#2614, #2615) but not yet released — confirmed via `diagnostics/MusicPitchWebSpike`. Re-check once Kni cuts a release containing both.
عرض على GitHub