Multiplayer: headless dedicated server, MrGameEng.Net, desktop and browser clients #1
@@ -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