A modern, high-fidelity 2D arcade space shooter built with Blazor WebAssembly (.NET 10, C# 14) and the HTML5 Canvas API. This project implements a high-performance hybrid architecture combining C# game engine logic with procedural JavaScript rendering and Web Audio API sound synthesis.
- Smooth 60 FPS Rendering: Driven by a native JavaScript
requestAnimationFrameloop rendering directly to HTML5 Canvas with neon glow styling, dynamic starfields, and particle explosions. - Hybrid C#/JS Architecture: Core game physics, state management, entity tracking, and wave mechanics are written in C# 14, while input handling, canvas rendering, and procedural audio run in JavaScript.
- Power-Ups System:
- 🛡️ Shield: Permanent protection until hit. Absorbs one collision with an asteroid, triggers an explosion, and grants temporary invulnerability.
- 🔥 Rapid Fire: Reduces weapon cooldown to
0.12sfor a rapid barrage (lasts8seconds). - 🔱 Triple Shot: Spreads shots in a three-way arc (lasts
8seconds).
- Leaderboard & Persistence: Top scores are saved locally in the browser's
localStorageand displayed asScore (Survival Time)(e.g.,12,500 (01:23)), sorted automatically with the highest score on top. - Mobile-Friendly: Includes desktop keyboard mappings and a responsive mobile touch-control overlay (Thrust, Rotate Left/Right, Fire).
- Dynamic Visuals: Screen shake on player hits/explosions, particle fireworks on wave completion, and retro scanline visual overlays.
| Action | Key Bindings |
|---|---|
| Thrust Forward | W / ArrowUp |
| Rotate Left | A / ArrowLeft |
| Rotate Right | D / ArrowRight |
| Brake / Slow Down | S / ArrowDown |
| Fire Laser | Spacebar |
| Pause / Resume | P |
| Restart Game | Spacebar (Only on Game Over screen) |
When loaded on mobile devices, interactive touch zones are displayed at the bottom of the screen:
- ◀ / ▶: Rotate ship left or right.
- ▲: Engage thrusters.
- ✸: Fire primary laser.
- Tap Game Over overlay to restart.
- Initial Lives: 3
- Starting Wave: Starts with 8 large asteroids. Each subsequent wave adds
2additional asteroids and increases their speed. - Invulnerability Window:
3seconds of safety granted upon respawning or after shield depletion. - Power-Up Drop Chance:
15%chance when any asteroid is destroyed. Pickup templates float in space for10seconds before disappearing. - Scoring Breakdown:
- 🪐 Large Asteroid: 20 points
- 🪐 Medium Asteroid: 50 points
- 🪐 Small Asteroid: 100 points
- .NET 10.0 SDK (for the Blazor app)
- .NET 8.0 SDK (for the API backend)
- Azure Functions Core Tools v4 (for local API hosting)
- Azurite (or an active Azure Storage connection string for local table storage emulator)
- Azure Static Web Apps CLI (
npm install -g @azure/static-web-apps-cli)
To run both the Blazor client and the Azure Functions API locally with proper proxying:
- Start the local Storage Emulator (Azurite):
# If installed via npm: azurite --silent - Start the API backend:
In a new terminal:
cd api dotnet restore func start # This starts the functions on http://localhost:7071
- Start the Blazor client:
In another terminal:
dotnet watch # This starts the app on http://localhost:5000 / https://localhost:5001 - Start the SWA CLI Emulator (Proxies both under one port):
In a third terminal:
swa start http://localhost:5000 --api-location http://localhost:7071
- Open your browser and navigate to
http://localhost:4280(the SWA emulator port). This port hosts your game and proxies/api/*requests to your local functions seamlessly.
If you just want to run the client-side game without the API:
dotnet runAnd open the hosting address (e.g. http://localhost:5000).
To generate a compiled bundle optimized for static hosting:
# Publish the client WASM
dotnet publish -c Release -o ./publish
# Copy staticwebapp config to output
cp staticwebapp.config.json ./publish/wwwroot/staticwebapp.config.json(The API will be built automatically from /api by the Azure SWA deployment action on git push).
dotnet test tests/Asteroids.Tests/Asteroids.Tests.csproj -c ReleaseStandalone Blazor WebAssembly is hosted on Azure Static Web Apps (not Linux App Service). Navigation fallback for client-side routes is configured in staticwebapp.config.json.
- Azure CLI installed and logged in:
az login - An active Azure subscription:
az account show
export RESOURCE_GROUP="MyTestRg"
export LOCATION="westeurope"
export SWA_NAME="my-blazor-swa-431"
export SWA_SKU="Free" # Free | Standard
export STORAGE_NAME="mystorage431" # Must be globally unique, 3-24 alphanumeric charactersaz group create \
--name "$RESOURCE_GROUP" \
--location "$LOCATION"Skip this step if the group already exists.
Create the app with Other as the source (no GitHub link yet). CI/CD is wired via the workflow and deployment token below.
az staticwebapp create \
--name "$SWA_NAME" \
--resource-group "$RESOURCE_GROUP" \
--location "$LOCATION" \
--sku "$SWA_SKU"# Default hostname (e.g. https://….azurestaticapps.net)
az staticwebapp show \
--name "$SWA_NAME" \
--resource-group "$RESOURCE_GROUP" \
--query "defaultHostname" -o tsv
# Deployment token (keep secret — used by GitHub Actions)
az staticwebapp secrets list \
--name "$SWA_NAME" \
--resource-group "$RESOURCE_GROUP" \
--query "properties.apiKey" -o tsvLocal deploys use the Azure Static Web Apps CLI (npm package @azure/static-web-apps-cli). The binary is swa. You need Node.js / npm and a deployment token (same secret as GitHub Actions).
dotnet publish Asteroids.csproj -c Release -o ./publish
cp staticwebapp.config.json ./publish/wwwroot/staticwebapp.config.jsonexport SWA_CLI_DEPLOYMENT_TOKEN="$(az staticwebapp secrets list \
-g "$RESOURCE_GROUP" -n "$SWA_NAME" \
--query "properties.apiKey" -o tsv)"Or pass --deployment-token "…" on every command instead of setting the env var.
| Approach | When to use | How you invoke deploy |
|---|---|---|
A. Global swa |
Daily local use; CLI already installed | swa deploy … |
B. npx |
No global install; clean machine; scripts/docs that should “just work” | npx @azure/static-web-apps-cli deploy … |
There is no functional difference for deploy: same CLI, same flags, same token. npx only changes how the package is downloaded/run.
A — Install once globally, then use swa (recommended for regular local deploys)
npm install -g @azure/static-web-apps-cli
swa deploy ./publish/wwwroot --env production
# or explicitly:
# swa deploy ./publish/wwwroot --deployment-token "$SWA_CLI_DEPLOYMENT_TOKEN" --env productionB — Run via npx (no global install)
npx @azure/static-web-apps-cli deploy ./publish/wwwroot --env production
# or:
# npx @azure/static-web-apps-cli deploy ./publish/wwwroot \
# --deployment-token "$SWA_CLI_DEPLOYMENT_TOKEN" --env productionOptional: pin a CLI version with npx @azure/static-web-apps-cli@2 ….
Note: GitHub Actions does not use
swaon the runner. CI uses theAzure/static-web-apps-deployaction with secretAZURE_STATIC_WEB_APPS_API_TOKEN(see GitHub CI/CD below).
# Delete only the Static Web App
az staticwebapp delete \
--name "$SWA_NAME" \
--resource-group "$RESOURCE_GROUP" \
--yes
# Or delete the entire resource group
az group delete --name "$RESOURCE_GROUP" --yes --no-waitTo make the global leaderboard work, the backend API requires access to an Azure Storage Table. You can set this up using the Azure CLI using either a secure System-Assigned Managed Identity (RBAC) or a traditional Connection String.
Ensure you have your variables configured:
# Verify variables are set in your current shell
echo "Group: $RESOURCE_GROUP, SWA: $SWA_NAME, Storage: $STORAGE_NAME, Location: $LOCATION"Run these commands to provision the storage resource and create the high-scores table:
# 1. Create a Standard LRS Storage Account
az storage account create \
--name "$STORAGE_NAME" \
--resource-group "$RESOURCE_GROUP" \
--location "$LOCATION" \
--sku Standard_LRS
# 2. Create the Table named "AsteroidsLeaderboard"
az storage table create \
--name "AsteroidsLeaderboard" \
--account-name "$STORAGE_NAME"Note
Free vs. Standard SWA Tier:
- Option A (Managed Identity) requires the Standard SKU (paid tier) of Azure Static Web Apps. The Free SKU does not support managed identities.
- Option B (Connection String) is fully supported on the Free SKU and allows you to keep hosting 100% free.
This configures the Azure Static Web App to authenticate using its system-assigned identity to connect securely via Entra ID (Azure AD):
# 1. Upgrade SWA to Standard SKU (required for managed identities)
az staticwebapp update \
--name "$SWA_NAME" \
--resource-group "$RESOURCE_GROUP" \
--sku Standard
# 2. Enable System-Assigned Managed Identity on SWA using raw REST PATCH (bypasses Azure CLI serialization bugs)
export SUBSCRIPTION_ID=$(az account show --query "id" -o tsv)
export PRINCIPAL_ID=$(az rest --method patch \
--uri "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Web/staticSites/$SWA_NAME?api-version=2022-03-01" \
--body '{"identity":{"type":"SystemAssigned"}}' \
--query "identity.principalId" \
-o tsv)
# 3. Retrieve the Storage Account's Azure resource ID
export STORAGE_ID=$(az storage account show \
--name "$STORAGE_NAME" \
--resource-group "$RESOURCE_GROUP" \
--query "id" \
-o tsv)
# 4. Assign "Storage Table Data Contributor" role to SWA's identity at the Storage Account scope
az role assignment create \
--assignee "$PRINCIPAL_ID" \
--role "Storage Table Data Contributor" \
--scope "$STORAGE_ID"
# 5. Set the environment variables on SWA to activate Managed Identity connection mode
az staticwebapp appsettings set \
--name "$SWA_NAME" \
--resource-group "$RESOURCE_GROUP" \
--setting-names \
"TableStorageUri=https://$STORAGE_NAME.table.core.windows.net" \
"TableStorageConnectionString=UseManagedIdentity"If you are using the Free SWA tier, configure SWA with the Storage Account connection string:
# 1. Retrieve the connection string from the storage account keys
export CONNECTION_STRING=$(az storage account show-connection-string \
--name "$STORAGE_NAME" \
--resource-group "$RESOURCE_GROUP" \
--query "connectionString" \
-o tsv)
# 2. Set the connection string variable on SWA
az staticwebapp appsettings set \
--name "$SWA_NAME" \
--resource-group "$RESOURCE_GROUP" \
--setting-names "TableStorageConnectionString=$CONNECTION_STRING"Pipeline file: .github/workflows/azure-static-web-apps.yml
| Trigger | Job | Behavior |
|---|---|---|
push to main |
Build, Test, and Deploy | Restore → build → test → publish → deploy to production SWA |
pull_request opened/sync/reopened on main |
Build, Test, and Deploy | Same pipeline; SWA creates a staging environment for the PR |
pull_request closed |
Close Pull Request | Tears down the SWA staging environment |
Steps in the main job:
- Checkout repository
- Setup .NET
10.0.x - Restore app + test projects
- Build
Asteroids.csproj(Release) - Test
tests/Asteroids.Tests/Asteroids.Tests.csproj - Publish to
./publishand copystaticwebapp.config.jsonintowwwroot - Deploy with
Azure/static-web-apps-deploy@v1(skip_app_build: true, uploadpublish/wwwroot)
| Secret name | Description |
|---|---|
AZURE_STATIC_WEB_APPS_API_TOKEN |
Deployment token for the Static Web App (API key) |
GITHUB_TOKEN is provided automatically by Actions (used for PR comments / staging). You do not add it manually.
- Open the repo on GitHub → Settings → Secrets and variables → Actions
- New repository secret
- Name:
AZURE_STATIC_WEB_APPS_API_TOKEN - Value: output of:
az staticwebapp secrets list \
--name "$SWA_NAME" \
--resource-group "$RESOURCE_GROUP" \
--query "properties.apiKey" -o tsv# Requires: gh auth login
az staticwebapp secrets list \
--name "$SWA_NAME" \
--resource-group "$RESOURCE_GROUP" \
--query "properties.apiKey" -o tsv \
| gh secret set AZURE_STATIC_WEB_APPS_API_TOKENgit add .github/workflows/azure-static-web-apps.yml
git commit -m "Add Azure Static Web Apps CI/CD workflow"
git push origin mainThen open Actions on GitHub and confirm the workflow is green. The live site is:
https://<defaultHostname from az staticwebapp show>
Example (this deployment): https://ashy-sand-0ff80b503.7.azurestaticapps.net
| Symptom | What to check |
|---|---|
deployment_token was not provided |
Secret is missing or empty. Create Actions secret AZURE_STATIC_WEB_APPS_API_TOKEN with the SWA deployment token (see above). Fork PR workflows cannot read repo secrets. |
| Deploy step fails with unauthorized | Wrong token, or token for a different SWA app; re-copy from Portal / az staticwebapp secrets list |
Warning: Unexpected input skip_api_build |
Not a valid input for Azure/static-web-apps-deploy@v1 — omit it (workflow already does) |
| Tests fail in CI | Run dotnet test tests/Asteroids.Tests/Asteroids.Tests.csproj -c Release locally |
| Blank page / 404 on deep links | Ensure staticwebapp.config.json is in the deployed wwwroot (workflow copies it) |
| Wrong .NET version | Workflow pins 10.0.x to match TargetFramework in Asteroids.csproj |
For a detailed walkthrough of the game loop sequence, data structures, JS interop boundaries, and coordinate systems, see ARCHITECTURE.md.