Multiplayer: headless dedicated server, MrGameEng.Net, desktop and browser clients #1
@@ -0,0 +1,62 @@
|
||||
# Веб-клиент: spike KNI и решение A/B
|
||||
|
||||
Дата: 2026-06-12. Спайк живёт в `spikes/KniWeb` (вне `LittleSim.sln`).
|
||||
|
||||
## Вопрос
|
||||
|
||||
Путь A — «MonoGame в браузере» через [KNI](https://github.com/kniEngine/kni)
|
||||
(форк MonoGame с платформой Blazor WebAssembly/WebGL, те же неймспейсы
|
||||
`Microsoft.Xna.Framework.*`). Путь B — тонкий веб-клиент без MonoGame
|
||||
(TypeScript/PixiJS поверх сетевой репликации). Спайк проверял минимальную
|
||||
жизнеспособность пути A: **ядро движка + Friflo + WebGL-спрайт в браузере**.
|
||||
|
||||
## Что сделано
|
||||
|
||||
`dotnet new kni-blazor-gl` (пакет шаблонов `nkast.Kni.Templates`, KNI 4.2.9001,
|
||||
net8.0) + ProjectReference на `engine/src/MrGameEng.Core` + мини-хост в духе
|
||||
`GameHost` поверх KNI `Game`. Сцена: 300 сущностей в Friflo `EntityStore`,
|
||||
`QuerySystem` двигает их в Update-фазе (отскок от краёв, seed 42), Draw-фаза
|
||||
рисует через KNI `SpriteBatch` (WebGL).
|
||||
|
||||
## Результат — путь A жизнеспособен
|
||||
|
||||
- **`MrGameEng.Core` работает в Blazor WASM без изменений**: `EngineContext`,
|
||||
`GameClock`, `Scene`/`SceneManager`, тайминг переходов — всё ядро завелось
|
||||
как есть (заслуга расслоения Core/Host: в ядре нет ни MonoGame, ни платформы).
|
||||
- **Friflo.Engine.ECS 3.6 работает в wasm**: создание сущностей, архетипы,
|
||||
`QuerySystem`, `ForEachEntity` — без ошибок в консоли браузера.
|
||||
- **KNI 4.2.9001 рендерит через WebGL** с XNA-API: `Game`,
|
||||
`GraphicsDeviceManager`, `SpriteBatch`, `Texture2D.SetData` — совпадает с
|
||||
кодом, который пишется под десктопный MonoGame.
|
||||
- Сборка тривиальна: обычный `Microsoft.NET.Sdk.BlazorWebAssembly` проект,
|
||||
никаких wasm-workload-плясок не понадобилось.
|
||||
|
||||
## Известные ограничения пути A (работа на этапе «веб-клиент»)
|
||||
|
||||
1. **Пер-платформенная компиляция библиотек движка.** `MrGameEng.Graphics`,
|
||||
`Content`, `Audio`, `UI` ссылаются на `MonoGame.Framework.DesktopGL`; для
|
||||
веба их надо собирать против пакетов `nkast.*` (типы те же по API, но другие
|
||||
сборки). Решение — msbuild-условие (`KniPlatform=BlazorGL` → nkast-пакеты),
|
||||
без изменения исходников.
|
||||
2. **Шейдеры.** `Renderer2D` использует прекомпилированные `dotnet-mgfxc`
|
||||
эффекты — KNI имеет собственный компилятор эффектов; совместимость надо
|
||||
проверять отдельным спайком, прежде чем тащить батчер в веб.
|
||||
3. **Нет файловой системы.** Моды/дефы/атласы в браузер приезжают по HTTP;
|
||||
текущая схема «собрать атласы при старте из PNG» в вебе не работает —
|
||||
атласы пре-билдятся и кладутся в `wwwroot` (или приезжают с сервера).
|
||||
4. **Потоки.** `Task.Run`-загрузка контента и `Thread.Sleep`-пейсинг не для
|
||||
браузера (клиенту `HeadlessHost.Run` и не нужен — цикл гонит
|
||||
`requestAnimationFrame` через KNI).
|
||||
5. **Производительность не мерялась** (300 спрайтов — гладко); бюджет
|
||||
сущностей в wasm-интерпретаторе будет заметно ниже десктопного, замерять
|
||||
на реальной сцене с включённым AOT.
|
||||
|
||||
## Решение
|
||||
|
||||
**Путь A (KNI)** — основной для веб-клиента: переиспользуем ядро, сцены и
|
||||
в перспективе графику движка; код игры один на все платформы. Путь B остаётся
|
||||
запасным, если упрёмся в шейдеры (п. 2) или производительность (п. 5).
|
||||
|
||||
Порядок работ не меняется: сначала `MrGameEng.Net` (WebSocket-транспорт +
|
||||
репликация — нужен любому пути) и сетевой мультиплеер на десктопе, затем
|
||||
`MrGameEng.Host.Web` поверх KNI по образцу спайка.
|
||||
@@ -0,0 +1,12 @@
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>Not found</PageTitle>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
#----------------------------- Global Properties ----------------------------#
|
||||
|
||||
/outputDir:bin/$(Platform)
|
||||
/intermediateDir:obj/$(Platform)
|
||||
/platform:BlazorGL
|
||||
/config:
|
||||
/profile:Reach
|
||||
/compress:True
|
||||
|
||||
#-------------------------------- References --------------------------------#
|
||||
|
||||
|
||||
#---------------------------------- Content ---------------------------------#
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<Project>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<RootNamespace>KniWebSpike</RootNamespace>
|
||||
<AssemblyName>KniWebSpike</AssemblyName>
|
||||
<DefineConstants>$(DefineConstants);BLAZORGL</DefineConstants>
|
||||
<KniPlatform>BlazorGL</KniPlatform>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
|
||||
<!--<InvariantGlobalization>true</InvariantGlobalization>-->
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="Pages\Index.razor.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="KniWebSpikeGame.cs" />
|
||||
<Compile Include="SpikeScene.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Суть спайка: платформо-независимое ядро движка + Friflo внутри Blazor WASM. -->
|
||||
<ProjectReference Include="..\..\engine\src\MrGameEng.Core\MrGameEng.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="nkast.Xna.Framework" Version="4.2.9001" />
|
||||
<PackageReference Include="nkast.Xna.Framework.Content" Version="4.2.9001" />
|
||||
<PackageReference Include="nkast.Xna.Framework.Graphics" Version="4.2.9001" />
|
||||
<PackageReference Include="nkast.Xna.Framework.Audio" Version="4.2.9001" />
|
||||
<PackageReference Include="nkast.Xna.Framework.Media" Version="4.2.9001" />
|
||||
<PackageReference Include="nkast.Xna.Framework.Input" Version="4.2.9001" />
|
||||
<PackageReference Include="nkast.Xna.Framework.Game" Version="4.2.9001" />
|
||||
<PackageReference Include="nkast.Xna.Framework.Devices" Version="4.2.9001" />
|
||||
<PackageReference Include="nkast.Xna.Framework.Storage" Version="4.2.9001" />
|
||||
<PackageReference Include="nkast.Xna.Framework.XR" Version="4.2.9001" />
|
||||
<PackageReference Include="nkast.Kni.Platform.Blazor.GL" Version="4.2.9001.2" />
|
||||
<PackageReference Include="nkast.Xna.Framework.Content.Pipeline.Builder" Version="4.2.9001" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.17" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.17" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<KniContentReference Include="Content\KniWebSpikeContent.mgcb" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.36811.4 d17.14
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KniWebSpike", "KniWebSpike.csproj", "{A902FD15-4463-413A-9D7E-9DB32E1DA469}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A902FD15-4463-413A-9D7E-9DB32E1DA469}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A902FD15-4463-413A-9D7E-9DB32E1DA469}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A902FD15-4463-413A-9D7E-9DB32E1DA469}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A902FD15-4463-413A-9D7E-9DB32E1DA469}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {56925F12-B776-4372-ACBD-785D2E46AE4E}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
using MrGameEng.Core;
|
||||
|
||||
namespace KniWebSpike
|
||||
{
|
||||
/// <summary>
|
||||
/// Мини-хост в духе MrGameEng.Host.GameHost, но поверх KNI (BlazorGL/WebGL):
|
||||
/// владеет EngineContext ядра, гонит GameClock и фазы сцены. Если этот класс
|
||||
/// работает в браузере — будущий MrGameEng.Host.Web реализуем.
|
||||
/// </summary>
|
||||
public class KniWebSpikeGame : Game
|
||||
{
|
||||
public EngineContext Context { get; } = new EngineContext();
|
||||
|
||||
private GraphicsDeviceManager _graphics;
|
||||
private SpriteBatch _spriteBatch;
|
||||
private Texture2D _pixel;
|
||||
|
||||
public KniWebSpikeGame()
|
||||
{
|
||||
_graphics = new GraphicsDeviceManager(this);
|
||||
Content.RootDirectory = "Content";
|
||||
}
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
var viewport = GraphicsDevice.Viewport;
|
||||
Context.Scenes.Switch(
|
||||
new SpikeScene(() => _spriteBatch, () => _pixel, viewport.Width, viewport.Height)
|
||||
);
|
||||
}
|
||||
|
||||
protected override void LoadContent()
|
||||
{
|
||||
_spriteBatch = new SpriteBatch(GraphicsDevice);
|
||||
_pixel = new Texture2D(GraphicsDevice, 1, 1);
|
||||
_pixel.SetData(new[] { Color.White });
|
||||
}
|
||||
|
||||
protected override void Update(GameTime gameTime)
|
||||
{
|
||||
Context.Clock.Advance((float)gameTime.ElapsedGameTime.TotalSeconds);
|
||||
Context.Scenes.Update(Context.Clock);
|
||||
base.Update(gameTime);
|
||||
}
|
||||
|
||||
protected override void Draw(GameTime gameTime)
|
||||
{
|
||||
GraphicsDevice.Clear(new Color(12, 16, 24));
|
||||
Context.Scenes.Draw(Context.Clock);
|
||||
base.Draw(gameTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
<main>
|
||||
@Body
|
||||
</main>
|
||||
</div>
|
||||
@@ -0,0 +1,98 @@
|
||||
.page
|
||||
{
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main
|
||||
{
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar
|
||||
{
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row
|
||||
{
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link
|
||||
{
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover
|
||||
{
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child
|
||||
{
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px)
|
||||
{
|
||||
.top-row:not(.auth)
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.top-row.auth
|
||||
{
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link
|
||||
{
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px)
|
||||
{
|
||||
.page
|
||||
{
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar
|
||||
{
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row
|
||||
{
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child
|
||||
{
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article
|
||||
{
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
@page "/"
|
||||
@page "/index.html"
|
||||
@inject IJSRuntime JsRuntime
|
||||
@using nkast.Wasm.Canvas
|
||||
|
||||
<PageTitle>KniWebSpike</PageTitle>
|
||||
|
||||
<div id="canvasHolder" style="
|
||||
background: #000;
|
||||
margin:0%;
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
width:100vw;
|
||||
height:100vh;
|
||||
">
|
||||
<canvas id="theCanvas" style="touch-action:none;"></canvas>
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using Microsoft.JSInterop;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace KniWebSpike.Pages
|
||||
{
|
||||
public partial class Index
|
||||
{
|
||||
Game _game;
|
||||
|
||||
protected override void OnAfterRender(bool firstRender)
|
||||
{
|
||||
base.OnAfterRender(firstRender);
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
JsRuntime.InvokeAsync<object>("initRenderJS", DotNetObjectReference.Create(this));
|
||||
}
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public void TickDotNet()
|
||||
{
|
||||
// init game
|
||||
if (_game == null)
|
||||
{
|
||||
_game = new KniWebSpikeGame();
|
||||
_game.Run();
|
||||
}
|
||||
|
||||
// run gameloop
|
||||
_game.Tick();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace KniWebSpike
|
||||
{
|
||||
internal class Program
|
||||
{
|
||||
private static async Task Main(string[] args)
|
||||
{
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
builder.Services.AddScoped(sp => new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
|
||||
});
|
||||
await builder.Build().RunAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:56897",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"KniWebSpike": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "http://localhost:5259",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using MrGameEng.Core;
|
||||
|
||||
namespace KniWebSpike
|
||||
{
|
||||
// Компоненты симуляции — платформо-независимые, как в LittleSim.
|
||||
public struct DotPosition : IComponent
|
||||
{
|
||||
public float X;
|
||||
public float Y;
|
||||
}
|
||||
|
||||
public struct DotVelocity : IComponent
|
||||
{
|
||||
public float X;
|
||||
public float Y;
|
||||
}
|
||||
|
||||
public struct DotTint : IComponent
|
||||
{
|
||||
public byte R;
|
||||
public byte G;
|
||||
public byte B;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Сцена спайка: 300 «спрайтов» в Friflo EntityStore, движение в Update-фазе
|
||||
/// (детерминированный seed, отскок от краёв), отрисовка в Draw-фазе через
|
||||
/// KNI SpriteBatch (WebGL). Сцена и системные корни — из MrGameEng.Core.
|
||||
/// </summary>
|
||||
public sealed class SpikeScene : Scene
|
||||
{
|
||||
private readonly Func<SpriteBatch> _spriteBatch;
|
||||
private readonly Func<Texture2D> _pixel;
|
||||
private readonly float _width;
|
||||
private readonly float _height;
|
||||
|
||||
public SpikeScene(Func<SpriteBatch> spriteBatch, Func<Texture2D> pixel, float width, float height)
|
||||
{
|
||||
_spriteBatch = spriteBatch;
|
||||
_pixel = pixel;
|
||||
_width = width;
|
||||
_height = height;
|
||||
}
|
||||
|
||||
protected override void OnLoad()
|
||||
{
|
||||
var random = new Random(42);
|
||||
for (var i = 0; i < 300; i++)
|
||||
{
|
||||
var angle = (float)(random.NextDouble() * Math.Tau);
|
||||
var speed = 40f + (float)random.NextDouble() * 160f;
|
||||
Store.CreateEntity(
|
||||
new DotPosition
|
||||
{
|
||||
X = (float)random.NextDouble() * _width,
|
||||
Y = (float)random.NextDouble() * _height,
|
||||
},
|
||||
new DotVelocity
|
||||
{
|
||||
X = MathF.Cos(angle) * speed,
|
||||
Y = MathF.Sin(angle) * speed,
|
||||
},
|
||||
new DotTint
|
||||
{
|
||||
R = (byte)random.Next(64, 256),
|
||||
G = (byte)random.Next(64, 256),
|
||||
B = (byte)random.Next(64, 256),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
UpdateSystems.Add(new BounceSystem(_width, _height));
|
||||
DrawSystems.Add(new DotDrawSystem(_spriteBatch, _pixel));
|
||||
}
|
||||
|
||||
private sealed class BounceSystem : QuerySystem<DotPosition, DotVelocity>
|
||||
{
|
||||
private readonly float _width;
|
||||
private readonly float _height;
|
||||
|
||||
public BounceSystem(float width, float height)
|
||||
{
|
||||
_width = width;
|
||||
_height = height;
|
||||
}
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
var delta = Tick.deltaTime;
|
||||
var width = _width;
|
||||
var height = _height;
|
||||
Query.ForEachEntity(
|
||||
(ref DotPosition pos, ref DotVelocity vel, Entity _) =>
|
||||
{
|
||||
pos.X += vel.X * delta;
|
||||
pos.Y += vel.Y * delta;
|
||||
if (pos.X < 0f || pos.X > width)
|
||||
{
|
||||
vel.X = -vel.X;
|
||||
pos.X = Math.Clamp(pos.X, 0f, width);
|
||||
}
|
||||
|
||||
if (pos.Y < 0f || pos.Y > height)
|
||||
{
|
||||
vel.Y = -vel.Y;
|
||||
pos.Y = Math.Clamp(pos.Y, 0f, height);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DotDrawSystem : QuerySystem<DotPosition, DotTint>
|
||||
{
|
||||
private readonly Func<SpriteBatch> _spriteBatch;
|
||||
private readonly Func<Texture2D> _pixel;
|
||||
|
||||
public DotDrawSystem(Func<SpriteBatch> spriteBatch, Func<Texture2D> pixel)
|
||||
{
|
||||
_spriteBatch = spriteBatch;
|
||||
_pixel = pixel;
|
||||
}
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
var batch = _spriteBatch();
|
||||
var pixel = _pixel();
|
||||
batch.Begin();
|
||||
Query.ForEachEntity(
|
||||
(ref DotPosition pos, ref DotTint tint, Entity _) =>
|
||||
{
|
||||
batch.Draw(
|
||||
pixel,
|
||||
new Rectangle((int)pos.X, (int)pos.Y, 6, 6),
|
||||
new Color(tint.R, tint.G, tint.B)
|
||||
);
|
||||
}
|
||||
);
|
||||
batch.End();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using nkast.Wasm.Canvas
|
||||
@using KniWebSpike
|
||||
@@ -0,0 +1,97 @@
|
||||
|
||||
html, body
|
||||
{
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1:focus
|
||||
{
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a, .btn-link
|
||||
{
|
||||
color: #0077cc;
|
||||
}
|
||||
|
||||
.btn-primary
|
||||
{
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.content
|
||||
{
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox])
|
||||
{
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid
|
||||
{
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
.validation-message
|
||||
{
|
||||
color: red;
|
||||
}
|
||||
|
||||
#blazor-error-ui
|
||||
{
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss
|
||||
{
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.blazor-error-boundary
|
||||
{
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after
|
||||
{
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
#theCanvas
|
||||
{
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
|
||||
/* Disable text highlighting and magnifying glass on iPhone/webkit */
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
|
||||
#canvas
|
||||
{
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,114 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>KniWebSpike</title>
|
||||
<base href="./" />
|
||||
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="css/app.css" rel="stylesheet" />
|
||||
<link href="KniWebSpike.styles.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="app">
|
||||
<div id="loading" style="display: table-cell; margin: auto; width:100vw; height:100vh; vertical-align: middle; background: #ffcc10;">
|
||||
<div style="display: block; margin: auto; width: 9em; color: white;font-family: 'Segoe UI', sans-serif;">
|
||||
<div style="text-align: center; font-size: 0.85em;">Made with<br/><a href="https://github.com/kniEngine/kni"><img src="kni.png" border="0" alt="Kni"></a></div>
|
||||
<div style="text-align: center; font-size: 1.8em;">loading <marquee style="width:0.9em; vertical-align: bottom;">. . . </marquee></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">x</a>
|
||||
</div>
|
||||
|
||||
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
|
||||
<script type="module">
|
||||
import { BrotliDecode } from './js/decode.min.js';
|
||||
window.BrotliDecode = BrotliDecode;
|
||||
// Set this to enable Brotli (.br) decompression on static webServers
|
||||
// that don't support content compression and http://.
|
||||
var enableBrotliDecompression = false;
|
||||
Blazor.start({
|
||||
loadBootResource: function (type, name, defaultUri, integrity)
|
||||
{
|
||||
if (enableBrotliDecompression === true && type !== 'dotnetjs' && location.hostname !== 'localhost')
|
||||
{
|
||||
return (async function()
|
||||
{
|
||||
const response = await fetch(defaultUri + '.br', { cache: 'no-cache' });
|
||||
if (!response.ok)
|
||||
throw new Error(response.statusText);
|
||||
const originalResponseBuffer = await response.arrayBuffer();
|
||||
const originalResponseArray = new Int8Array(originalResponseBuffer);
|
||||
const contentType = (type === 'dotnetwasm')
|
||||
? 'application/wasm'
|
||||
: 'application/octet-stream';
|
||||
const decompressedResponseArray = BrotliDecode(originalResponseArray);
|
||||
return new Response(decompressedResponseArray,
|
||||
{ headers: { 'content-type': contentType }
|
||||
});
|
||||
})();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script src="_content/nkast.Wasm.JSInterop/js/JSObject.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.Dom/js/Window.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.Dom/js/Document.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.Dom/js/Navigator.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.Dom/js/Gamepad.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.Dom/js/Media.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.XHR/js/XHR.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.Canvas/js/Canvas.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.Canvas/js/CanvasGLContext.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.Audio/js/Audio.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.XR/js/XR.8.0.11.js"></script>
|
||||
|
||||
<script>
|
||||
function tickJS()
|
||||
{
|
||||
window.theInstance.invokeMethod('TickDotNet');
|
||||
window.requestAnimationFrame(tickJS);
|
||||
}
|
||||
|
||||
window.initRenderJS = (instance) =>
|
||||
{
|
||||
window.theInstance = instance;
|
||||
|
||||
// set initial canvas size
|
||||
var canvas = document.getElementById('theCanvas');
|
||||
var holder = document.getElementById('canvasHolder');
|
||||
canvas.width = holder.clientWidth;
|
||||
canvas.height = holder.clientHeight;
|
||||
// disable context menu on right click
|
||||
canvas.addEventListener("contextmenu", e => e.preventDefault());
|
||||
|
||||
// begin game loop
|
||||
window.requestAnimationFrame(tickJS);
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", function(event)
|
||||
{
|
||||
// Prevent Arrows Keys and Spacebar scrolling the outer page
|
||||
// when running inside an iframe. e.g: itch.io embedding.
|
||||
if ([32, 37, 38, 39, 40].indexOf(event.keyCode) > -1)
|
||||
event.preventDefault();
|
||||
});
|
||||
window.addEventListener("wheel", function(event)
|
||||
{
|
||||
// Prevent Mousewheel scrolling the outer page
|
||||
// when running inside an iframe. e.g: itch.io embedding.
|
||||
event.preventDefault();
|
||||
}, { passive: false });
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,77 @@
|
||||
// micProcessor.js
|
||||
class MicProcessor extends AudioWorkletProcessor
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
|
||||
// global variables for testing
|
||||
var sampleRate = globalThis.sampleRate;
|
||||
var currentFrame = globalThis.currentFrame;
|
||||
var currentTime = globalThis.currentTime;
|
||||
var currentRenderQuantum = globalThis.currentRenderQuantum;
|
||||
|
||||
this.SampleRate = sampleRate;
|
||||
this.TargetSamples = Math.floor(this.SampleRate * 0.1); // 100ms
|
||||
this.Buffer = new Float32Array(this.TargetSamples);
|
||||
this.BufferIndex = 0;
|
||||
|
||||
this.port.onmessage = (event) =>
|
||||
{
|
||||
var data = event.data;
|
||||
|
||||
if (typeof data === 'number')
|
||||
{
|
||||
//this.port.postMessage(data); // echo back test
|
||||
}
|
||||
if (data instanceof Uint8Array)
|
||||
{
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters)
|
||||
{
|
||||
var inChannel0 = inputs[0][0];
|
||||
if (!inChannel0) return true;
|
||||
|
||||
let srcIndex = 0;
|
||||
var srcLen = inChannel0.length;
|
||||
|
||||
while (srcIndex < srcLen)
|
||||
{
|
||||
var remaining = this.TargetSamples - this.BufferIndex;
|
||||
var copyCount = Math.min(remaining, srcLen - srcIndex);
|
||||
|
||||
this.Buffer.set(
|
||||
inChannel0.subarray(srcIndex, srcIndex + copyCount),
|
||||
this.BufferIndex);
|
||||
|
||||
this.BufferIndex += copyCount;
|
||||
srcIndex += copyCount;
|
||||
|
||||
if (this.BufferIndex >= this.TargetSamples)
|
||||
{
|
||||
this.SendBuffer();
|
||||
this.BufferIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
SendBuffer()
|
||||
{
|
||||
// convert to 16-6bit PCM
|
||||
var int16 = new Int16Array(this.TargetSamples);
|
||||
for (var i = 0; i < this.TargetSamples; i++)
|
||||
{
|
||||
int16[i] = this.Buffer[i] * 32767;
|
||||
}
|
||||
|
||||
var byteArray = new Uint8Array(int16.buffer);
|
||||
this.port.postMessage(byteArray, [byteArray.buffer]);
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('mic-processor', MicProcessor);
|
||||
@@ -0,0 +1,87 @@
|
||||
// streamProcessor.js
|
||||
class StreamProcessor extends AudioWorkletProcessor
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
this.queue = [];
|
||||
|
||||
this.port.onmessage = (event) =>
|
||||
{
|
||||
var data = event.data;
|
||||
|
||||
if (typeof data === 'number')
|
||||
{
|
||||
if (data === 2)
|
||||
{
|
||||
this.queue = [];
|
||||
}
|
||||
}
|
||||
if (data instanceof Uint8Array)
|
||||
{
|
||||
const buffer = new Int16Array(data.buffer, data.byteOffset, data.length / 2);
|
||||
buffer.offset = 0;
|
||||
this.queue.push(buffer);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
process(inputs, outputs, parameters)
|
||||
{
|
||||
const output = outputs[0];
|
||||
|
||||
const channelCount = output.length;
|
||||
const sampleCount = output[0].length;
|
||||
|
||||
let written = 0;
|
||||
|
||||
while (written < sampleCount && this.queue.length > 0)
|
||||
{
|
||||
const buffer = this.queue[0];
|
||||
const offset = buffer.offset;
|
||||
|
||||
const available = buffer.length - offset;
|
||||
const needed = sampleCount - written;
|
||||
const copyCount = Math.min(available, needed);
|
||||
|
||||
for (let i = 0; i < copyCount; i++)
|
||||
{
|
||||
for (let c = 0; c < channelCount; c++)
|
||||
{
|
||||
const channel = output[c];
|
||||
let value = (buffer[offset+i] / 32767);
|
||||
channel[written+i] = value;
|
||||
}
|
||||
}
|
||||
|
||||
written += copyCount;
|
||||
buffer.offset += copyCount;
|
||||
|
||||
if (buffer.offset >= buffer.length)
|
||||
{
|
||||
this.queue.shift();
|
||||
this.port.postMessage(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Fill remaining samples with silence
|
||||
if (written < sampleCount)
|
||||
{
|
||||
for (let c = 0; c < channelCount; c++)
|
||||
{
|
||||
const channel = output[c];
|
||||
|
||||
for (let i = written; i < sampleCount; i++)
|
||||
{
|
||||
let value = 0;
|
||||
channel[i] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor("stream-processor", StreamProcessor);
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 423 B |
Reference in New Issue
Block a user