Go to article Go to contents Go to chapter navigation

sOU uses cookies for sign-in and form protection. There are no analytics or advertising cookies and no third-party tracking.

You can continue immediately. Only if you choose “Hide notice for 180 days” do we also store this preference in your browser.

Practical examples for HUD authors

Complete SDK v3 examples for selective player displays, local clock updates, panel actions and safe cleanup.

Updated: 28 October 2026 · approx. 6 minute read

This complete Blazor example adapts the SDK v3 patterns from the HUD sources listed below into a small project: one shared session, separate display subscriptions, a local clock display and one-time panel actions. Use SDK 0.3.0-preview.2 and sOU Desktop 0.3.34 or later. Displayed values come from the actual session; missing values remain unknown.

Project and shared session

Create a Blazor project as described in the SDK introduction and use the complete files below. The required NuGet packages are also included in the SDK bundle. Place hud.json in the project directory and panel.json and index.html in wwwroot. _Imports.razor applies to the four Razor components in the same project. Retain the GPL license and source notices below when distributing the adapted examples.

Set up and package the SDK · Method reference

Hud.csproj

    <Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  <PropertyGroup><TargetFramework>net10.0</TargetFramework><RootNamespace>PracticeHud</RootNamespace><Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings></PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.14" />
    <PackageReference Include="SOU.Hud.Sdk.Blazor" Version="[0.3.0-preview.2]" />
    <None Update="hud.json" CopyToPublishDirectory="PreserveNewest" />
  </ItemGroup>
</Project>
  

PracticeOptions.cs

    // SPDX-License-Identifier: GPL-3.0-only
using System.Text.Json.Serialization;
namespace PracticeHud;
public sealed record PracticeOptions(string Caption = "Broadcast");
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(PracticeOptions))]
public partial class PracticeJson : JsonSerializerContext;
  

Program.cs

    // SPDX-License-Identifier: GPL-3.0-only
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using PracticeHud;
using SOU.Hud.Sdk.Blazor;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(_ => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddSouHud<PracticeOptions>(PracticeJson.Default.PracticeOptions);
await builder.Build().RunAsync();
  

_Imports.razor

    @namespace PracticeHud
@using Microsoft.AspNetCore.Components
@using SOU.Hud.Sdk
@using SOU.Hud.Sdk.Blazor
@using SOU.Hud.Sdk.Presentation
@using SOU.Live.Contracts
@using PracticeHud
@using System.Globalization
  

App.razor

    @* SPDX-License-Identifier: GPL-3.0-only *@
<SouHudHost TOptions="PracticeOptions">
    <main>
        <PlayerHealth />
        <RoundClock />
        <PanelBanner />
    </main>
</SouHudHost>
  

SouHudHost owns the shared SDK runtime. Child components use the same HudSession and open no additional live connections. The host waits for the synchronized clock and removes the display when clock readiness is invalid. A child releases only its own subscriptions; it does not stop the shared session.

Selective player display

PlayerHealth.razor applies the small-render-region pattern used by both HUDs. Its selector uses the explicitly observed player and returns only the visible text. Changes to other players, panel values or map coordinates do not update the content when the selection stays equal. A delivered HP value of 0 remains 0; missing or stale values produce —.

    @* SPDX-License-Identifier: GPL-3.0-only *@
<SouHudRegion TOptions="PracticeOptions" TValue="string" Select="ReadHealth" Context="health">
    <p>HP: @health</p>
</SouHudRegion>
@code {
    static string ReadHealth(HudSessionSnapshot<PracticeOptions> state)
    {
        if (!state.Ready || state.Game is not { Freshness: DataFreshness.Live } game) return "—";
        return game.ObservedPlayer?.Vitals.Health?.ToString(CultureInfo.InvariantCulture) ?? "—";
    }
}
  

Local clock updates without a global render timer

RoundClock.razor adapts the explicitly chosen presentation bridge and render boundary from NYXIA and the local timed boundary used by the WASM HUD. WithoutMotion fits this display because it needs no radar, grenade or position data. It is not a general default for other displays. The 16 ms coalescing interval applies to render requests; the timed region updates separately every 100 ms.

    @* SPDX-License-Identifier: GPL-3.0-only *@
<SouHudSnapshotView TOptions="PracticeOptions" Projection="HudProjectionOptions.WithoutMotion" CoalesceMilliseconds="16" Context="frame">
    <HudRenderBoundary RenderKey="@((frame.Snapshot?.SessionId, frame.Snapshot?.Freshness, frame.Snapshot?.ReceivedAt, frame.DisplayGame?.Countdown))"
                       Now="frame.Now" ClockMilliseconds="100" Context="now">
        <p>Round: @Label(frame.Snapshot, now)</p>
    </HudRenderBoundary>
</SouHudSnapshotView>
@code {
    static string Label(LiveSnapshot? snapshot, DateTimeOffset now)
    {
        if (snapshot is not { Freshness: DataFreshness.Live, Game: { Countdown: { } countdown } }) return "—";
        var remaining = HudCountdown.Remaining(countdown.SecondsRemaining, snapshot.ReceivedAt, now,
            paused: countdown.Phase == CountdownPhase.Paused);
        return remaining is { } value ? Math.Ceiling(value).ToString("0", CultureInfo.InvariantCulture) : "—";
    }
}
  

Remaining time is recomputed from the delivered seconds, its GSI receipt timestamp and synchronized server time. A pause holds the source value, and the result is clamped at 0. Clock ticks do not make an old GSI sample fresh. Without a countdown, receipt timestamp or valid live state, the component displays —.

Panel actions, reset and cleanup

PanelBanner.razor uses the action pattern from the current WASM HUD: a show or hide command changes only the local banner state. Lost readiness or a new game session resets it. The caption has its own selective region and may intentionally be empty. Rendering again or reconnecting does not replay an old action.

    @* SPDX-License-Identifier: GPL-3.0-only *@
@implements IDisposable
@inject HudSession<PracticeOptions> Session
@if (visible)
{
    <SouHudRegion TOptions="PracticeOptions" TValue="string" Select='@(state => state.Panel?.Text("production", "caption") ?? state.Options?.Caption ?? "")' Context="caption">
        <p role="status">@caption</p>
    </SouHudRegion>
}
@code {
    IDisposable? reset;
    bool visible, disposed;
    protected override void OnInitialized()
    {
        reset = Session.Observe(state => (state.Ready, state.Game?.SessionId), identity => {
            if (!disposed) _ = InvokeAsync(() => { if (!disposed) { visible = false; StateHasChanged(); } });
        });
        Session.PanelAction += Action;
    }
    void Action(HudPanelAction action)
    {
        if (action is not { Section: "production", Name: "lowerThird", Value: "show" or "hide" } || disposed) return;
        _ = InvokeAsync(() => { if (!disposed) { visible = action.Value == "show"; StateHasChanged(); } });
    }
    public void Dispose()
    {
        disposed = true;
        reset?.Dispose();
        Session.PanelAction -= Action;
    }
}
  

Dispose releases the component’s reset subscription and event handler. The disposed guard discards already queued UI callbacks after component removal. For custom delayed animations, also check the session or generation and stop their timers.

wwwroot/panel.json

    [
  {"name":"production","label":"Production","inputs":[
    {"type":"text","name":"caption","label":"Caption"},
    {"type":"action","name":"lowerThird","label":"Banner","values":[
      {"name":"show","label":"Show"},{"name":"hide","label":"Hide"}
    ]}
  ]}
]
  

hud.json

    {"id":"docs-practice-hud","version":"0.1.0","name":"Documentation Practice HUD","apiVersion":3,"runtime":"blazor-wasm","entryPoint":"index.html"}
  

wwwroot/index.html

    <!DOCTYPE html>
<!-- SPDX-License-Identifier: GPL-3.0-only -->
<html lang="en"><head><meta charset="utf-8"><base href="./"><title>Practice HUD</title><link rel="stylesheet" href="practice.css"></head>
<body><div id="app">Loading…</div><script src="_framework/blazor.webassembly.js"></script></body></html>
  

wwwroot/practice.css

    /* SPDX-License-Identifier: GPL-3.0-only */
html, body { margin: 0; background: transparent; color: #fff; font-family: system-ui, sans-serif; }
main { padding: 24px; font-size: 24px; }
p { margin: 0 0 12px; }
  

Check the behavior in your HUD

  • Switch observed players: health must follow the supplied player; do not choose a substitute when no player is supplied.
  • Check unknown health, 0, restored health and stale/disconnected GSI. A missing sample remains unknown.
  • Run and pause the countdown; only the timed region should need its local cadence.
  • Save the panel caption, trigger show/hide, interrupt the connection and accept a new baseline. A previous banner stays hidden after reset.
  • Remove and reinsert a component: no duplicate listeners or actions. Check the finished package in the browser and OBS.

Original sources and licenses

These source examples are adapted derivative examples for this guide under GPL-3.0-only. Copyright (c) 2026 sofc; modifications for the sOU documentation 2026. The complete files are supplied above. Original graphics, fonts and private layout references are not included. The SDK, NuGet packages and SDK templates retain their MIT license; these example notices do not change that license.

Back to top

Image viewer