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.Tilemaps; using MrGameEng.UI; using Myra.Graphics2D.UI; namespace MrGameEng.Sample.Scenes; /// /// Интерактивная демо-сцена: ассеты через сгенерированные хендлы, анимация, слои /// (мир + Y-sort + screen-space HUD), камера с зумом/поворотом, ввод, звук и музыка. /// WASD — игрок, колесо — зум, Q/E — поворот, Space — звук, P — пауза, M — музыка, Tab — стресс-сцена. /// 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() ?? Context.UseAssets(); var audio = Context.Services.GetOrDefault() ?? 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); this.UseTilemaps(); 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)), }; // Тайловая площадка под стартовой зоной: шахматный пол, строится кодом из TileSet/TileGrid. // Видимые клетки сабмитятся каждый кадр; Depth -10 кладёт пол под декорации. var floorTiles = new TileSet(); var floorA = floorTiles.Add(shapeRegions[0], new Color(70, 75, 95)); var floorB = floorTiles.Add(shapeRegions[2], new Color(55, 60, 78)); var floorGrid = new TileGrid(24, 14); for (var y = 0; y < floorGrid.Height; y++) { for (var x = 0; x < floorGrid.Width; x++) { floorGrid[x, y] = (x + y) % 2 == 0 ? floorA : floorB; } } Store.CreateEntity(new Tilemap(floorGrid, floorTiles, tileSize: 48f) { Origin = new Vector2(-24 * 24f, -14 * 24f), Depth = -10f, }); // Декорации по всему миру: уезжаешь камерой — попадают под 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)); } } }