Assets live in Resources/Raw/ and are addressed by their path
below it. A file at Resources/Raw/Assets/island.glb is loaded
as "Assets/island.glb" on all six platforms, with no
per-platform path handling in your code.
Hello/
Hello.csproj
GlobalUsings.cs
App.cs
Panels/
Scene.cs
Platforms/
Windows/ App.xaml, App.xaml.cs, Package.appxmanifest
Linux/ Linux.cs
Android/ MainActivity.cs, AndroidManifest.xml
iOS/ Program.cs, Info.plist
MacCatalyst/ Program.cs, Info.plist
Resources/
AppIcon/ appicon.svg
Splash/ splash.svg
Raw/
Assets/ island.glb, robot.glb, NotoSansMono-VariableFont.ttf, ...
Everything outside Platforms/ is shared. The web build is a
separate Blazor WebAssembly project that references the same
App class, because a browser host has a different project SDK
rather than a different source file — see
requirements for the three-project shape.
Mobile and Windows read assets through the MAUI asset system; Windows and Linux desktop
builds also want them next to the executable. Both groups declare the same
LogicalName/Link pattern, which
is what keeps the lookup path identical everywhere.
<ItemGroup>
<MauiAsset Include="Resources\Raw\**"
LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<ItemGroup Condition="'$(IsWindows)' == 'true' or '$(IsLinux)' == 'true'">
<Content Include="Resources\Raw\**"
CopyToOutputDirectory="PreserveNewest"
Link="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
Check these two groups first. A missing
MauiAsset entry produces exactly that symptom: the desktop
build finds the file on disk through the Content copy while
the packaged build has nothing to open. Loading is asynchronous and a missing asset leaves
the control not Ready rather than throwing, so the failure
looks like an invisible object rather than an exception.
The Linux target is plain net10.0, so its entry point must not
be compiled into the MAUI target frameworks. The sample projects do that with two small
targets rather than with #if blocks:
<Target Name="IncludeLinuxFilesForLinuxTarget" BeforeTargets="BeforeCompile"
Condition="'$(IsLinux)' == 'true'">
<ItemGroup>
<Compile Include="Platforms\Linux\**\*.cs" />
</ItemGroup>
</Target>
<Target Name="ExcludeLinuxFilesForNonLinuxTargets" AfterTargets="BeforeCompile"
Condition="'$(IsLinux)' != 'true'">
<ItemGroup>
<Compile Remove="Platforms\Linux\**\*.cs" />
</ItemGroup>
</Target>
The IsWindows, IsLinux,
IsAndroid and friends used in these conditions are ordinary
MSBuild properties derived from TargetFramework at the top of
the project file. Copying them along with the targets is the least error-prone way to start.
Applications normally declare their namespaces once, which is why sample code carries almost
no using lines and why the snippets on this site do not repeat
them:
global using System.Numerics;
global using Season.Basic;
global using Season.Utils;
global using Season.Storage;
global using Season.Models;
global using Season.Controls;
global using Season.Panels;
global using Season.Rendering;
| Namespace | What lives there |
|---|---|
Season.Basic | BaseApp, DeviceServices, Graphics, Colors, Platform. |
Season.Controls | Model, Mesh3D, InstancedModel, Sprite2D, Sprite3D, Shape, Texts, and the MeshInstanceTransform you derive instance types from. |
Season.Panels | Panel and the ready-made panels: FrameButton, Input, Picker, ObjectPicker. |
Season.Rendering | RenderQuality, FrameSchedule, Atmosphere, WorldSettings, SceneLighting. |
Season.Models | glTF loading and animation: GltfAsset, GLTFAnimationPlayer, ModelAnimationInfo, PickMesh. |
Season.Storage | StorageService and the settings persistence used by the quality screen. |
Season.Fonts | Font. Not usually a global using, since you touch it once at startup. |