CI / build-test (push) Failing after 1m13s
AtlasBuilder packs a directory tree of loose images into atlas pages plus JSON metadata (deterministic shelf packing, incremental rebuilds, orphan cleanup); TextureAtlas loads them back handing out Texture2DRegions, so sprites from one page batch into a single draw call. The asset handle generator maps .atlas files to TextureAtlas and skips page images. Demonstrated in the sample (Assets/Atlases + 'atlas' console command), wrapped as tools/MrGameEng.AtlasTool for build scripts. Documented dependency exception: Atlases depends on Graphics and Assets.
172 lines
8.2 KiB
C#
172 lines
8.2 KiB
C#
using Friflo.Engine.ECS;
|
|
using Microsoft.Xna.Framework;
|
|
using MrGameEng.Assets;
|
|
using MrGameEng.Atlases;
|
|
using MrGameEng.Audio;
|
|
using MrGameEng.Core;
|
|
using MrGameEng.DevConsole;
|
|
using MrGameEng.Graphics;
|
|
using MrGameEng.Input;
|
|
using MrGameEng.UI;
|
|
using Myra.Graphics2D.UI;
|
|
|
|
namespace MrGameEng.Sample.Scenes;
|
|
|
|
/// <summary>
|
|
/// Интерактивная демо-сцена: ассеты через сгенерированные хендлы, анимация, слои
|
|
/// (мир + Y-sort + screen-space HUD), камера с зумом/поворотом, ввод, звук и музыка.
|
|
/// WASD — игрок, колесо — зум, Q/E — поворот, Space — звук, P — пауза, M — музыка, Tab — стресс-сцена.
|
|
/// </summary>
|
|
public sealed class MainScene : Scene
|
|
{
|
|
private const int DecorCount = 1500;
|
|
private static readonly RectF WorldBounds = new(-2000f, -2000f, 4000f, 4000f);
|
|
|
|
protected override void OnLoad()
|
|
{
|
|
var assets = Context.Services.GetOrDefault<AssetManager>() ?? Context.UseAssets();
|
|
var audio = Context.Services.GetOrDefault<AudioManager>() ?? Context.UseAudio();
|
|
var input = this.UseInput();
|
|
var actions = SampleInput.CreateActions(input);
|
|
this.UseSpriteAnimation();
|
|
|
|
var renderer = this.UseRenderer2D(new Renderer2DOptions { VirtualResolution = new Point(1280, 720) });
|
|
SampleLayers.EnsureRegistered(renderer);
|
|
|
|
var playerTexture = assets.Load(GameAssets.Textures.Player);
|
|
var shapesTexture = assets.Load(GameAssets.Textures.Shapes);
|
|
var beep = assets.Load(GameAssets.Sounds.Beep);
|
|
|
|
// Текстурный атлас: Assets/Atlases собран утилитой MrGameEng.AtlasTool из Assets/Textures
|
|
// (см. README). Регионы адресуются исходным путём без расширения и батчатся в один draw call.
|
|
Context.UseTextureAtlases();
|
|
var atlas = assets.Load(GameAssets.Atlases.Textures);
|
|
|
|
var shapeRegions = new[]
|
|
{
|
|
new Texture2DRegion(shapesTexture, new Rectangle(0, 0, 32, 32)),
|
|
new Texture2DRegion(shapesTexture, new Rectangle(32, 0, 32, 32)),
|
|
new Texture2DRegion(shapesTexture, new Rectangle(0, 32, 32, 32)),
|
|
new Texture2DRegion(shapesTexture, new Rectangle(32, 32, 32, 32)),
|
|
};
|
|
|
|
// Декорации по всему миру: уезжаешь камерой — попадают под culling (см. заголовок окна).
|
|
var random = new Random(42);
|
|
for (var i = 0; i < DecorCount; i++)
|
|
{
|
|
var sprite = new Sprite(shapeRegions[random.Next(shapeRegions.Length)]);
|
|
sprite.CenterOrigin();
|
|
sprite.Color = Color.White * 0.35f;
|
|
Store.CreateEntity(
|
|
new Transform2D(
|
|
new Vector2(
|
|
random.NextSingle() * WorldBounds.Width + WorldBounds.Left,
|
|
random.NextSingle() * WorldBounds.Height + WorldBounds.Top),
|
|
rotation: random.NextSingle() * MathF.Tau,
|
|
scale: new Vector2(0.5f + random.NextSingle())),
|
|
sprite);
|
|
}
|
|
|
|
// Бродячие "существа" на Y-sort слое: кто ниже на экране — тот ближе.
|
|
for (var i = 0; i < 200; i++)
|
|
{
|
|
var sprite = new Sprite(shapeRegions[random.Next(shapeRegions.Length)], SampleLayers.Actors);
|
|
sprite.CenterOrigin();
|
|
var angle = random.NextSingle() * MathF.Tau;
|
|
Store.CreateEntity(
|
|
new Transform2D(new Vector2(random.Next(-600, 600), random.Next(-400, 400))),
|
|
sprite,
|
|
new Velocity { Value = new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * (40f + random.Next(80)) });
|
|
}
|
|
|
|
// Игрок: анимированный спрайт (2 кадра из атласа player.png), Y-sort слой.
|
|
var playerSprite = new Sprite(new Texture2DRegion(playerTexture, new Rectangle(0, 0, 32, 32)), SampleLayers.Actors);
|
|
playerSprite.CenterOrigin();
|
|
var blink = new SpriteAnimationClip(
|
|
[
|
|
new Texture2DRegion(playerTexture, new Rectangle(0, 0, 32, 32)),
|
|
new Texture2DRegion(playerTexture, new Rectangle(32, 0, 32, 32)),
|
|
], framesPerSecond: 3f);
|
|
|
|
var player = Store.CreateEntity(
|
|
new Transform2D(Vector2.Zero, scale: new Vector2(2f)),
|
|
playerSprite,
|
|
new SpriteAnimator(blink));
|
|
|
|
// Пара спрайтов из атласа рядом со стартом игрока — вся пара рисуется одним draw call.
|
|
var atlasShowcase = new[] { ("player", -80f), ("shapes", 80f) };
|
|
foreach (var (key, offsetX) in atlasShowcase)
|
|
{
|
|
var sprite = new Sprite(atlas.GetRegion(key), SampleLayers.Actors);
|
|
sprite.CenterOrigin();
|
|
Store.CreateEntity(new Transform2D(new Vector2(offsetX, -120f)), sprite);
|
|
}
|
|
|
|
var camera = Store.CreateEntity(new Camera(Vector2.Zero, zoom: 1f, bounds: WorldBounds));
|
|
|
|
// HUD: золотой квадрат в углу на screen-space слое — не двигается с камерой.
|
|
var hud = new Sprite(shapeRegions[3], SampleLayers.Ui);
|
|
Store.CreateEntity(new Transform2D(new Vector2(16f, 16f)), hud);
|
|
|
|
// UI (Myra): HUD со статистикой, слайдер громкости, кнопка перехода. После UseRenderer2D!
|
|
var desktop = this.UseUI();
|
|
var statsLabel = new Label();
|
|
var volumeSlider = new HorizontalSlider { Minimum = 0f, Maximum = 1f, Value = audio.Music.Volume, Width = 180 };
|
|
volumeSlider.ValueChanged += (_, _) => audio.Music.Volume = volumeSlider.Value;
|
|
var switchButton = new Button { Content = new Label { Text = "Stress scene (Tab)" } };
|
|
switchButton.Click += (_, _) =>
|
|
{
|
|
if (!Context.Scenes.IsTransitioning)
|
|
{
|
|
Context.Scenes.Switch(new StressScene(), Transition.Fade(0.8f));
|
|
}
|
|
};
|
|
|
|
var panel = new VerticalStackPanel { Left = 12, Top = 12, Spacing = 6 };
|
|
panel.Widgets.Add(statsLabel);
|
|
panel.Widgets.Add(new Label { Text = "Music volume (M — pause)" });
|
|
panel.Widgets.Add(volumeSlider);
|
|
panel.Widgets.Add(switchButton);
|
|
desktop.Root = panel;
|
|
|
|
// Консоль разработчика (клавиша `) — последней, чтобы рисовалась поверх UI.
|
|
var console = this.UseDevConsole();
|
|
console.Register("stress", "switch to the stress scene", (_, _) =>
|
|
{
|
|
if (!Context.Scenes.IsTransitioning)
|
|
{
|
|
Context.Scenes.Switch(new StressScene(), Transition.Fade(0.8f));
|
|
}
|
|
});
|
|
console.Register("main", "switch to the main scene", (_, _) =>
|
|
{
|
|
if (!Context.Scenes.IsTransitioning)
|
|
{
|
|
Context.Scenes.Switch(new MainScene(), Transition.Fade(0.8f));
|
|
}
|
|
});
|
|
console.Register("beep", "play the beep sound", (_, _) => audio.Play(beep));
|
|
console.Register("atlas", "list texture atlas regions", (c, _) =>
|
|
{
|
|
c.WriteLine($"atlas '{atlas.Name}': {atlas.Pages.Count} page(s), {atlas.Regions.Count} region(s)");
|
|
foreach (var (key, region) in atlas.Regions.OrderBy(r => r.Key, StringComparer.Ordinal))
|
|
{
|
|
c.WriteLine($" {key}: {region.Bounds.Width}x{region.Bounds.Height} at ({region.Bounds.X},{region.Bounds.Y})");
|
|
}
|
|
});
|
|
|
|
UpdateSystems.Add(new PlayerControlSystem(player, actions, audio, beep, console));
|
|
UpdateSystems.Add(new BounceSystem(WorldBounds));
|
|
UpdateSystems.Add(new CameraControlSystem(camera, player, input));
|
|
UpdateSystems.Add(new SceneHotkeysSystem(Context, actions, () => new StressScene(), Transition.Fade(0.8f)));
|
|
UpdateSystems.Add(new StatsSystem(Context, renderer, "Main", statsLabel));
|
|
|
|
var music = audio.Music;
|
|
if (!music.IsPlaying)
|
|
{
|
|
music.Volume = 0.4f;
|
|
music.Play(assets.Load(GameAssets.Music.Theme));
|
|
}
|
|
}
|
|
}
|