Browser multiplayer client: promote the KNI spike to src/LittleSim.Web
LittleSim.Web (Blazor WASM + KNI/WebGL) is a real network client now: it connects to the dedicated server over WebSocket (ClientWebSocket maps to the browser socket), applies MrGameEng.Net delta snapshots into its EntityStore and draws pawns through SpriteBatch — fatigue dims them just like on desktop. The server address comes from ?server=ws://host:port in the page URL, defaulting to the page's host on port 9050. The net contract is mirrored in NetContract.cs (KNI and DesktopGL assemblies can't mix until the graphics libraries build per platform) with loud keep-in-sync comments on both sides. Both clients now smooth replicated positions between 10 Hz snapshots: NetLerp + NetSmoothingSystem lerp the visual position toward the latest server position every frame (exponential, ~0.25 s to converge). Verified against a live LittleSim.Server --listen: the browser client connects (server log), draws ~3.3k lit pixels of pawns whose layout changes between samples, and survives 400+ ticks without errors. Found along the way: requestAnimationFrame freezes in hidden windows — the game loop only runs while the tab is visible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e8273f9ffa
commit
580cb6ccc9
@@ -16,8 +16,12 @@ Game design docs live in `docs/` and are written in **Russian**. Engine rules li
|
|||||||
```
|
```
|
||||||
engine/ mrgameeng git submodule (own repo, own CLAUDE.md)
|
engine/ mrgameeng git submodule (own repo, own CLAUDE.md)
|
||||||
src/LittleSim the game (net8.0); references engine projects directly
|
src/LittleSim the game (net8.0); references engine projects directly
|
||||||
src/LittleSim.Server dedicated-server prototype: the world headless (engine HeadlessHost),
|
src/LittleSim.Server dedicated server: the world headless (engine HeadlessHost) +
|
||||||
loads mods/defs without atlases; the future network server grows here
|
WebSocket replication (MrGameEng.Net); also fast-forward and probe modes
|
||||||
|
src/LittleSim.Web browser client (Blazor WASM + KNI/WebGL): connects to the dedicated
|
||||||
|
server, renders replicated pawns; mirrors the net contract (NetContract.cs)
|
||||||
|
because KNI and DesktopGL assemblies can't mix — keep in sync with
|
||||||
|
src/LittleSim/Net/NetSchema.cs. Outside LittleSim.sln's test flow.
|
||||||
Mods/Core the game's own content as a mod: About, Defs, Languages, Textures
|
Mods/Core the game's own content as a mod: About, Defs, Languages, Textures
|
||||||
Cache/ runtime-built atlases (gitignored)
|
Cache/ runtime-built atlases (gitignored)
|
||||||
docs/ концепт, симуляция, моды, roadmap (Russian)
|
docs/ концепт, симуляция, моды, roadmap (Russian)
|
||||||
@@ -34,6 +38,7 @@ dotnet run --project src/LittleSim.Server -- --days 10 --tps 60 # headless fas
|
|||||||
dotnet run --project src/LittleSim.Server -- --listen # multiplayer world (WebSocket)
|
dotnet run --project src/LittleSim.Server -- --listen # multiplayer world (WebSocket)
|
||||||
dotnet run --project src/LittleSim -- --connect # client → ws://localhost:9050
|
dotnet run --project src/LittleSim -- --connect # client → ws://localhost:9050
|
||||||
dotnet run --project src/LittleSim.Server -- --probe # CLI check of a running server
|
dotnet run --project src/LittleSim.Server -- --probe # CLI check of a running server
|
||||||
|
dotnet run --project src/LittleSim.Web # browser client (?server=ws://…)
|
||||||
dotnet test LittleSim.sln # runs the engine test suites
|
dotnet test LittleSim.sln # runs the engine test suites
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Net", "engine\src
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Net.Tests", "engine\tests\MrGameEng.Net.Tests\MrGameEng.Net.Tests.csproj", "{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Net.Tests", "engine\tests\MrGameEng.Net.Tests\MrGameEng.Net.Tests.csproj", "{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LittleSim.Web", "src\LittleSim.Web\LittleSim.Web.csproj", "{270A9E52-E0E3-4885-B662-79E35AF6F7B4}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -301,6 +303,18 @@ Global
|
|||||||
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Release|x64.Build.0 = Release|Any CPU
|
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Release|x86.ActiveCfg = Release|Any CPU
|
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Release|x86.Build.0 = Release|Any CPU
|
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{270A9E52-E0E3-4885-B662-79E35AF6F7B4}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -328,5 +342,6 @@ Global
|
|||||||
{BEFB468B-6784-468E-9B60-EB44AB078D15} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
{BEFB468B-6784-468E-9B60-EB44AB078D15} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||||
{61C84FE7-1E0A-4CF2-A63F-39B23F936A7E} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
{61C84FE7-1E0A-4CF2-A63F-39B23F936A7E} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519}
|
||||||
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
{03EC6C39-E1DE-4C66-835F-4B05B5C40DB0} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
|
||||||
|
{270A9E52-E0E3-4885-B662-79E35AF6F7B4} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
+7
-1
@@ -1,6 +1,12 @@
|
|||||||
# Веб-клиент: spike KNI и решение A/B
|
# Веб-клиент: spike KNI и решение A/B
|
||||||
|
|
||||||
Дата: 2026-06-12. Спайк живёт в `spikes/KniWeb` (вне `LittleSim.sln`).
|
Дата: 2026-06-12. Спайк жил в `spikes/KniWeb`; после успеха повышен до
|
||||||
|
**`src/LittleSim.Web`** — рабочего браузерного клиента мультиплеера (Blazor WASM + KNI):
|
||||||
|
он подключается к `LittleSim.Server --listen` по WebSocket, применяет дельта-снапшоты
|
||||||
|
`MrGameEng.Net` и рисует жителей через WebGL со сглаживанием позиций. Адрес сервера —
|
||||||
|
`?server=ws://host:port` в URL страницы (по умолчанию — хост страницы, порт 9050).
|
||||||
|
Сетевой контракт там продублирован бинарным зеркалом (`NetContract.cs`) — KNI- и
|
||||||
|
DesktopGL-сборки нельзя смешивать, пока графика движка не собирается пер-платформенно.
|
||||||
|
|
||||||
## Вопрос
|
## Вопрос
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
|
|
||||||
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
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +1,63 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Nullable>disable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>disable</ImplicitUsings>
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
<RootNamespace>KniWebSpike</RootNamespace>
|
<RootNamespace>LittleSim.Web</RootNamespace>
|
||||||
<AssemblyName>KniWebSpike</AssemblyName>
|
<AssemblyName>LittleSim.Web</AssemblyName>
|
||||||
<DefineConstants>$(DefineConstants);BLAZORGL</DefineConstants>
|
<DefineConstants>$(DefineConstants);BLAZORGL</DefineConstants>
|
||||||
<KniPlatform>BlazorGL</KniPlatform>
|
<KniPlatform>BlazorGL</KniPlatform>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
|
<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
|
||||||
<!--<InvariantGlobalization>true</InvariantGlobalization>-->
|
</PropertyGroup>
|
||||||
</PropertyGroup>
|
|
||||||
|
<ItemGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "></PropertyGroup>
|
<Compile Include="Pages\Index.razor.cs" />
|
||||||
|
<Compile Include="Program.cs" />
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "></PropertyGroup>
|
<Compile Include="LittleSimWebGame.cs" />
|
||||||
|
<Compile Include="NetContract.cs" />
|
||||||
<ItemGroup>
|
<Compile Include="WorldViewScene.cs" />
|
||||||
<Compile Include="Pages\Index.razor.cs" />
|
</ItemGroup>
|
||||||
<Compile Include="Program.cs" />
|
|
||||||
<Compile Include="KniWebSpikeGame.cs" />
|
<ItemGroup>
|
||||||
<Compile Include="SpikeScene.cs" />
|
<!--
|
||||||
</ItemGroup>
|
Веб-клиент собирается против KNI (форк MonoGame с платформой Blazor/WebGL), поэтому
|
||||||
|
ссылается только на платформо-независимые библиотеки движка: Core и Net.
|
||||||
<ItemGroup>
|
Графические библиотеки движка (DesktopGL) сюда подключать нельзя — у KNI свои
|
||||||
<!-- Суть спайка: платформо-независимое ядро движка + Friflo внутри Blazor WASM. -->
|
сборки с теми же неймспейсами.
|
||||||
<ProjectReference Include="..\..\engine\src\MrGameEng.Core\MrGameEng.Core.csproj" />
|
-->
|
||||||
</ItemGroup>
|
<ProjectReference Include="..\..\engine\src\MrGameEng.Core\MrGameEng.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\..\engine\src\MrGameEng.Net\MrGameEng.Net.csproj" />
|
||||||
<ItemGroup>
|
</ItemGroup>
|
||||||
<PackageReference Include="nkast.Xna.Framework" Version="4.2.9001" />
|
|
||||||
<PackageReference Include="nkast.Xna.Framework.Content" Version="4.2.9001" />
|
<ItemGroup>
|
||||||
<PackageReference Include="nkast.Xna.Framework.Graphics" Version="4.2.9001" />
|
<PackageReference Include="nkast.Xna.Framework" Version="4.2.9001" />
|
||||||
<PackageReference Include="nkast.Xna.Framework.Audio" Version="4.2.9001" />
|
<PackageReference Include="nkast.Xna.Framework.Content" Version="4.2.9001" />
|
||||||
<PackageReference Include="nkast.Xna.Framework.Media" Version="4.2.9001" />
|
<PackageReference Include="nkast.Xna.Framework.Graphics" Version="4.2.9001" />
|
||||||
<PackageReference Include="nkast.Xna.Framework.Input" Version="4.2.9001" />
|
<PackageReference Include="nkast.Xna.Framework.Audio" Version="4.2.9001" />
|
||||||
<PackageReference Include="nkast.Xna.Framework.Game" Version="4.2.9001" />
|
<PackageReference Include="nkast.Xna.Framework.Media" Version="4.2.9001" />
|
||||||
<PackageReference Include="nkast.Xna.Framework.Devices" Version="4.2.9001" />
|
<PackageReference Include="nkast.Xna.Framework.Input" Version="4.2.9001" />
|
||||||
<PackageReference Include="nkast.Xna.Framework.Storage" Version="4.2.9001" />
|
<PackageReference Include="nkast.Xna.Framework.Game" Version="4.2.9001" />
|
||||||
<PackageReference Include="nkast.Xna.Framework.XR" Version="4.2.9001" />
|
<PackageReference Include="nkast.Xna.Framework.Devices" Version="4.2.9001" />
|
||||||
<PackageReference Include="nkast.Kni.Platform.Blazor.GL" Version="4.2.9001.2" />
|
<PackageReference Include="nkast.Xna.Framework.Storage" Version="4.2.9001" />
|
||||||
<PackageReference Include="nkast.Xna.Framework.Content.Pipeline.Builder" Version="4.2.9001" />
|
<PackageReference Include="nkast.Xna.Framework.XR" Version="4.2.9001" />
|
||||||
</ItemGroup>
|
<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 Condition=" '$(TargetFramework)' == 'net8.0' ">
|
</ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.17" />
|
|
||||||
<PackageReference
|
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
|
||||||
Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer"
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.17" />
|
||||||
Version="8.0.17"
|
<PackageReference
|
||||||
PrivateAssets="all"
|
Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer"
|
||||||
/>
|
Version="8.0.17"
|
||||||
</ItemGroup>
|
PrivateAssets="all"
|
||||||
|
/>
|
||||||
<ItemGroup>
|
</ItemGroup>
|
||||||
<KniContentReference Include="Content\KniWebSpikeContent.mgcb" />
|
|
||||||
</ItemGroup>
|
<ItemGroup>
|
||||||
</Project>
|
<KniContentReference Include="Content\LittleSimWebContent.mgcb" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
|
||||||
|
namespace LittleSim.Web;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Веб-хост LittleSim поверх KNI (BlazorGL/WebGL) — браузерный аналог
|
||||||
|
/// MrGameEng.Host.GameHost: владеет EngineContext ядра, гонит GameClock и фазы сцены.
|
||||||
|
/// Цикл тикается из requestAnimationFrame (см. Pages/Index.razor.cs). Когда графика
|
||||||
|
/// движка научится собираться под KNI, этот класс переедет в MrGameEng.Host.Web.
|
||||||
|
/// </summary>
|
||||||
|
public class LittleSimWebGame : Game
|
||||||
|
{
|
||||||
|
/// <summary>Контекст ядра движка, общий со сценами и системами.</summary>
|
||||||
|
public EngineContext Context { get; } = new EngineContext();
|
||||||
|
|
||||||
|
private readonly Uri _server;
|
||||||
|
private GraphicsDeviceManager _graphics;
|
||||||
|
private SpriteBatch _spriteBatch = null!;
|
||||||
|
private Texture2D _pixel = null!;
|
||||||
|
|
||||||
|
/// <summary>Игра, подключающаяся к серверу <paramref name="server"/>.</summary>
|
||||||
|
public LittleSimWebGame(Uri server)
|
||||||
|
{
|
||||||
|
_server = server;
|
||||||
|
_graphics = new GraphicsDeviceManager(this);
|
||||||
|
Content.RootDirectory = "Content";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
Context.Scenes.Switch(new WorldViewScene(_server, () => _spriteBatch, () => _pixel));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void LoadContent()
|
||||||
|
{
|
||||||
|
_spriteBatch = new SpriteBatch(GraphicsDevice);
|
||||||
|
_pixel = new Texture2D(GraphicsDevice, 1, 1);
|
||||||
|
_pixel.SetData(new[] { Color.White });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Update(GameTime gameTime)
|
||||||
|
{
|
||||||
|
Context.Clock.Advance((float)gameTime.ElapsedGameTime.TotalSeconds);
|
||||||
|
Context.Scenes.Update(Context.Clock);
|
||||||
|
base.Update(gameTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Draw(GameTime gameTime)
|
||||||
|
{
|
||||||
|
GraphicsDevice.Clear(new Color(12, 16, 24));
|
||||||
|
Context.Scenes.Draw(Context.Clock);
|
||||||
|
base.Draw(gameTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Net;
|
||||||
|
|
||||||
|
namespace LittleSim.Web;
|
||||||
|
|
||||||
|
// ВНИМАНИЕ: бинарное зеркало сетевого контракта десктопа (src/LittleSim/Net/NetSchema.cs).
|
||||||
|
// Веб-клиент не может ссылаться на LittleSim/MrGameEng.Graphics (они собраны против
|
||||||
|
// MonoGame DesktopGL, а тут KNI), поэтому реплицируемые компоненты продублированы
|
||||||
|
// со СТРОГО тем же лейаутом и порядком регистрации. Меняешь схему там — меняй здесь.
|
||||||
|
// Уйдёт после пер-платформенной сборки графических библиотек (см. docs/web-client.md).
|
||||||
|
|
||||||
|
/// <summary>Зеркало MrGameEng.Graphics.Transform2D: Position(8) + Rotation(4) + Scale(8).</summary>
|
||||||
|
public struct NetTransform : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Позиция в мировых координатах.</summary>
|
||||||
|
public Vector2 Position;
|
||||||
|
|
||||||
|
/// <summary>Поворот в радианах.</summary>
|
||||||
|
public float Rotation;
|
||||||
|
|
||||||
|
/// <summary>Масштаб (у жителей — размер квада в пикселях).</summary>
|
||||||
|
public Vector2 Scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Зеркало LittleSim.Sim.PawnNeeds: Energy(4).</summary>
|
||||||
|
public struct NetPawnNeeds : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Запас сил жителя в [0, 1] — затемняет спрайт.</summary>
|
||||||
|
public float Energy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Схема репликации веб-клиента — порядок тот же, что в NetSchema десктопа.</summary>
|
||||||
|
public static class WebNetSchema
|
||||||
|
{
|
||||||
|
/// <summary>Порт сервера по умолчанию (NetSchema.DefaultPort).</summary>
|
||||||
|
public const int DefaultPort = 9050;
|
||||||
|
|
||||||
|
/// <summary>Transform2D → NetTransform, PawnNeeds → NetPawnNeeds.</summary>
|
||||||
|
public static ReplicationSchema Create() =>
|
||||||
|
new ReplicationSchema().Register<NetTransform>().Register<NetPawnNeeds>();
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
@inject IJSRuntime JsRuntime
|
@inject IJSRuntime JsRuntime
|
||||||
@using nkast.Wasm.Canvas
|
@using nkast.Wasm.Canvas
|
||||||
|
|
||||||
<PageTitle>KniWebSpike</PageTitle>
|
<PageTitle>LittleSim</PageTitle>
|
||||||
|
|
||||||
<div id="canvasHolder" style="
|
<div id="canvasHolder" style="
|
||||||
background: #000;
|
background: #000;
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
|
||||||
|
namespace LittleSim.Web.Pages
|
||||||
|
{
|
||||||
|
public partial class Index
|
||||||
|
{
|
||||||
|
[Inject]
|
||||||
|
private NavigationManager Navigation { get; set; } = null!;
|
||||||
|
|
||||||
|
private Game? _game;
|
||||||
|
|
||||||
|
protected override void OnAfterRender(bool firstRender)
|
||||||
|
{
|
||||||
|
base.OnAfterRender(firstRender);
|
||||||
|
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
JsRuntime.InvokeAsync<object>("initRenderJS", DotNetObjectReference.Create(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public void TickDotNet()
|
||||||
|
{
|
||||||
|
if (_game == null)
|
||||||
|
{
|
||||||
|
_game = new LittleSimWebGame(ResolveServerUri());
|
||||||
|
_game.Run();
|
||||||
|
}
|
||||||
|
|
||||||
|
_game.Tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Адрес сервера: ?server=ws://host:port в URL страницы; по умолчанию —
|
||||||
|
// хост самой страницы на порту LittleSim.Server.
|
||||||
|
private Uri ResolveServerUri()
|
||||||
|
{
|
||||||
|
var page = new Uri(Navigation.Uri);
|
||||||
|
var query = page.Query.TrimStart('?');
|
||||||
|
foreach (var pair in query.Split('&', StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
{
|
||||||
|
var separator = pair.IndexOf('=');
|
||||||
|
if (separator > 0 && pair[..separator] == "server")
|
||||||
|
{
|
||||||
|
return new Uri(Uri.UnescapeDataString(pair[(separator + 1)..]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UriBuilder
|
||||||
|
{
|
||||||
|
Scheme = "ws",
|
||||||
|
Host = page.Host,
|
||||||
|
Port = WebNetSchema.DefaultPort,
|
||||||
|
}.Uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Components.Web;
|
|||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace KniWebSpike
|
namespace LittleSim.Web
|
||||||
{
|
{
|
||||||
internal class Program
|
internal class Program
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using MrGameEng.Core;
|
||||||
|
using MrGameEng.Net;
|
||||||
|
|
||||||
|
namespace LittleSim.Web;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Браузерный клиент мира LittleSim: подключается к дедикейтед-серверу
|
||||||
|
/// (LittleSim.Server --listen), применяет дельта-снапшоты в свой EntityStore и рисует
|
||||||
|
/// жителей через KNI SpriteBatch (WebGL). Симуляция целиком на сервере — сюда приезжают
|
||||||
|
/// только компоненты схемы (позиция + потребности); позиции сглаживаются до частоты
|
||||||
|
/// кадра, усталость затемняет квадратик жителя.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WorldViewScene : Scene
|
||||||
|
{
|
||||||
|
private readonly Uri _server;
|
||||||
|
private readonly Func<SpriteBatch> _spriteBatch;
|
||||||
|
private readonly Func<Texture2D> _pixel;
|
||||||
|
|
||||||
|
private ReplicationClient _replication = null!;
|
||||||
|
private Task<WebSocketClient>? _connecting;
|
||||||
|
private WebSocketClient? _connection;
|
||||||
|
|
||||||
|
/// <summary>Сцена, подключающаяся к <paramref name="server"/>.</summary>
|
||||||
|
public WorldViewScene(Uri server, Func<SpriteBatch> spriteBatch, Func<Texture2D> pixel)
|
||||||
|
{
|
||||||
|
_server = server;
|
||||||
|
_spriteBatch = spriteBatch;
|
||||||
|
_pixel = pixel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Сглаживание сетевой позиции: цель из снапшота, визуал лерпится покадрово.</summary>
|
||||||
|
private struct NetLerp : IComponent
|
||||||
|
{
|
||||||
|
public Vector2 Visual;
|
||||||
|
public Vector2 Target;
|
||||||
|
public bool Initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnLoad()
|
||||||
|
{
|
||||||
|
_replication = new ReplicationClient(WebNetSchema.Create(), Store);
|
||||||
|
_replication.EntitySpawned += entity => entity.AddComponent(new NetLerp());
|
||||||
|
|
||||||
|
UpdateSystems.Add(new CallbackSystem(Pump));
|
||||||
|
UpdateSystems.Add(new NetSmoothingSystem());
|
||||||
|
DrawSystems.Add(new PawnDrawSystem(_spriteBatch, _pixel));
|
||||||
|
|
||||||
|
Log.Info($"LittleSim.Web: connecting to {_server}…");
|
||||||
|
_connecting = WebSocketClient.ConnectAsync(_server);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUnload() => _connection?.Close();
|
||||||
|
|
||||||
|
private void Pump()
|
||||||
|
{
|
||||||
|
if (_connecting is { IsCompleted: true } finished)
|
||||||
|
{
|
||||||
|
_connecting = null;
|
||||||
|
if (finished.IsFaulted)
|
||||||
|
{
|
||||||
|
Log.Error(
|
||||||
|
$"Connect to {_server} failed: "
|
||||||
|
+ finished.Exception?.GetBaseException().Message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_connection = finished.Result;
|
||||||
|
Log.Info($"Connected to {_server}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_connection is not null)
|
||||||
|
{
|
||||||
|
_replication.Pump(_connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Вызывает делегат каждый тик — мелкая логика сцены без отдельного класса.</summary>
|
||||||
|
private sealed class CallbackSystem(Action update) : BaseSystem
|
||||||
|
{
|
||||||
|
protected override void OnUpdateGroup() => update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Та же экспонента, что в NetSmoothingSystem десктопа (LittleSim/Net/NetSmoothing.cs).</summary>
|
||||||
|
private sealed class NetSmoothingSystem : QuerySystem<NetTransform, NetLerp>
|
||||||
|
{
|
||||||
|
private const float Rate = 12f;
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
var blend = 1f - MathF.Exp(-Rate * Tick.deltaTime);
|
||||||
|
Query.ForEachEntity(
|
||||||
|
(ref NetTransform transform, ref NetLerp lerp, Entity _) =>
|
||||||
|
{
|
||||||
|
if (!lerp.Initialized)
|
||||||
|
{
|
||||||
|
lerp.Visual = lerp.Target = transform.Position;
|
||||||
|
lerp.Initialized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transform.Position != lerp.Visual)
|
||||||
|
{
|
||||||
|
lerp.Target = transform.Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
lerp.Visual = Vector2.Lerp(lerp.Visual, lerp.Target, blend);
|
||||||
|
transform.Position = lerp.Visual;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Житель — квадратик размером Scale, затемняющийся с усталостью (как на десктопе).</summary>
|
||||||
|
private sealed class PawnDrawSystem(Func<SpriteBatch> spriteBatch, Func<Texture2D> pixel)
|
||||||
|
: QuerySystem<NetTransform, NetPawnNeeds>
|
||||||
|
{
|
||||||
|
private const float MinBrightness = 0.45f;
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
var batch = spriteBatch();
|
||||||
|
var white = pixel();
|
||||||
|
batch.Begin();
|
||||||
|
Query.ForEachEntity(
|
||||||
|
(ref NetTransform transform, ref NetPawnNeeds needs, Entity _) =>
|
||||||
|
{
|
||||||
|
var size = transform.Scale;
|
||||||
|
var brightness =
|
||||||
|
MinBrightness + (1f - MinBrightness) * Math.Clamp(needs.Energy, 0f, 1f);
|
||||||
|
batch.Draw(
|
||||||
|
white,
|
||||||
|
new Rectangle(
|
||||||
|
(int)(transform.Position.X - size.X / 2f),
|
||||||
|
(int)(transform.Position.Y - size.Y / 2f),
|
||||||
|
(int)size.X,
|
||||||
|
(int)size.Y
|
||||||
|
),
|
||||||
|
Color.White * brightness
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
batch.End();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,4 +7,4 @@
|
|||||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using nkast.Wasm.Canvas
|
@using nkast.Wasm.Canvas
|
||||||
@using KniWebSpike
|
@using LittleSim.Web
|
||||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
@@ -4,11 +4,11 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
<title>KniWebSpike</title>
|
<title>LittleSim</title>
|
||||||
<base href="./" />
|
<base href="./" />
|
||||||
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||||
<link href="css/app.css" rel="stylesheet" />
|
<link href="css/app.css" rel="stylesheet" />
|
||||||
<link href="KniWebSpike.styles.css" rel="stylesheet" />
|
<link href="LittleSim.Web.styles.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
Before Width: | Height: | Size: 423 B After Width: | Height: | Size: 423 B |
@@ -8,6 +8,9 @@ namespace LittleSim.Net;
|
|||||||
/// Сетевой контракт LittleSim: какие компоненты реплицируются с сервера на клиентов.
|
/// Сетевой контракт LittleSim: какие компоненты реплицируются с сервера на клиентов.
|
||||||
/// Сервер (LittleSim.Server) и клиент (<see cref="Scenes.MultiplayerScene"/>) обязаны
|
/// Сервер (LittleSim.Server) и клиент (<see cref="Scenes.MultiplayerScene"/>) обязаны
|
||||||
/// строить схему одинаково — порядок регистрации определяет wire-id компонентов.
|
/// строить схему одинаково — порядок регистрации определяет wire-id компонентов.
|
||||||
|
/// ВНИМАНИЕ: у веб-клиента бинарное зеркало этой схемы
|
||||||
|
/// (src/LittleSim.Web/NetContract.cs — он собран против KNI и не может ссылаться сюда);
|
||||||
|
/// меняешь состав или порядок — меняй и там.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class NetSchema
|
public static class NetSchema
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using Friflo.Engine.ECS;
|
||||||
|
using Friflo.Engine.ECS.Systems;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using MrGameEng.Graphics;
|
||||||
|
|
||||||
|
namespace LittleSim.Net;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сглаживание сетевой позиции между снапшотами. Сервер шлёт ~10 снапшотов в секунду,
|
||||||
|
/// а рендер идёт на частоте кадра — без сглаживания жители телепортируются рывками.
|
||||||
|
/// Снапшот пишет в <see cref="Transform2D.Position"/>; система ловит это (позиция
|
||||||
|
/// разошлась с нарисованной), запоминает цель и каждый кадр экспоненциально подтягивает
|
||||||
|
/// видимую позицию к цели, записывая её обратно в трансформ для рендера.
|
||||||
|
/// </summary>
|
||||||
|
public struct NetLerp : IComponent
|
||||||
|
{
|
||||||
|
/// <summary>Нарисованная (сглаженная) позиция прошлого кадра.</summary>
|
||||||
|
public Vector2 Visual;
|
||||||
|
|
||||||
|
/// <summary>Последняя серверная позиция — цель сглаживания.</summary>
|
||||||
|
public Vector2 Target;
|
||||||
|
|
||||||
|
/// <summary>Ложь до первого кадра: стартуем точно с серверной позиции, без подлёта.</summary>
|
||||||
|
public bool Initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Двигает <see cref="NetLerp.Visual"/> к <see cref="NetLerp.Target"/> и пишет результат в
|
||||||
|
/// <see cref="Transform2D.Position"/>. Ставится после прокачки сети и до рендера.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NetSmoothingSystem : QuerySystem<Transform2D, NetLerp>
|
||||||
|
{
|
||||||
|
// Скорость экспоненциального сглаживания: за ~0.25 с визуал почти догоняет цель.
|
||||||
|
private const float Rate = 12f;
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
var blend = 1f - MathF.Exp(-Rate * Tick.deltaTime);
|
||||||
|
foreach (var (transforms, lerps, _) in Query.Chunks)
|
||||||
|
{
|
||||||
|
var t = transforms.Span;
|
||||||
|
var l = lerps.Span;
|
||||||
|
for (var i = 0; i < t.Length; i++)
|
||||||
|
{
|
||||||
|
ref var lerp = ref l[i];
|
||||||
|
ref var position = ref t[i].Position;
|
||||||
|
if (!lerp.Initialized)
|
||||||
|
{
|
||||||
|
lerp.Visual = lerp.Target = position;
|
||||||
|
lerp.Initialized = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Транформ трогает только снапшот: разошёлся с нарисованным — новая цель.
|
||||||
|
if (position != lerp.Visual)
|
||||||
|
{
|
||||||
|
lerp.Target = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
lerp.Visual = Vector2.Lerp(lerp.Visual, lerp.Target, blend);
|
||||||
|
position = lerp.Visual;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,6 +61,7 @@ public sealed class MultiplayerScene : Scene
|
|||||||
var sprite = new Sprite(white, GameLayers.Beings);
|
var sprite = new Sprite(white, GameLayers.Beings);
|
||||||
sprite.CenterOrigin();
|
sprite.CenterOrigin();
|
||||||
entity.AddComponent(sprite);
|
entity.AddComponent(sprite);
|
||||||
|
entity.AddComponent(new NetLerp());
|
||||||
};
|
};
|
||||||
|
|
||||||
var desktop = this.UseUI();
|
var desktop = this.UseUI();
|
||||||
@@ -68,6 +69,8 @@ public sealed class MultiplayerScene : Scene
|
|||||||
desktop.Root = Ui.Screen(_hud);
|
desktop.Root = Ui.Screen(_hud);
|
||||||
|
|
||||||
UpdateSystems.Add(new CallbackSystem(Pump));
|
UpdateSystems.Add(new CallbackSystem(Pump));
|
||||||
|
// Снапшоты приходят ~10 раз в секунду — сглаживаем позиции до частоты кадра.
|
||||||
|
UpdateSystems.Add(new NetSmoothingSystem());
|
||||||
// Усталость жителей видна и по сети: PawnNeeds реплицируется, спрайт темнеет локально.
|
// Усталость жителей видна и по сети: PawnNeeds реплицируется, спрайт темнеет локально.
|
||||||
UpdateSystems.Add(new PawnAppearanceSystem());
|
UpdateSystems.Add(new PawnAppearanceSystem());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user